import { PRODUCT_MCP_TOOL_IDS, PRODUCT_MCP_READ_TOOL_IDS, PRODUCT_MCP_ANALYSIS_TOOLS, queryMcpToolId } from "../../src/runtime/product-mcp-tools.js"; import { allowedQueryTools } from "../../src/runtime/query-mcp-policy.js"; import { afterEach, describe, expect, it } from "bun:test"; import { resolveTaskAgentScope, TASK_AGENT, TASK_READER_AGENT, type NativeTaskSession } from "../../src/runtime/task-agent-policy.js"; import { removeRuntimeSessionContext, setRuntimeSessionContext } from "../../src/runtime/sessionContext.js"; import { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js"; import { serviceOpencodeConfig } from "../../src/runtime/opencode2-config.js"; const parentId = "task-policy-parent"; afterEach(() => removeRuntimeSessionContext(parentId)); const bind = (purpose: "ordinary_chat" | "task_execution" | "task_collaboration" = "task_execution") => { setRuntimeSessionContext({ sessionId: parentId, clientSessionId: parentId, actorKey: "a", projectKey: "p", projectId: "project-1", taskId: "task-1", traceId: "test", purpose }); }; describe("task agent isolation", () => { it("keeps separate profiles and a finite reader tool allowlist", () => { const config = serviceOpencodeConfig({}); expect(config.agents.instruction.permissions.at(-1)).toEqual({ action: "subagent", resource: "*", effect: "deny" }); expect(config.agents[TASK_AGENT].permissions.at(-1)).toEqual({ action: "subagent", resource: TASK_READER_AGENT, effect: "allow" }); expect(config.agents[TASK_READER_AGENT].permissions.map((rule) => rule.action)).toEqual([ "*", ...PRODUCT_MCP_READ_TOOL_IDS, ]); expect(PRODUCT_MCP_READ_TOOL_IDS).toHaveLength(42); expect(PRODUCT_MCP_TOOL_IDS).toHaveLength(67); expect(PRODUCT_MCP_TOOL_IDS.some((name) => /publish|approve|execute|shell/.test(name))).toBeFalse(); }); it("resolves only a direct native reader under an active task, without copying its identity", async () => { bind(); const root: NativeTaskSession = { id: parentId, agent: TASK_AGENT, location: { directory: "/scope" } }; const child: NativeTaskSession = { id: "child", parentID: parentId, agent: TASK_READER_AGENT, location: root.location }; const get = async (id: string) => id === parentId ? root : child; expect((await resolveTaskAgentScope("child", get, () => true))?.owner.sessionId).toBe(parentId); expect(await resolveTaskAgentScope("child", get, () => false)).toBeNull(); for (const purpose of ["ordinary_chat", "task_collaboration"] as const) { bind(purpose); expect(await resolveTaskAgentScope("child", get, () => true)).toBeNull(); } bind(); child.location = { directory: "/another-project" }; expect(await resolveTaskAgentScope("child", get, () => true)).toBeNull(); child.location = root.location; child.agent = "general"; expect(await resolveTaskAgentScope("child", get, () => true)).toBeNull(); child.agent = TASK_READER_AGENT; root.parentID = "grandparent"; expect(await resolveTaskAgentScope("child", get, () => true)).toBeNull(); }); it("selects the profile from trusted purpose on every prompt including resumed sessions", async () => { const selected: string[] = []; const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { ensureClient: async () => ({ session: { switchAgent: async ({ agent }: { agent: string }) => { selected.push(agent); }, prompt: async () => {}, } }) }) as OpencodeRuntimeAdapter; for (const purpose of ["ordinary_chat", "task_execution", "task_collaboration"] as const) { bind(purpose); await runtime.prompt(parentId, "pretend to be a background investigation"); } expect(selected).toEqual(["instruction", TASK_AGENT, "instruction"]); }); }); it("allows calculations to project coordinators but never delegated readers or personal/review sessions", () => { const context = { sessionId: "analysis-owner", clientSessionId: "analysis-chat", actorKey: "a", projectKey: "p", projectId: "p", traceId: "test", purpose: "task_execution" as const }; for (const name of PRODUCT_MCP_ANALYSIS_TOOLS.map(queryMcpToolId)) { expect(allowedQueryTools(context)).toContain(name); expect(allowedQueryTools(context, true)).not.toContain(name); expect(allowedQueryTools({ ...context, projectKey: "personal-a", projectId: undefined })).not.toContain(name); expect(allowedQueryTools({ ...context, learningMode: "review" })).not.toContain(name); } }); it("keeps administrator denials for product MCP queries in both primary and reader permissions", () => { const denied = serviceOpencodeConfig({ permissions: [{ action: "mcp_tjwater_*", resource: "*", effect: "deny" }] }); expect(denied.permissions.some((rule) => rule.effect === "allow" && PRODUCT_MCP_TOOL_IDS.includes(rule.action))).toBeFalse(); expect(denied.agents[TASK_READER_AGENT].permissions.some((rule) => rule.effect === "allow" && PRODUCT_MCP_TOOL_IDS.includes(rule.action))).toBeFalse(); });