feat(agent): migrate OpenCode 2 runtime and sandbox tools
Upgrade the runtime and native plugin API to OpenCode 2.0.24. Restore session-scoped learning, credential renewal, typed project queries and controlled frontend actions from the legacy Agent. Replace host Shell execution with an approved Landlock/seccomp executor and persistent per-session workspaces. Add typed Bocha search and Tianditu geocoding bridges using the owning session credential. Require a working kernel sandbox before startup and persist workspace files in deployments. Validation completed before this commit: TypeScript checks, 150 Bun tests, Docker build and UID 10001 sandbox execution, native Shell approval and session recovery. Tianditu succeeded; Bocha currently reports exhausted account quota. Unrelated investigation-report contract work is excluded.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { CredentialRefreshCoordinator, runWithCredentialRefresh, bindSessionCredential, credentialRefreshCoordinator, refreshSessionCredential, releaseSessionCredential, requestWithSessionCredential } from "../../src/product/credential-refresh.js";
|
||||
|
||||
describe("credential renewal", () => {
|
||||
it("rejects stale replies and stops repeated requests when the renewed credential is still rejected", async () => {
|
||||
const session = "rejected-credential-session";
|
||||
bindSessionCredential(session, { authorization: "Bearer OLD", projectId: "p" });
|
||||
let calls = 0;
|
||||
let failures = 0;
|
||||
const unsubscribe = credentialRefreshCoordinator.subscribe(session, (event) => {
|
||||
if (event.type === "credential_refresh_required") {
|
||||
expect(refreshSessionCredential(session, { authorization: "Bearer NEW", projectId: "other" }, event.requestId)).toBeFalse();
|
||||
expect(refreshSessionCredential(session, { authorization: "Bearer NEW", projectId: "p" }, "stale")).toBeFalse();
|
||||
refreshSessionCredential(session, { authorization: "Bearer NEW", projectId: "p" }, event.requestId);
|
||||
} else if (event.type === "credential_refresh_failed") failures += 1;
|
||||
});
|
||||
try {
|
||||
expect((await requestWithSessionCredential(session, async () => { calls += 1; return new Response(null, { status: 401 }); })).status).toBe(401);
|
||||
await expect(requestWithSessionCredential(session, async () => { calls += 1; return new Response(null, { status: 200 }); })).rejects.toThrow("renewal failed");
|
||||
expect(calls).toBe(2);
|
||||
expect(failures).toBe(1);
|
||||
} finally { unsubscribe(); releaseSessionCredential(session); }
|
||||
});
|
||||
it("coalesces concurrent 401 responses, resumes both calls, and retries only once", async () => {
|
||||
const coordinator = new CredentialRefreshCoordinator(1000);
|
||||
const old = { authorization: "Bearer TEST_OLD", projectId: "p" };
|
||||
const renewed = { authorization: "Bearer TEST_NEW", projectId: "p" };
|
||||
let requests = 0;
|
||||
coordinator.subscribe("s", (event) => {
|
||||
if (event.type === "credential_refresh_required") {
|
||||
requests += 1;
|
||||
queueMicrotask(() => coordinator.resolve("s", event.requestId, renewed));
|
||||
}
|
||||
});
|
||||
const calls: string[] = [];
|
||||
const execute = async (credential: typeof old) => {
|
||||
calls.push(credential.authorization);
|
||||
return { status: credential.authorization === old.authorization ? 401 : 200 };
|
||||
};
|
||||
expect((await Promise.all([runWithCredentialRefresh(coordinator, "s", old, execute), runWithCredentialRefresh(coordinator, "s", old, execute)])).map((result) => result.status)).toEqual([200, 200]);
|
||||
expect(requests).toBe(1);
|
||||
expect(calls).toHaveLength(4);
|
||||
});
|
||||
it("does not refresh 403 and fails when refresh is unavailable or cancelled", async () => {
|
||||
const coordinator = new CredentialRefreshCoordinator(1000);
|
||||
const credential = { authorization: "Bearer TEST", projectId: "p" };
|
||||
expect((await runWithCredentialRefresh(coordinator, "s", credential, async () => ({ status: 403 }))).status).toBe(403);
|
||||
await expect(runWithCredentialRefresh(coordinator, "s", credential, async () => ({ status: 401 }))).rejects.toThrow("unavailable");
|
||||
coordinator.enableBackground("s");
|
||||
const pending = coordinator.request("s", "access_token_expired");
|
||||
coordinator.cancelSession("s");
|
||||
await expect(pending).rejects.toThrow("cancelled");
|
||||
});
|
||||
it("renews before near-expiry execution and exposes the same pending request for reconnect", async () => {
|
||||
const coordinator = new CredentialRefreshCoordinator(1000);
|
||||
const old = { authorization: "Bearer OLD", projectId: "p", expiresAt: new Date(Date.now() + 1000).toISOString() };
|
||||
coordinator.subscribe("s", (event) => {
|
||||
if (event.type === "credential_refresh_required") {
|
||||
expect(coordinator.getPendingEvent("s")?.requestId).toBe(event.requestId);
|
||||
coordinator.resolve("s", event.requestId, { authorization: "Bearer NEW", projectId: "p" });
|
||||
}
|
||||
});
|
||||
expect(await runWithCredentialRefresh(coordinator, "s", old, async (credential) => ({ status: 200, authorization: credential.authorization }))).toMatchObject({ authorization: "Bearer NEW" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { afterEach, describe, expect, it } from "bun:test";
|
||||
import { ExternalInformationClient, geocodeInput, webSearchInput } from "../../src/product/external-information-client.js";
|
||||
import { bindSessionCredential, credentialRefreshCoordinator, refreshSessionCredential, releaseSessionCredential } from "../../src/product/credential-refresh.js";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
const session = "external-information-test";
|
||||
afterEach(() => { globalThis.fetch = originalFetch; releaseSessionCredential(session); });
|
||||
describe("typed external information", () => {
|
||||
it("uses the fixed Server endpoint and trusted project, translating freshness", async () => {
|
||||
bindSessionCredential(session, { authorization: "Bearer TEST", projectId: "trusted" });
|
||||
globalThis.fetch = (async (url: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
||||
expect(String(url)).toBe("http://server.test/api/v1/web-searches");
|
||||
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer TEST");
|
||||
expect(new Headers(init?.headers).get("X-Project-Id")).toBe("trusted");
|
||||
expect(JSON.parse(String(init?.body))).toEqual({ query: "供水政策", freshness: "oneWeek", summary: true, count: 10, include: ["gov.cn"] });
|
||||
return Response.json({ data: { webPages: { value: [{ url: "https://gov.cn/example", name: "example" }] } } });
|
||||
}) as unknown as typeof fetch;
|
||||
const result = await new ExternalInformationClient("http://server.test").webSearch(session, webSearchInput.parse({ query: "供水政策", freshness: "one_week", include: ["gov.cn"] }));
|
||||
expect(result).toHaveProperty("data.webPages.value");
|
||||
});
|
||||
|
||||
it("normalizes valid Tianditu coordinates for zoom_to_map and rejects fabricated coordinates", async () => {
|
||||
bindSessionCredential(session, { authorization: "Bearer TEST", projectId: "trusted" });
|
||||
globalThis.fetch = (async (url: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
||||
expect(new URL(String(url)).pathname).toBe("/api/v1/geocoding-requests");
|
||||
expect(JSON.parse(String(init?.body))).toEqual({ keyword: "北京市人民政府" });
|
||||
return Response.json({ status: "0", location: { lon: "116.4", lat: "39.9", level: 1 } });
|
||||
}) as unknown as typeof fetch;
|
||||
expect(await new ExternalInformationClient("http://server.test").geocode(session, { keyword: "北京市人民政府" })).toMatchObject({ source_crs: "EPSG:4326", center: [116.4, 39.9], location: { lon: 116.4, lat: 39.9 } });
|
||||
for (const data of [null, { status: "0", location: { lon: "", lat: " " } }, { status: "1" }, { status: "0", location: { lon: 181, lat: 39 } }, { status: "0", location: { lon: null, lat: null } }]) {
|
||||
globalThis.fetch = (async () => Response.json(data)) as unknown as typeof fetch;
|
||||
await expect(new ExternalInformationClient("http://server.test").geocode(session, { keyword: "北京市人民政府" })).rejects.toThrow("no valid coordinate");
|
||||
}
|
||||
});
|
||||
|
||||
it("renews a rejected credential once and never turns a 403 into a renewal", async () => {
|
||||
bindSessionCredential(session, { authorization: "Bearer OLD", projectId: "trusted" });
|
||||
let requests = 0;
|
||||
const unsubscribe = credentialRefreshCoordinator.subscribe(session, (event) => {
|
||||
if (event.type === "credential_refresh_required") refreshSessionCredential(session, { authorization: "Bearer NEW", projectId: "trusted" }, event.requestId);
|
||||
});
|
||||
try {
|
||||
globalThis.fetch = (async (_url: Parameters<typeof fetch>[0], init?: RequestInit) => {
|
||||
requests++;
|
||||
return new Headers(init?.headers).get("Authorization") === "Bearer OLD" ? new Response(null, { status: 401 }) : Response.json({ status: "0", location: { lon: 116, lat: 39 } });
|
||||
}) as unknown as typeof fetch;
|
||||
await new ExternalInformationClient("http://server.test").geocode(session, { keyword: "北京" });
|
||||
expect(requests).toBe(2);
|
||||
globalThis.fetch = (async () => { requests++; return new Response(null, { status: 403 }); }) as unknown as typeof fetch;
|
||||
await expect(new ExternalInformationClient("http://server.test").geocode(session, { keyword: "北京" })).rejects.toMatchObject({ status: 403 });
|
||||
expect(requests).toBe(3);
|
||||
} finally { unsubscribe(); }
|
||||
});
|
||||
|
||||
it("rejects scope, URL overrides, empty queries and invalid counts before dispatch", async () => {
|
||||
expect(webSearchInput.safeParse({ query: "test", url: "http://untrusted" }).success).toBeFalse();
|
||||
expect(webSearchInput.safeParse({ query: "test", project_id: "other" }).success).toBeFalse();
|
||||
expect(webSearchInput.safeParse({ query: "test", count: 51 }).success).toBeFalse();
|
||||
expect(webSearchInput.safeParse({ query: "test", include: ["https://example.com/path"] }).success).toBeFalse();
|
||||
expect(geocodeInput.safeParse({ keyword: " " }).success).toBeFalse();
|
||||
await expect(new ExternalInformationClient().geocode(session, { keyword: "北京" })).rejects.toMatchObject({ status: 401 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { afterEach, describe, expect, it } from "bun:test";
|
||||
import { ProjectDataClient, projectDataQuerySchema } from "../../src/product/project-data-client.js";
|
||||
import { bindSessionCredential, releaseSessionCredential } from "../../src/product/credential-refresh.js";
|
||||
import { historyChart } from "../../src/product/history-chart.js";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
afterEach(() => { globalThis.fetch = originalFetch; releaseSessionCredential("query-session"); });
|
||||
describe("controlled project queries", () => {
|
||||
it("dispatches SCADA history to the existing fields endpoint using monitored_value", async () => {
|
||||
bindSessionCredential("query-session", { authorization: "Bearer TEST", projectId: "trusted-project" });
|
||||
globalThis.fetch = (async (url) => {
|
||||
const parsed = new URL(String(url));
|
||||
expect(parsed.pathname).toBe("/api/v1/timeseries/scada-readings/fields");
|
||||
expect(parsed.searchParams.get("field")).toBe("monitored_value");
|
||||
expect(parsed.searchParams.get("device_ids")).toBe("D1,D2");
|
||||
return Response.json([{ device_id: "D1", value: 20 }]);
|
||||
}) as typeof fetch;
|
||||
await new ProjectDataClient("http://server.test").query("query-session", { source: "scada_history", device_ids: ["D1", "D2"], field: "monitored_value", start: "2026-09-01T00:00:00Z", end: "2026-09-01T01:00:00Z" });
|
||||
});
|
||||
it("uses fixed routes and the bound project credential, including encoded asset IDs", async () => {
|
||||
bindSessionCredential("query-session", { authorization: "Bearer TEST", projectId: "trusted-project" });
|
||||
let requested: URL | undefined;
|
||||
globalThis.fetch = (async (url, init) => {
|
||||
requested = new URL(String(url));
|
||||
expect(new Headers(init?.headers).get("X-Project-Id")).toBe("trusted-project");
|
||||
return Response.json({ pressure: 20 });
|
||||
}) as typeof fetch;
|
||||
expect(await new ProjectDataClient("http://server.test").query("query-session", { source: "element_properties", element_type: "junction", element_id: "J&network=other" })).toEqual({ pressure: 20 });
|
||||
expect(requested?.pathname).toBe("/api/v1/junctions/properties");
|
||||
expect(requested?.searchParams.get("junction")).toBe("J&network=other");
|
||||
expect(requested?.searchParams.has("network")).toBeFalse();
|
||||
});
|
||||
it("rejects arbitrary routes, project overrides and excessive time windows before dispatch", () => {
|
||||
expect(projectDataQuerySchema.safeParse({ source: "scada_devices", project_id: "other" }).success).toBeFalse();
|
||||
expect(projectDataQuerySchema.safeParse({ source: "http", url: "http://other" }).success).toBeFalse();
|
||||
expect(projectDataQuerySchema.safeParse({ source: "scada_history", device_ids: ["D1"], field: "monitored_value", start: "2026-09-01T00:00:00Z", end: "2026-09-03T00:00:00Z" }).success).toBeFalse();
|
||||
});
|
||||
it("keeps returned units, drops null samples and never manufactures missing history", () => {
|
||||
expect(() => historyChart({ series: [] }, "J1")).toThrow("NO_HISTORY_DATA");
|
||||
const chart = historyChart({ series: [{ element_id: "J1", metric: "pressure", source: "scada_raw", source_unit: "m", points: [{ time: "t1", value: null }, { time: "t2", value: 20 }] }] }, "J1");
|
||||
expect(chart.title).toContain("(m)");
|
||||
expect(chart.x_data).toEqual(["t2"]);
|
||||
expect(chart.series[0].data).toEqual([20]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user