Files
jiang 6a49d2209e feat(agent): unify MCP tools and adopt Responses delivery
Move project and host queries into native MCP with host-owned OAuth/context,
while retaining memory, skills, actions and publication plugins. Add nearby-model
queries and explicit chat/task/learning model defaults.

Adopt Responses HTTP/SSE and native steer/queue inbox delivery, preserving scoped
session ownership, pending input edits and investigation communication.

Disable local-provider discovery: Server /health allowed native vLLM to probe
/v1/models on port 8000. A native before/after regression prevents recurrence.

Validation: 170 focused Bun tests; service/plugin typechecks; native discovery,
provider-discovery and input-inbox smoke checks; no paid model inference.
2026-10-19 18:46:58 +08:00

131 lines
9.2 KiB
TypeScript

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<typeof Bun.serve> | 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<string, any>();
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<void> = 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]);
}
});
});