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.
56 lines
4.2 KiB
TypeScript
56 lines
4.2 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import express from "express";
|
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { attachTrustedAgentContext } from "../../src/context/localContext.js";
|
|
import { registerChatInteractionRoutes } from "../../src/routes/chatInteractionRoutes.js";
|
|
import { bindSessionCredential, credentialRefreshCoordinator, releaseSessionCredential } from "../../src/product/credential-refresh.js";
|
|
import { SessionMetadataStore } from "../../src/sessions/metadataStore.js";
|
|
import { SessionUiStateStore } from "../../src/sessions/uiStateStore.js";
|
|
import { toActorKey, toProjectKey } from "../../src/utils/fileStore.js";
|
|
import type { ActiveRun } from "../../src/routes/chatUiState.js";
|
|
import type { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";
|
|
|
|
describe("credential refresh HTTP route", () => {
|
|
it("binds fresh headers only after validating the session owner, project and pending request", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "credential-route-"));
|
|
const originalFetch = globalThis.fetch;
|
|
const metadata = new SessionMetadataStore(join(directory, "metadata"));
|
|
await metadata.initialize();
|
|
const projectId = "10000000-0000-4000-8000-000000000001";
|
|
await metadata.ensure({ sessionId: "credential-route-session", actorKey: toActorKey("product:owner"), projectKey: toProjectKey(`product:${projectId}`), projectId, userId: "owner" });
|
|
globalThis.fetch = (async (_url, init) => {
|
|
const headers = new Headers(init?.headers);
|
|
return Response.json({ user_id: headers.get("Authorization") === "Bearer TEST_OWNER_NEW" ? "owner" : "other", project_id: headers.get("X-Project-Id"), network: "supply", token_expires_at: new Date(Date.now() + 300000).toISOString() });
|
|
}) as typeof fetch;
|
|
const app = express(); app.use(express.json());
|
|
const router = express.Router();
|
|
router.use((req, res, next) => { void attachTrustedAgentContext(req).then(() => next()).catch(() => res.sendStatus(401)); });
|
|
const activeRuns = new Map<string, ActiveRun>([["credential-route-session", { clientSessionId: "credential-route-session", controller: new AbortController(), messages: [], pendingPermissions: new Map(), pendingQuestions: new Map(), subscribers: new Set(), status: "running" }]]);
|
|
registerChatInteractionRoutes(router, { activeRuns, runtime: {} as OpencodeRuntimeAdapter, sessionMetadataStore: metadata, sessionUiStateStore: new SessionUiStateStore(join(directory, "ui")) });
|
|
app.use(router);
|
|
const server = app.listen(0, "127.0.0.1");
|
|
await new Promise<void>((resolve) => server.once("listening", resolve));
|
|
const base = `http://127.0.0.1:${(server.address() as { port: number }).port}`;
|
|
bindSessionCredential("credential-route-session", { authorization: "Bearer TEST_OLD", projectId });
|
|
const unsubscribe = credentialRefreshCoordinator.subscribe("credential-route-session", () => {});
|
|
const pending = credentialRefreshCoordinator.request("credential-route-session", "access_token_rejected");
|
|
const requestId = credentialRefreshCoordinator.getPendingRequestId("credential-route-session")!;
|
|
const post = (authorization: string, id = requestId, project = projectId) => originalFetch(`${base}/session/credential-route-session/credentials`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: authorization, "X-Project-Id": project }, body: JSON.stringify({ request_id: id }) });
|
|
try {
|
|
expect((await post("Bearer TEST_OTHER")).status).toBe(404);
|
|
expect((await post("Bearer TEST_OWNER_NEW", requestId, "other-project")).status).toBe(404);
|
|
expect((await post("Bearer TEST_OWNER_NEW", "stale-request")).status).toBe(409);
|
|
expect((await post("Bearer TEST_OWNER_NEW")).status).toBe(202);
|
|
expect(await pending).toMatchObject({ authorization: "Bearer TEST_OWNER_NEW", projectId });
|
|
} finally {
|
|
unsubscribe(); releaseSessionCredential("credential-route-session");
|
|
await pending.catch(() => undefined);
|
|
globalThis.fetch = originalFetch;
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
await rm(directory, { force: true, recursive: true });
|
|
}
|
|
});
|
|
});
|