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

75 lines
5.8 KiB
TypeScript

// Native configuration and permission checks only, no model or production calls.
import assert from "node:assert/strict";
import { randomBytes } from "node:crypto";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import express from "express";
import { serviceOpencodeConfig } from "../../src/runtime/opencode2-config.js";
import { startOpencode2 } from "../../src/runtime/opencode2-server.js";
import { bindScopedConversationWorkspace, createConversationWorkspace } from "../../src/runtime/conversationWorkspace.js";
import { registerTaskAgentBridge } from "../../src/routes/task-agent-bridge.js";
import { registerMcpPolicyBridge } from "../../src/routes/mcpRoutes.js";
import { QUERY_MCP_TOOL_IDS } from "../../src/runtime/product-mcp-tools.js";
import { removeRuntimeSessionContext, setRuntimeSessionContext } from "../../src/runtime/sessionContext.js";
import { TASK_AGENT, TASK_READER_AGENT } from "../../src/runtime/task-agent-policy.js";
const root = await mkdtemp(join(tmpdir(), "tjwater-task-agent-smoke-"));
const workspaceRoot = join(root, "workspaces");
const token = randomBytes(32).toString("hex");
let native: Awaited<ReturnType<typeof startOpencode2>> | undefined;
const ids: string[] = [];
const active = new Set<string>();
const app = express(); app.use(express.json());
const readContext = registerTaskAgentBridge(app, { internalToken: token,
getSession: (sessionID) => native!.client.session.get({ sessionID }), hasActiveRun: (id) => active.has(id) });
registerMcpPolicyBridge(app, { internalToken: token, runtime: { mcpReadonlyTools: () => QUERY_MCP_TOOL_IDS }, resolveReader: readContext });
const server = app.listen(0, "127.0.0.1");
await new Promise<void>((done) => server.once("listening", done));
try {
const input = JSON.parse(await readFile("opencode.json", "utf8"));
input.plugins = [...(input.plugins ?? []), resolve(".opencode/test-fixtures/task-agent-probe-plugin")];
native = await startOpencode2({ command: process.env.OPENCODE_COMMAND ?? "opencode2", hostname: "127.0.0.1", port: 0, timeout: 30_000,
config: serviceOpencodeConfig(input, undefined, workspaceRoot),
env: { AGENT_INTERNAL_TOKEN: token, TJWATER_AGENT_WORKSPACE_ROOT: workspaceRoot,
TJWATER_AGENT_MCP_CONNECTION_PATH: join(root, "absent-query-mcp.json"),
TJWATER_AGENT_INTERNAL_BASE_URL: `http://127.0.0.1:${(server.address() as { port: number }).port}`,
WATERNETWORK_API_KEY: "TEST_ONLY", OPENCODE_DB: join(root, "native-data", "opencode.db"),
XDG_DATA_HOME: join(root, "native-data"), XDG_CONFIG_HOME: join(root, "native-config"),
XDG_CACHE_HOME: join(root, "native-cache"), XDG_STATE_HOME: join(root, "native-state") },
});
const initial = await createConversationWorkspace(workspaceRoot);
const parent = await native.client.session.create({ agent: TASK_AGENT, location: { directory: initial } }); ids.push(parent.id);
const directory = await bindScopedConversationWorkspace(initial, workspaceRoot, { actorKey: "actor-smoke", projectKey: "project-smoke", sessionId: parent.id });
await native.client.session.move({ sessionID: parent.id, directory });
const context = { sessionId: parent.id, clientSessionId: parent.id, actorKey: "actor-smoke", projectKey: "project-smoke", projectId: "test-project", taskId: "test-task", traceId: "test", purpose: "task_execution" as const };
setRuntimeSessionContext(context); active.add(parent.id);
await native.client.rpc.call({ rpcID: "tjwater.tools", method: "list", input: {}, location: { directory } });
const fields = (await native.client.rpc.call({ rpcID: "test.task-agent-probe", method: "subagent_input", input: {}, location: { directory } })).output as string[];
assert.ok(fields.includes("agent") && fields.includes("background") && fields.includes("sessionID"), `unexpected native subagent input: ${fields}`);
const decision = async (sessionID: string, action: string, resource = "*") => {
const session = await native!.client.session.get({ sessionID });
return (await native!.client.permission.create({ sessionID, agent: session.agent, action, resources: [resource], save: [] })).effect;
};
assert.equal(await decision(parent.id, "subagent", TASK_READER_AGENT), "allow");
assert.equal(await decision(parent.id, "subagent", "general"), "deny");
const child = await native.client.session.create({ parentID: parent.id, agent: TASK_READER_AGENT, location: { directory } }); ids.push(child.id);
assert.equal((await readContext(child.id))?.runtimeSessionId, parent.id);
assert.equal(await decision(child.id, "mcp_tjwater_query_scada_readings"), "allow");
for (const action of ["subagent", "publish_task_report", "propose_task_action", "shell", "execute", "edit"]) assert.equal(await decision(child.id, action), "deny");
await native.client.session.switchAgent({ sessionID: parent.id, agent: "instruction" });
assert.equal(await decision(parent.id, "subagent", TASK_READER_AGENT), "deny");
await native.client.session.switchAgent({ sessionID: parent.id, agent: TASK_AGENT });
setRuntimeSessionContext({ ...context, purpose: "task_collaboration" });
assert.equal(await decision(parent.id, "subagent", TASK_READER_AGENT), "deny");
setRuntimeSessionContext(context); active.clear();
assert.equal(await readContext(child.id), null);
assert.equal(await decision(parent.id, "subagent", TASK_READER_AGENT), "deny");
console.log("Native task/main profiles, subagent input, read-only permissions, parent binding and revocation passed. No model inference.");
} finally {
for (const id of ids.reverse()) { removeRuntimeSessionContext(id); if (native) await native.client.session.remove({ sessionID: id }).catch(() => {}); }
native?.close(); if (native) await native.exited;
await new Promise<void>((done) => server.close(() => done()));
await rm(root, { recursive: true, force: true });
}