import { beforeEach, afterEach, describe, expect, it } from "bun:test"; import plugin from "../../.opencode/plugins/tjwater-tools.js"; import { AGENT_MESSAGE_TOOLS, registerAgentMessagePolicy } from "../../.opencode/agent-message-policy.js"; import { serviceOpencodeConfig } from "../../src/runtime/opencode2-config.js"; import { TASK_AGENT, TASK_READER_AGENT } from "../../src/runtime/task-agent-policy.js"; const originalConnectionPath = process.env.TJWATER_AGENT_MCP_CONNECTION_PATH; beforeEach(() => { process.env.TJWATER_AGENT_MCP_CONNECTION_PATH = `/tmp/tjwater-mcp-absent-${process.pid}.json`; }); afterEach(() => { if (originalConnectionPath === undefined) delete process.env.TJWATER_AGENT_MCP_CONNECTION_PATH; else process.env.TJWATER_AGENT_MCP_CONNECTION_PATH = originalConnectionPath; }); const taskId = "60000000-0000-4000-8000-000000000001"; const runId = "60000000-0000-4000-8000-000000000002"; const messageId = "60000000-0000-4000-8000-000000000003"; const requestId = "60000000-0000-4000-8000-000000000004"; let bridgeServer: ReturnType | undefined; afterEach(() => { bridgeServer?.stop(true); bridgeServer = undefined; }); async function runTools(script: string) { const child = Bun.spawn([process.execPath, "-e", script], { cwd: process.cwd(), env: { ...process.env, AGENT_INTERNAL_TOKEN: "MESSAGE_TEST_ONLY", TJWATER_AGENT_INTERNAL_TOKEN: "", TJWATER_AGENT_INTERNAL_BASE_URL: `http://127.0.0.1:${bridgeServer!.port}` }, stdout: "pipe", stderr: "pipe", }); const [exitCode, stdout, stderr] = await Promise.all([child.exited, new Response(child.stdout).text(), new Response(child.stderr).text()]); expect({ exitCode, stderr }).toEqual({ exitCode: 0, stderr: "" }); return JSON.parse(stdout); } describe("controlled investigation message tools", () => { it("registers exact direct tools and rejects identity or arbitrary session targets at dispatch", async () => { const tools = new Map(); await plugin.setup({ tool: { transform: async (fn: any) => fn({ list: () => [...tools].map(([id, tool]) => ({ ...tool, id })), add: (tool: any) => tools.set(tool.name, tool), remove: (name: string) => tools.delete(name), update: () => {} }), list: async () => [...tools].map(([id, tool]) => ({ ...tool, id })) }, mcp: { transform: async (fn: any) => fn({ list: () => [] }), list: async () => ({ data: [] }) }, shell: { hook: async () => {} }, permission: { hook: async () => {} }, session: { hook: async () => {} }, rpc: { register: async () => {} }, storage: { set: async () => {}, get: async () => undefined }, } as never); for (const name of ["send_task_message", "reply_agent_message"]) expect(tools.get(name).options).toEqual({ codemode: false, permission: name }); for (const name of AGENT_MESSAGE_TOOLS.filter((name) => name.startsWith("mcp_"))) expect(tools.has(name)).toBeFalse(); expect(tools.has("memory_manager")).toBeTrue(); expect(tools.has("skill_manager")).toBeTrue(); const send = tools.get("send_task_message"); for (const extra of [{ session_id: "other" }, { project_id: "other" }, { authorization: "secret" }, { operation: "reply" }]) { await expect(send.execute({ task_id: taskId, run_id: runId, message: "question", request_id: requestId, ...extra }, { sessionID: "native-owner", id: "native-call" })).rejects.toThrow(); } expect(tools.has("message")).toBeFalse(); }); it("passes each fixed operation through the authenticated bridge without rewriting delivery or provenance", async () => { const calls: Array<{ operation: string; token: string | null; method: string; body: unknown }> = []; bridgeServer = Bun.serve({ hostname: "127.0.0.1", port: 0, async fetch(request) { const operation = new URL(request.url).pathname; calls.push({ operation, token: request.headers.get("x-agent-internal-token"), method: request.method, body: await request.json() }); return Response.json({ status: "queued", message_id: messageId, originalMessage: { role: "agent", message: "source retained" } }); } }); const inputs = [{ task_id: taskId, run_id: runId, message: "check current evidence", request_id: requestId }, { message_id: messageId, message: "evidence reviewed", request_id: requestId }]; const output = await runTools([ `const names = ${JSON.stringify(["send_task_message", "reply_agent_message"])}; const inputs = ${JSON.stringify(inputs)};`, 'const results = []; for (let i = 0; i < names.length; i++) { const { default: tool } = await import(`./.opencode/tools/${names[i]}.ts`); results.push(JSON.parse(await tool.execute(inputs[i], { sessionID: "native-owner" }))); }', "process.stdout.write(JSON.stringify(results));", ].join("\n")); expect(calls).toEqual(["send", "reply"].map((operation, index) => ({ operation: `/internal/agent-messages/${operation}`, token: "MESSAGE_TEST_ONLY", method: "POST", body: { session_id: "native-owner", input: inputs[index] }, }))); expect(output).toEqual(inputs.map(() => ({ status: "queued", message_id: messageId, originalMessage: { role: "agent", message: "source retained" } }))); }); it("validates UUIDs, bounds, empty messages and forbidden fields before network access and honors cancellation", async () => { let received = 0; bridgeServer = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch() { received++; return Response.json({}); } }); const cases = [ ["send_task_message", { task_id: taskId, run_id: runId, message: "", request_id: requestId }], ["send_task_message", { task_id: taskId, run_id: runId, message: "x".repeat(8001), request_id: requestId }], ["send_task_message", { task_id: taskId, run_id: "invalid", message: "q", request_id: requestId }], ["reply_agent_message", { message_id: "invalid", message: "a", request_id: requestId }], ["reply_agent_message", { message_id: messageId, message: "a", request_id: "invalid" }], ["reply_agent_message", { message_id: messageId, message: "a", request_id: requestId, session_id: "foreign" }], ]; const output = await runTools([ `const cases = ${JSON.stringify(cases)}; let rejected = 0;`, 'for (const [name, input] of cases) { const { default: tool } = await import(`./.opencode/tools/${name}.ts`); try { await tool.execute(input, { sessionID: "owner" }); } catch { rejected++; } }', 'const { default: read } = await import("./.opencode/tools/reply_agent_message.ts"); const controller = new AbortController(); controller.abort();', `let cancelled = false; try { await read.execute(${JSON.stringify({ message_id: messageId, message: "a", request_id: requestId })}, { sessionID: "owner", signal: controller.signal }); } catch { cancelled = true; }`, "process.stdout.write(JSON.stringify({ rejected, cancelled }));", ].join("\n")); expect(output).toEqual({ rejected: cases.length, cancelled: true }); expect(received).toBe(0); }); it("fails a denied bridge response rather than claiming a message was queued", async () => { bridgeServer = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch() { return Response.json({ error: "TASK_RUN_NOT_ACTIVE" }, { status: 409 }); } }); const output = await runTools([ 'import send from "./.opencode/tools/send_task_message.ts";', `let error = ""; try { await send.execute(${JSON.stringify({ task_id: taskId, run_id: runId, message: "q", request_id: requestId })}, { sessionID: "owner" }); } catch (caught) { error = caught.message; }`, "process.stdout.write(JSON.stringify({ error }));", ].join("\n")); expect(output.error).toContain("agent message send failed (409)"); expect(output.error).toContain("TASK_RUN_NOT_ACTIVE"); }); it("limits static profiles and catalogs while preserving ordinary-chat subagent denial and administrator rules", async () => { const administratorDeny = { action: "send_task_message", resource: "*", effect: "deny" }; const config = serviceOpencodeConfig({ agents: { instruction: { permissions: [administratorDeny] } } }); for (const action of AGENT_MESSAGE_TOOLS) { expect(config.agents.instruction.permissions.find((rule) => rule.action === action)?.effect).toBe("allow"); expect(config.agents[TASK_AGENT].permissions.find((rule) => rule.action === action)?.effect) .toBe(["mcp_tjwater_read_agent_messages", "reply_agent_message"].includes(action) ? "allow" : "deny"); } expect(config.agents.instruction.permissions.at(-2)).toEqual(administratorDeny); expect(config.agents.instruction.permissions.at(-1)).toEqual({ action: "subagent", resource: "*", effect: "deny" }); expect(config.agents[TASK_READER_AGENT].permissions.some((rule) => (AGENT_MESSAGE_TOOLS as readonly string[]).includes(rule.action))).toBeFalse(); let hook: (event: any) => Promise = async () => {}; await registerAgentMessagePolicy({ session: { hook: async (_name: string, fn: typeof hook) => { hook = fn; } } } as never); for (const [agent, expected] of [["instruction", AGENT_MESSAGE_TOOLS], [TASK_AGENT, AGENT_MESSAGE_TOOLS.slice(3)], [TASK_READER_AGENT, []]] as const) { const event = { agent, sessionID: "owner", tools: Object.fromEntries(AGENT_MESSAGE_TOOLS.map((name) => [name, {}])) }; await hook(event); expect(Object.keys(event.tools)).toEqual([...expected]); } }); });