Reuse OpenCode 2 HTTP/stdio clients with per-session read-only grants, native approval and sanitized owner-scoped MCP status. Narrow the environment inherited by trusted local MCP processes. Validated in an isolated checkout with bun run check and 20 focused tests.
86 lines
5.5 KiB
TypeScript
86 lines
5.5 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import express, { type Request } from "express";
|
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { config } from "../../src/config.js";
|
|
import { attachTrustedAgentContext, getLocalAgentContext, ProductContextError } from "../../src/context/localContext.js";
|
|
import { registerMcpPolicyBridge, registerMcpStatusRoutes } from "../../src/routes/mcpRoutes.js";
|
|
import { removeRuntimeSessionContext, setRuntimeSessionContext } from "../../src/runtime/sessionContext.js";
|
|
import { SessionMetadataStore } from "../../src/sessions/metadataStore.js";
|
|
|
|
describe("MCP routes", () => {
|
|
it("protects the internal policy bridge against browser and context injection", async () => {
|
|
const app = express(); app.use(express.json());
|
|
registerMcpPolicyBridge(app, { internalToken: "MCP_TEST_ONLY", runtime: { mcpReadonlyTools: () => ["mcp_fixture_read"] } });
|
|
const server = app.listen(0, "127.0.0.1");
|
|
await new Promise<void>((resolve) => server.once("listening", resolve));
|
|
const url = `http://127.0.0.1:${(server.address() as { port: number }).port}/internal/mcp/session-policy`;
|
|
const sessionID = "mcp-route-test";
|
|
setRuntimeSessionContext({ sessionId: sessionID, clientSessionId: sessionID, actorKey: "PRIVATE_ACTOR", projectKey: "PRIVATE_PROJECT", traceId: "test", purpose: "task_execution" });
|
|
const post = (body: unknown, headers: Record<string, string> = {}) => fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "x-agent-internal-token": "MCP_TEST_ONLY", ...headers }, body: JSON.stringify(body) });
|
|
try {
|
|
expect((await post({ session_id: sessionID }, { "x-agent-internal-token": "wrong" })).status).toBe(403);
|
|
expect((await post({ session_id: sessionID }, { Origin: "https://browser" })).status).toBe(403);
|
|
expect((await post({ session_id: sessionID }, { "Sec-Fetch-Site": "same-origin" })).status).toBe(403);
|
|
expect((await post({ session_id: sessionID, project_id: "other" })).status).toBe(400);
|
|
expect((await post({ session_id: "missing" })).status).toBe(404);
|
|
expect(await (await post({ session_id: sessionID })).json()).toEqual({ mode: "readonly", readonly_tools: ["mcp_fixture_read"] });
|
|
} finally {
|
|
removeRuntimeSessionContext(sessionID);
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
}
|
|
});
|
|
|
|
it("requires verified session ownership and returns constant errors without credentials", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const previousEnv = config.NODE_ENV;
|
|
const root = await mkdtemp(join(tmpdir(), "mcp-status-test-"));
|
|
const store = new SessionMetadataStore(root);
|
|
const app = express();
|
|
let calls = 0;
|
|
let failed = false;
|
|
globalThis.fetch = (async (input: Parameters<typeof fetch>[0], init: Parameters<typeof fetch>[1]) => {
|
|
if (String(input).endsWith("/api/v1/agent-auth-context")) {
|
|
const headers = new Headers(init?.headers);
|
|
return Response.json({ user_id: headers.get("Authorization") === "Bearer OWNER" ? "owner" : "other", project_id: headers.get("X-Project-Id"), network: "supply" });
|
|
}
|
|
return originalFetch(input, init);
|
|
}) as typeof fetch;
|
|
config.NODE_ENV = "production";
|
|
const owner = { header: (name: string) => ({ authorization: "Bearer OWNER", "x-project-id": "project" })[name.toLowerCase() as "authorization" | "x-project-id"] } as Request;
|
|
await attachTrustedAgentContext(owner);
|
|
await store.ensure({ ...getLocalAgentContext(owner), sessionId: "private-session" });
|
|
const router = express.Router();
|
|
router.use(async (req, res, next) => {
|
|
try { await attachTrustedAgentContext(req); next(); }
|
|
catch (error) { res.status(error instanceof ProductContextError ? error.status : 503).json({ message: "authentication failed" }); }
|
|
});
|
|
registerMcpStatusRoutes(router, { sessionMetadataStore: store, runtime: { mcpStatus: async () => {
|
|
calls++;
|
|
if (failed) throw new Error("SECRET_URL_AND_TOKEN");
|
|
return { configuration_errors: [], servers: [{ name: "mcp_fixture", type: "remote", status: "needs_auth", configured_protocol: "auto", tools: [] }] };
|
|
} } });
|
|
app.use("/api/v1/agent/chat", router);
|
|
const server = app.listen(0, "127.0.0.1");
|
|
await new Promise<void>((resolve) => server.once("listening", resolve));
|
|
const url = `http://127.0.0.1:${(server.address() as { port: number }).port}/api/v1/agent/chat/session/private-session/mcp`;
|
|
try {
|
|
expect((await originalFetch(url)).status).toBe(401);
|
|
const headers = { Authorization: "Bearer OWNER", "X-Project-Id": "project" };
|
|
expect((await originalFetch(url, { headers: { ...headers, Authorization: "Bearer OTHER" } })).status).toBe(404);
|
|
expect((await originalFetch(url, { headers: { ...headers, "X-Project-Id": "other" } })).status).toBe(404);
|
|
expect(calls).toBe(0);
|
|
expect(await (await originalFetch(url, { headers })).json()).toEqual({ servers: [{ name: "mcp_fixture", type: "remote", status: "needs_auth", configured_protocol: "auto", tools: [] }] });
|
|
failed = true;
|
|
const error = await originalFetch(url, { headers });
|
|
expect(error.status).toBe(503);
|
|
expect(await error.json()).toEqual({ message: "MCP runtime unavailable" });
|
|
} finally {
|
|
globalThis.fetch = originalFetch; config.NODE_ENV = previousEnv;
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
await rm(root, { recursive: true });
|
|
}
|
|
});
|
|
});
|