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.
This commit is contained in:
2026-10-19 18:46:58 +08:00
parent f5c3cde2c2
commit 6a49d2209e
164 changed files with 12286 additions and 2368 deletions
+119
View File
@@ -0,0 +1,119 @@
// Opt-in native inbox smoke. A deterministic localhost SSE fixture is the only
// model endpoint; no paid inference, product writes or running service restarts.
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 } from "node:path";
import express from "express";
import { parseAgentModelConfig } from "../../src/chat/modelConfig.js";
import { serviceOpencodeConfig } from "../../src/runtime/opencode2-config.js";
import { startOpencode2 } from "../../src/runtime/opencode2-server.js";
import { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";
import { bindScopedConversationWorkspace, createConversationWorkspace } from "../../src/runtime/conversationWorkspace.js";
import { registerTaskAgentBridge } from "../../src/routes/task-agent-bridge.js";
import { removeRuntimeSessionContext, setRuntimeSessionContext } from "../../src/runtime/sessionContext.js";
import { startQueryMcpServer } from "../../src/mcp/query-server.js";
import { writeQueryMcpConnection } from "../../src/runtime/query-mcp-connection.js";
import { registerMcpPolicyBridge } from "../../src/routes/mcpRoutes.js";
import { QUERY_MCP_TOOL_IDS } from "../../src/runtime/product-mcp-tools.js";
import { TASK_AGENT, TASK_READER_AGENT } from "../../src/runtime/task-agent-policy.js";
const root = await mkdtemp(join(tmpdir(), "tjwater-agent-messages-smoke-"));
const previousConnectionPath = process.env.TJWATER_AGENT_MCP_CONNECTION_PATH;
const workspaceRoot = join(root, "workspaces");
const token = randomBytes(32).toString("hex");
let native: Awaited<ReturnType<typeof startOpencode2>> | undefined;
const ids: string[] = [];
const modelInputs: Array<{ messages?: Array<{ role: string; content: unknown }> }> = [];
const fixture = Bun.serve({ hostname: "127.0.0.1", port: 0, async fetch(request) {
if (new URL(request.url).pathname !== "/v1/chat/completions") return new Response("local fixture route not found", { status: 404 });
modelInputs.push(await request.json());
const chunk = (delta: object, finishReason: string | null) => JSON.stringify({ id: "chatcmpl-local-fixture", object: "chat.completion.chunk",
created: 1, model: "fake", choices: [{ index: 0, delta, finish_reason: finishReason }] });
return new Response(`data: ${chunk({ role: "assistant", content: "Local inbox fixture completed." }, null)}\n\ndata: ${chunk({}, "stop")}\n\ndata: [DONE]\n\n`,
{ headers: { "Content-Type": "text/event-stream" } });
} });
const app = express(); app.use(express.json());
registerTaskAgentBridge(app, { internalToken: token, getSession: (sessionID) => native!.client.session.get({ sessionID }), hasActiveRun: () => false });
registerMcpPolicyBridge(app, { internalToken: token, runtime: { mcpReadonlyTools: () => QUERY_MCP_TOOL_IDS } });
const queryMcp = startQueryMcpServer({ internalToken: token, productClient: null, productTools: [], resolveReader: async () => null, executeLocal: async () => ({ items: [] }) });
writeQueryMcpConnection(join(root, "sessions"), queryMcp.url);
const server = app.listen(0, "127.0.0.1");
await new Promise<void>((done) => server.once("listening", done));
try {
const models = parseAgentModelConfig({ defaults: { chat: "local-smoke/fake", task: "local-smoke/fake", learning_gate: "local-smoke/fake" }, providers: { "local-smoke": {
name: "Deterministic local smoke fixture", api_url: `http://127.0.0.1:${fixture.port}/v1`, api_key: "SMOKE_ONLY",
models: { fake: { capabilities: { tools: true }, limit: { context: 32768, output: 1024 } } },
} } });
const input = JSON.parse(await readFile("opencode.json", "utf8"));
native = await startOpencode2({ command: process.env.OPENCODE_COMMAND ?? "opencode2", hostname: "127.0.0.1", port: 0, timeout: 30_000,
config: serviceOpencodeConfig(input, models, workspaceRoot),
env: { AGENT_INTERNAL_TOKEN: token, TJWATER_AGENT_WORKSPACE_ROOT: workspaceRoot,
TJWATER_AGENT_MCP_CONNECTION_PATH: process.env.TJWATER_AGENT_MCP_CONNECTION_PATH!,
TJWATER_AGENT_INTERNAL_BASE_URL: `http://127.0.0.1:${(server.address() as { port: number }).port}`,
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 session = await native.client.session.create({ agent: "instruction", location: { directory: initial } }); ids.push(session.id);
const directory = await bindScopedConversationWorkspace(initial, workspaceRoot, { actorKey: "actor-smoke", projectKey: "project-smoke", sessionId: session.id });
await native.client.session.move({ sessionID: session.id, directory });
setRuntimeSessionContext({ sessionId: session.id, clientSessionId: session.id, actorKey: "actor-smoke", projectKey: "project-smoke",
projectId: "test-project", traceId: "test", purpose: "ordinary_chat" });
const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { ensureClient: async () => native!.client }) as OpencodeRuntimeAdapter;
const required = ["mcp_tjwater_list_investigation_tasks", "mcp_tjwater_get_investigation_task", "send_task_message", "mcp_tjwater_read_agent_messages", "reply_agent_message"];
let discovered: Array<{ id: string; direct: boolean }> = [];
const catalogDeadline = Date.now() + 10_000;
do {
discovered = (await native.client.rpc.call({ rpcID: "tjwater.tools", method: "list", input: {}, location: { directory } })).output as typeof discovered;
if (required.every((id) => discovered.some((tool) => tool.id === id && tool.direct))) break;
await new Promise((resolve) => setTimeout(resolve, 100));
} while (Date.now() < catalogDeadline);
for (const id of required) {
assert.ok(discovered.some((tool) => tool.id === id && tool.direct), `missing direct registered tool ${id}`);
assert.equal((await native.client.permission.create({ sessionID: session.id, agent: "instruction", action: id, resources: ["*"], save: [] })).effect, "allow");
assert.equal((await native.client.permission.create({ sessionID: session.id, agent: TASK_AGENT, action: id, resources: ["*"], save: [] })).effect,
["mcp_tjwater_read_agent_messages", "reply_agent_message"].includes(id) ? "allow" : "deny");
assert.equal((await native.client.permission.create({ sessionID: session.id, agent: TASK_READER_AGENT, action: id, resources: ["*"], save: [] })).effect, "deny");
}
assert.ok(!discovered.some((tool) => ["message", "send_message", "session_message"].includes(tool.id)), "arbitrary-session message tool exposed");
const nativeId = `msg_${randomBytes(16).toString("hex")}`;
assert.equal(await runtime.agentMessageState(session.id, nativeId), "missing");
const originalMessage = { message_id: "62000000-0000-4000-8000-000000000001", source_session_id: "investigation-source", message: "Local cooperation provenance marker" };
await runtime.enqueueAgentMessage(session.id, nativeId, JSON.stringify({ originalMessage }), { source: "agent_message", message_id: originalMessage.message_id });
assert.equal(await runtime.agentMessageState(session.id, nativeId), "enqueued");
assert.equal((await native.client.session.inbox.list({ sessionID: session.id })).filter((item) => item.id === nativeId).length, 1);
assert.ok(!(session.id in await native.client.session.active()), "resume:false unexpectedly started the idle main session");
assert.equal(modelInputs.length, 0, "idle synthetic enqueue invoked a model");
// Admission recovery checks state before retrying. The inbox must retain one
// stable entry, while native duplicate admission is rejected or idempotent.
try { await runtime.enqueueAgentMessage(session.id, nativeId, JSON.stringify({ originalMessage }), { source: "agent_message" }); }
catch (error) { assert.ok(error instanceof Error || typeof error === "object", "invalid duplicate rejection"); }
assert.equal((await native.client.session.inbox.list({ sessionID: session.id })).filter((item) => item.id === nativeId).length, 1);
assert.equal(modelInputs.length, 0);
await native.client.session.prompt({ sessionID: session.id, text: "Consume the local inbox fixture and finish." });
await native.client.session.wait({ sessionID: session.id }, { signal: AbortSignal.timeout(30_000) });
assert.equal(await runtime.agentMessageState(session.id, nativeId), "delivered");
const stored = await native.client.session.message.get({ sessionID: session.id, messageID: nativeId });
assert.equal(stored.type, "synthetic");
assert.ok(JSON.stringify(stored).includes("originalMessage"), "native synthetic lost originalMessage provenance");
assert.equal((await native.client.session.inbox.list({ sessionID: session.id })).filter((item) => item.id === nativeId).length, 0);
assert.ok(modelInputs.length > 0, "local fake provider was not invoked to consume inbox");
const fixtureText = JSON.stringify(modelInputs);
assert.ok(fixtureText.includes("Local cooperation provenance marker"), "recipient did not receive synthetic discussion");
assert.ok(fixtureText.includes("originalMessage"), "model context lost sender provenance");
const receivedDiscussion = modelInputs.flatMap((input) => input.messages ?? []).filter((message) =>
message.role !== "system" && JSON.stringify(message.content).includes("Local cooperation provenance marker"));
assert.ok(receivedDiscussion.length > 0);
for (const message of receivedDiscussion) assert.ok(!JSON.stringify(message.content).includes("<userinstruction>"), "synthetic discussion upgraded to userinstruction");
assert.ok(!(session.id in await native.client.session.active()), "local fixture did not return to idle");
console.log("Native controlled tool discovery, primary permissions, missing/enqueued/delivered state, stable-ID dedup and resume:false idle behavior passed. Only a local fake SSE provider was used.");
} 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())); fixture.stop(true);
await queryMcp.close();
if (previousConnectionPath === undefined) delete process.env.TJWATER_AGENT_MCP_CONNECTION_PATH; else process.env.TJWATER_AGENT_MCP_CONNECTION_PATH = previousConnectionPath;
await rm(root, { recursive: true, force: true });
}
+71
View File
@@ -0,0 +1,71 @@
// Opt-in: MCP_TEST_PYTHON=<server env python> MCP_TEST_SERVER_ROOT=<server repo> bun tests/integration/mcp-query-smoke.ts
import { resolve } from "node:path";
import { McpQueryClient, type McpHttpFetch } from "../../src/product/mcp-query-client.js";
import { bindSessionCredential, releaseSessionCredential } from "../../src/product/credential-refresh.js";
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
import { startQueryMcpServer } from "../../src/mcp/query-server.js";
import { setRuntimeSessionContext, removeRuntimeSessionContext } from "../../src/runtime/sessionContext.js";
const python = process.env.MCP_TEST_PYTHON;
const root = process.env.MCP_TEST_SERVER_ROOT;
if (!python || !root) throw new Error("MCP smoke requires an explicit isolated Server environment");
// The fixture owns its OAuth/resource settings. Local Agent MCP configuration
// (notably a private development issuer) must not override its loopback setup.
const fixtureEnv = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("TJWATER_MCP_")));
const processHandle = Bun.spawn([python, resolve(root, "tests/fixtures/mcp_query_app.py")], { cwd: root, env: { ...fixtureEnv, PYTHONPATH: root }, stdout: "pipe", stderr: "pipe" });
const session = "fixture-host-session";
let gateway: ReturnType<typeof startQueryMcpServer> | undefined;
let nativeClient: Client | undefined;
try {
const reader = processHandle.stdout.getReader();
const output = await Promise.race([reader.read(), Bun.sleep(10_000).then(() => { throw new Error("MCP fixture did not start"); })]);
reader.releaseLock();
const resource = new TextDecoder().decode(output.value).trim().split("\n").at(-1)!;
if (!/^http:\/\/127\.0\.0\.1:\d+\/mcp$/.test(resource)) throw new Error("MCP fixture endpoint invalid");
const requests: string[] = [];
const grants: string[] = [];
const issuer = "http://localhost/realms/test";
const tokenEndpoint = "http://localhost/fixture/oauth/token";
const fixtureJwt = (kind: string) => `e30.${Buffer.from(JSON.stringify({ sub: "be3b27c8-0e58-4e3a-9df5-e62bc43f4e51", iss: "http://localhost/realms/test", aud: "tjwater-mcp", azp: "tjwater-agent-mcp", fixture: kind })).toString("base64url")}.fixture`;
const http: McpHttpFetch = async (input, init) => {
const url = String(input);
if (url === "http://localhost/.well-known/oauth-authorization-server/realms/test") return Response.json({
issuer, authorization_endpoint: "http://localhost/fixture/oauth/authorize", token_endpoint: tokenEndpoint,
response_types_supported: ["code"], token_endpoint_auth_methods_supported: ["client_secret_post"],
});
if (url === tokenEndpoint) {
const body = new URLSearchParams(String(init?.body));
if (body.get("resource") !== resource) throw new Error("OAuth grant omitted its resource indicator");
const grant = body.get("grant_type"); grants.push(grant ?? "");
return Response.json({ access_token: fixtureJwt(grant === "client_credentials" ? "catalog" : "user"), token_type: "Bearer", expires_in: 300 });
}
if (url === resource) requests.push(new Headers(init?.headers).get("MCP-Method") ?? init?.method ?? "GET");
return fetch(input, init);
};
const client = new McpQueryClient({ issuer: "http://localhost/realms/test", resource, audience: "tjwater-mcp", clientId: "tjwater-agent-mcp", clientSecret: "fixture" }, http);
const catalog = await client.prepare();
if (catalog.length !== 35) throw new Error("Query catalog discovery failed");
if (!catalog.some((tool) => tool.name === "run_simulation" && tool.inputSchema.properties?.start_time && tool.inputSchema.properties?.output_node_ids)) throw new Error("Standard hydraulic simulation schema missing");
bindSessionCredential(session, { authorization: `Bearer ${fixtureJwt("source")}`, projectId: "d44a9241-de48-4e60-9348-867dc466a031" });
setRuntimeSessionContext({ sessionId: session, clientSessionId: session, actorKey: "fixture", traceId: "fixture", projectId: "d44a9241-de48-4e60-9348-867dc466a031", projectKey: "fixture-project", purpose: "ordinary_chat" });
gateway = startQueryMcpServer({ internalToken: "MCP_TEST_ONLY", productClient: client, productTools: catalog, resolveReader: async () => null, executeLocal: async () => { throw new Error("unused fixture query"); } });
nativeClient = new Client({ name: "fixture-native", version: "1" }, { versionNegotiation: { mode: { pin: "2026-07-28" } } });
await nativeClient.connect(new StreamableHTTPClientTransport(new URL(gateway.url), { requestInit: { headers: { "x-agent-internal-token": "MCP_TEST_ONLY" } } }));
if ((await nativeClient.listTools()).tools.length !== 41) throw new Error("Merged MCP query catalog discovery failed");
const result = await nativeClient.callTool({ name: "list_elements", arguments: { element_type: "pipe", limit: 1 }, _meta: { "ai.opencode/sessionID": session } });
if (result.isError || JSON.stringify((result.structuredContent as { data?: unknown } | undefined)?.data) !== '[{"id":"P-1"}]') throw new Error("Typed query execution failed");
const nearbyTool = catalog.find((tool) => tool.name === "query_nearby_elements");
if (!nearbyTool?.inputSchema.properties?.center) throw new Error("Nearby model tool schema missing");
const nearby = await nativeClient.callTool({ name: "query_nearby_elements", arguments: { center: [121.5, 30.9], radius_m: 500, limit: 5 }, _meta: { "ai.opencode/sessionID": session } });
if (nearby.isError) throw new Error("Nearby model query failed through merged MCP");
if (!requests.includes("server/discover") || !requests.includes("tools/call") || requests.includes("initialize") || requests.includes("GET")) throw new Error("Transport used unexpected protocol requests");
if (grants.join(",") !== "client_credentials,urn:ietf:params:oauth:grant-type:token-exchange") throw new Error("OAuth grants were not validated");
console.log("PASS: local MCP 41-tool discovery, real Server schemas, OAuth discovery, resource-bound grants, host binding, query and revocation over MCP HTTP");
} finally {
await nativeClient?.close();
await gateway?.close();
removeRuntimeSessionContext(session);
releaseSessionCredential(session);
processHandle.kill("SIGTERM");
await processHandle.exited;
}
+3 -2
View File
@@ -14,7 +14,7 @@ const root = await mkdtemp(join(tmpdir(), "tjwater-mcp-smoke-"));
const pluginDirectory = join(root, "fixture-plugin");
await mkdir(pluginDirectory);
await writeFile(join(pluginDirectory, "package.json"), JSON.stringify({ name: "tjwater-mcp-fixture", type: "module", exports: "./index.ts" }));
await writeFile(join(pluginDirectory, "index.ts"), `export { default } from ${JSON.stringify(resolve("tests/fixtures/mcp-call-plugin.ts"))};\n`);
await writeFile(join(pluginDirectory, "index.ts"), `export { default } from ${JSON.stringify(resolve(".opencode/test-fixtures/mcp-call-plugin.ts"))};\n`);
const app = express(); app.use(express.json());
registerMcpPolicyBridge(app, { internalToken: "MCP_TEST_ONLY", runtime: { mcpReadonlyTools: () => ["mcp_stdio_echo"] } });
const wire: { method: string; version?: string; header?: string }[] = [];
@@ -43,7 +43,8 @@ try {
}, permissions: [...source.permissions, { action: "mcp_stdio_echo", resource: "*", effect: "allow" }] });
native = await startOpencode2({ command: process.env.OPENCODE_COMMAND ?? "opencode2", hostname: "127.0.0.1", port: 0, timeout: 30_000, config: nativeConfig, env: {
AGENT_INTERNAL_TOKEN: "MCP_TEST_ONLY", TJWATER_AGENT_INTERNAL_BASE_URL: baseUrl, TJWATER_AGENT_WORKSPACE_ROOT: join(root, "workspaces"),
DEEPSEEK_API_KEY: "TEST_ONLY_NEVER_USED", XDG_DATA_HOME: join(root, "data"), XDG_CONFIG_HOME: join(root, "config"), XDG_CACHE_HOME: join(root, "cache"), XDG_STATE_HOME: join(root, "state"),
TJWATER_AGENT_MCP_CONNECTION_PATH: join(root, "absent-query-mcp.json"),
DEEPSEEK_API_KEY: "TEST_ONLY_NEVER_USED", OPENCODE_DB: join(root, "data", "opencode.db"), XDG_DATA_HOME: join(root, "data"), XDG_CONFIG_HOME: join(root, "config"), XDG_CACHE_HOME: join(root, "cache"), XDG_STATE_HOME: join(root, "state"),
} });
const session = await native.client.session.create({ location: { directory: process.cwd() }, agent: "instruction" });
sessionId = session.id;
@@ -0,0 +1,15 @@
import { createPromptSuggestionService } from "../../src/chat/prompt-suggestions.js";
import { config } from "../../src/config.js";
import { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";
// Opt-in feature smoke: one real inference, no persisted chat or business tools.
const runtime = new OpencodeRuntimeAdapter();
try {
const generate = createPromptSuggestionService(runtime);
const result = await generate({ actorKey: "suggestion-smoke", projectKey: "suggestion-smoke", projectId: "suggestion-smoke", userId: "suggestion-smoke", network: "supply", traceId: "suggestion-smoke" }, {
activities: [{ id: "demo:pressure", kind: "task", title: "演示压力监测数据核查", occurred_at: new Date().toISOString(), summary: "仅为功能验证的演示活动,不代表真实告警。", origin: "demo", status: "pending" }],
}, config.OPENCODE_MODEL);
console.log(JSON.stringify({ model: config.OPENCODE_MODEL, count: result.suggestions.length, questions: result.suggestions.map((item) => item.question) }));
} finally {
await runtime.dispose();
}
@@ -0,0 +1,72 @@
import assert from "node:assert/strict";
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { parseAgentModelConfig } from "../../src/chat/modelConfig.js";
import { serviceOpencodeConfig } from "../../src/runtime/opencode2-config.js";
import { startOpencode2 } from "../../src/runtime/opencode2-server.js";
// No inference or live business requests: redirect every local discovery probe
// to a fixture that behaves like TJWater Server (/health exists, /v1/models does not).
const root = await mkdtemp(join(tmpdir(), "tjwater-provider-discovery-"));
const originalDirectory = process.cwd();
const requests: string[] = [];
const localProviders = ["vllm", "lmstudio", "ollama"];
const fixture = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch(request) {
const path = new URL(request.url).pathname;
requests.push(path);
if (path === "/health") return Response.json({ status: "ok" });
if (path === "/api/v1/models" || path === "/api/tags") return Response.json({ models: [] });
return new Response(null, { status: 404 });
} });
try {
const models = parseAgentModelConfig({
defaults: { chat: "fixture/fake", task: "fixture/fake", learning_gate: "fixture/fake" },
providers: Object.fromEntries(["fixture", ...localProviders].map((id) => [id, {
name: id, api_url: `http://127.0.0.1:${fixture.port}/v1`, models: { fake: {} },
}])),
});
const generated = serviceOpencodeConfig({}, models);
// Keep the service's actual discovery filters, without loading unrelated
// TJWater query/action plugins in this isolated provider-only test.
const filters = generated.plugins.filter((plugin) => typeof plugin === "string" && plugin.startsWith("-opencode.provider."));
process.chdir(root);
for (const fixed of [false, true]) {
const location = join(root, fixed ? "fixed" : "baseline");
await mkdir(location, { recursive: true });
const start = requests.length;
const runtime = await startOpencode2({
command: process.env.OPENCODE_COMMAND ?? "opencode2", hostname: "127.0.0.1", port: 0, timeout: 30_000,
config: { model: generated.model, providers: generated.providers, plugins: ["*", ...(fixed ? filters : [])] },
env: {
OPENCODE_DB: join(location, "native.db"), OPENCODE_CONFIG_DIR: join(location, "config"),
XDG_DATA_HOME: join(location, "data"), XDG_CONFIG_HOME: join(location, "config"),
XDG_CACHE_HOME: join(location, "cache"), XDG_STATE_HOME: join(location, "state"),
},
});
try {
const catalog = await runtime.client.model.list({ location: { directory: location } });
await Bun.sleep(1500);
const observed = requests.slice(start);
console.log(fixed ? "Fixed discovery requests:" : "Baseline discovery requests:", JSON.stringify(observed));
if (fixed) {
assert.deepEqual(observed, [], "native local-provider discovery still makes unsolicited HTTP requests");
for (const providerID of ["fixture", ...localProviders]) {
assert.ok(catalog.data.some((model) => model.providerID === providerID && model.id === "fake" && model.enabled),
`configured model unavailable: ${providerID}/fake`);
}
} else {
assert.ok(observed.includes("/health"), "baseline did not perform the vLLM health probe");
assert.ok(observed.includes("/v1/models"), "baseline did not reproduce the unwanted model request");
assert.ok(observed.includes("/api/v1/models"), "baseline did not probe LM Studio");
assert.ok(observed.includes("/api/tags"), "baseline did not probe Ollama");
}
} finally { runtime.close(); await runtime.exited; }
}
console.log("Native provider discovery regression passed; configured local models retained, no inference.");
} finally {
process.chdir(originalDirectory);
fixture.stop(true);
await rm(root, { recursive: true, force: true });
}
+129
View File
@@ -0,0 +1,129 @@
import assert from "node:assert/strict";
import express from "express";
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { startOpencode2 } from "../../src/runtime/opencode2-server.js";
import { serviceOpencodeConfig } from "../../src/runtime/opencode2-config.js";
import { assertWebSearchReady } from "../../src/runtime/web-search-check.js";
import { QUERY_MCP_TOOL_IDS, LOCAL_QUERY_TOOL_IDS } from "../../src/runtime/product-mcp-tools.js";
import { writeQueryMcpConnection } from "../../src/runtime/query-mcp-connection.js";
import { startQueryMcpServer } from "../../src/mcp/query-server.js";
import { registerMcpPolicyBridge } from "../../src/routes/mcpRoutes.js";
import { setRuntimeSessionContext, removeRuntimeSessionContext } from "../../src/runtime/sessionContext.js";
// Native discovery/execution only. Its fixture context hook stops all inference.
const root = await mkdtemp(join(tmpdir(), "tjwater-native-query-mcp-"));
const previousPath = process.env.TJWATER_AGENT_MCP_CONNECTION_PATH;
const longCalculation = process.argv.includes("--long-calculation");
const pluginDirectory = join(root, "fixture-plugin");
await mkdir(pluginDirectory);
await writeFile(join(pluginDirectory, "package.json"), JSON.stringify({ name: "tjwater-query-fixture", type: "module", exports: "./index.ts" }));
await writeFile(join(pluginDirectory, "index.ts"), `export { default } from ${JSON.stringify(resolve(".opencode/test-fixtures/mcp-call-plugin.ts"))};\n`);
const app = express(); app.use(express.json());
registerMcpPolicyBridge(app, { internalToken: "MCP_TEST_ONLY", runtime: { mcpReadonlyTools: () => QUERY_MCP_TOOL_IDS } });
const bridge = app.listen(0, "127.0.0.1");
await new Promise<void>((resolve) => bridge.once("listening", resolve));
let native: Awaited<ReturnType<typeof startOpencode2>> | undefined;
let sessionId: string | undefined;
const receivedSessions: string[] = [];
const query = startQueryMcpServer({ internalToken: "MCP_TEST_ONLY",
productClient: longCalculation ? { query: async (_id, _owner, name, args) => {
assert.equal(name, "run_simulation");
assert.equal(args.timeout_seconds, 90);
await Bun.sleep(65_000);
return { content: [{ type: "text", text: "isolated long calculation completed" }] };
} } : null,
productTools: longCalculation ? [{ name: "run_simulation", inputSchema: { type: "object", properties: {
timeout_seconds: { type: "integer", minimum: 1, maximum: 120, default: 120 },
}, additionalProperties: false } }] : [], resolveReader: async () => null,
executeLocal: async (name, args, context, signal) => {
receivedSessions.push(context.sessionId);
if (name === "todoread") {
return (await native!.client.rpc.call({ rpcID: "tjwater.tools", method: "read_todos", input: { session_id: context.sessionId }, location: { directory: context.workspaceDirectory! } }, { signal })).output;
}
return { query: args.query, actor: context.actorKey };
},
});
writeQueryMcpConnection(join(root, "sessions"), query.url, query.executionTimeoutMs);
try {
const source = JSON.parse(await readFile("opencode.json", "utf8"));
if (longCalculation) source.permissions = [...(source.permissions ?? []), {
action: "mcp_tjwater_run_simulation", resource: "*", effect: "allow",
}];
native = await startOpencode2({ command: process.env.OPENCODE_COMMAND ?? "opencode2", hostname: "127.0.0.1", port: 0, timeout: 30_000,
config: serviceOpencodeConfig({ ...source, plugins: [pluginDirectory], mcp: { servers: {} }, websearch: { provider: "fixture-search" } }),
env: { AGENT_INTERNAL_TOKEN: "MCP_TEST_ONLY", TJWATER_AGENT_INTERNAL_BASE_URL: `http://127.0.0.1:${(bridge.address() as { port: number }).port}`,
TJWATER_AGENT_MCP_CONNECTION_PATH: process.env.TJWATER_AGENT_MCP_CONNECTION_PATH!, TJWATER_AGENT_WORKSPACE_ROOT: join(root, "workspaces"),
OPENCODE_DB: join(root, "data", "opencode.db"), XDG_DATA_HOME: join(root, "data"), XDG_CONFIG_HOME: join(root, "config"), XDG_CACHE_HOME: join(root, "cache"), XDG_STATE_HOME: join(root, "state") },
});
const session = await native.client.session.create({ location: { directory: process.cwd() }, agent: "instruction" });
sessionId = session.id;
setRuntimeSessionContext({ sessionId, clientSessionId: sessionId, actorKey: "fixture-actor", projectKey: "personal-fixture", traceId: "test", purpose: "ordinary_chat", workspaceDirectory: session.location.directory });
await native.client.session.prompt({ sessionID: sessionId, text: "Materialize fixture tools; stop before inference." });
await native.client.session.wait({ sessionID: sessionId }, { signal: AbortSignal.timeout(20_000) });
const deadline = Date.now() + 10_000;
let tools: { id: string; direct: boolean }[] = [];
do {
tools = (await native.client.rpc.call({ rpcID: "tjwater.tools", method: "list", input: {}, location: session.location })).output as typeof tools;
if (LOCAL_QUERY_TOOL_IDS.every((id) => tools.some((tool) => tool.id === id && tool.direct))) break;
await new Promise((resolve) => setTimeout(resolve, 100));
} while (Date.now() < deadline);
assert(LOCAL_QUERY_TOOL_IDS.every((id) => tools.some((tool) => tool.id === id && tool.direct)), "native MCP catalog must expose every independent query");
assert(tools.some((tool) => tool.id === "websearch"), "native websearch must remain registered");
await assertWebSearchReady(native.client, session.location.directory);
assert(!tools.some((tool) => tool.id.startsWith("browser_")), "unused native browser automation tools must not be registered");
const plugins = await native.client.plugin.list({ location: session.location });
assert(!plugins.data.some((plugin) => plugin.id === "opencode.browser" && plugin.state.status === "active"), "native browser plugin must be disabled");
for (const action of ["browser", "browser_click"]) {
assert.equal((await native.client.permission.create({ sessionID: sessionId, action, resources: ["*"], save: [] })).effect, "deny");
}
for (const action of ["present_answer", "show_chart"]) {
assert.equal((await native.client.permission.create({ sessionID: sessionId, action, resources: ["*"], save: [] })).effect, "allow");
}
for (const id of ["opencode_models", "opencode_session_move", "opencode_session_rename",
"opencode_worktree_create", "opencode_worktree_list", "opencode_worktree_remove"]) {
assert(tools.some((tool) => tool.id === id), `${id} must remain registered`);
}
assert(!tools.some((tool) => tool.id === "mcp_tjwater_web_search"), "web search must not have a duplicate MCP tool");
for (const id of ["query_project_data", "query_operational_task_data", "get_operational_task_context", "session_search", "web_search", "geocode", "todoread", "list_investigation_tasks", "get_investigation_task", "read_agent_messages"]) assert(!tools.some((tool) => tool.id === id), `${id} must not remain a standalone plugin query`);
for (const id of ["memory_manager", "skill_manager", "send_task_message", "reply_agent_message", "todowrite",
"present_answer", "final_answer", "locate_features", "zoom_to_map", "show_chart", "view_history",
"render_scada_analysis", "clear_scada_analysis", "publish_execution_plan", "publish_task_report",
"publish_task_progress", "publish_task_presentation", "propose_task_action", "add_task_human_evidence",
"request_task_data_access"]) assert(tools.some((tool) => tool.id === id), `${id} must remain a plugin tool`);
for (const tool of ["mcp_tjwater_session_search", "mcp_tjwater_todoread"]) {
const result = await native.client.rpc.call({ rpcID: "tjwater.mcp-fixture", method: "call", input: { session_id: sessionId, tool, arguments: tool.endsWith("session_search") ? { query: "native-mcp" } : {} }, location: session.location }, { signal: AbortSignal.timeout(10_000) });
assert.equal((result.output as { error?: string }).error, undefined, JSON.stringify(result.output));
assert((result.output as { evaluated: string[] }).evaluated.includes(tool));
}
assert.deepEqual(receivedSessions, [sessionId, sessionId], "OpenCode must supply the invoking session in native MCP metadata");
const search = await native.client.rpc.call({ rpcID: "tjwater.mcp-fixture", method: "call", input: {
session_id: sessionId, tool: "websearch", arguments: { query: "native-search-fixture" },
}, location: session.location }, { signal: AbortSignal.timeout(10_000) });
assert.equal((search.output as { error?: string }).error, undefined, JSON.stringify(search.output));
assert(JSON.stringify(search.output).includes("https://example.test/search-result"), "native websearch must execute through its selected provider");
if (longCalculation) {
setRuntimeSessionContext({ sessionId, clientSessionId: sessionId, actorKey: "fixture-actor", projectKey: "fixture-project", projectId: "fixture-project", traceId: "test", purpose: "ordinary_chat", workspaceDirectory: session.location.directory });
const started = performance.now();
console.log("Starting isolated native MCP delay: requested budget 90 seconds, fixture delay 65 seconds.");
const result = await native.client.rpc.call({ rpcID: "tjwater.mcp-fixture", method: "call", input: {
session_id: sessionId, tool: "mcp_tjwater_run_simulation", arguments: { timeout_seconds: 90 },
}, location: session.location }, { signal: AbortSignal.timeout(110_000) });
const output = result.output as { error?: string; content: unknown };
assert(!output.error, output.error);
assert(JSON.stringify(output.content).includes("isolated long calculation completed"));
const elapsed = performance.now() - started;
assert(elapsed >= 60_000, `Long-call fixture returned too early (${Math.round(elapsed)}ms)`);
console.log(`Native MCP long calculation passed after ${Math.round(elapsed)}ms: a 90-second Agent budget crosses the old 30/35/60-second limits; no real simulation or model inference.`);
}
console.log("Native query MCP smoke passed: browser automation absent; business UI and managers retained; discovery, permissions, session metadata, checklist RPC and native websearch verified; no model inference or external search.");
} finally {
if (sessionId) { removeRuntimeSessionContext(sessionId); await native?.client.session.remove({ sessionID: sessionId }).catch(() => {}); }
native?.close();
if (native) await native.exited;
await query.close();
await new Promise<void>((resolve) => bridge.close(() => resolve()));
if (previousPath === undefined) delete process.env.TJWATER_AGENT_MCP_CONNECTION_PATH; else process.env.TJWATER_AGENT_MCP_CONNECTION_PATH = previousPath;
await rm(root, { recursive: true, force: true });
}
+89
View File
@@ -0,0 +1,89 @@
// Native inbox acceptance and delivery changes against a local deterministic model.
import assert from "node:assert/strict";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { startOpencode2 } from "../../src/runtime/opencode2-server.js";
import { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";
import { ResponseInputTracker } from "../../src/chat/response-inputs.js";
import { streamPromptResponse } from "../../src/routes/chatStream.js";
import { config } from "../../src/config.js";
const root = await mkdtemp(join(tmpdir(), "tjwater-response-input-"));
config.LLM_REQUEST_AUDIT_LOG_PATH = join(root, "fixture-audit.log");
let release = () => {};
const gate = new Promise<void>((resolve) => { release = resolve; });
let releaseSecond = () => {};
const secondGate = new Promise<void>((resolve) => { releaseSecond = resolve; });
const prompts: unknown[] = [];
const fixture = Bun.serve({ hostname: "127.0.0.1", port: 0, async fetch(request) {
if (new URL(request.url).pathname !== "/v1/chat/completions") return new Response(null, { status: 404 });
prompts.push(await request.json());
if (prompts.length === 1) await gate;
if (prompts.length === 2) await secondGate;
const chunk = (delta: object, finish_reason: string | null) => JSON.stringify({ id: "fixture", object: "chat.completion.chunk", created: 1,
model: "fake", choices: [{ index: 0, delta, finish_reason }] });
return new Response(`data: ${chunk({ role: "assistant", content: "Local completed." }, null)}\n\ndata: ${chunk({}, "stop")}\n\ndata: [DONE]\n\n`,
{ headers: { "Content-Type": "text/event-stream" } });
} });
let native: Awaited<ReturnType<typeof startOpencode2>> | undefined;
const wait = async (label: string, predicate: () => boolean | Promise<boolean>) => {
const deadline = Date.now() + 15_000;
while (!await predicate()) { if (Date.now() > deadline) throw new Error(`native input smoke timed out: ${label}, fixture requests=${prompts.length}`); await Bun.sleep(20); }
};
try {
native = await startOpencode2({ command: process.env.OPENCODE_COMMAND ?? "opencode2", hostname: "127.0.0.1", port: 0, timeout: 30_000,
config: { model: "fixture/fake", agents: { instruction: { mode: "primary", model: "fixture/fake", system: "Reply with a short local fixture answer." } },
providers: { fixture: { name: "Fixture", package: "@opencode/ai/providers/openai-compatible",
settings: { baseURL: `http://127.0.0.1:${fixture.port}/v1`, apiKey: "LOCAL_FIXTURE" },
models: { fake: { capabilities: { tools: true }, limit: { context: 32768, output: 1024 } } } } } },
env: { OPENCODE_DB: join(root, "native.db"), XDG_DATA_HOME: join(root, "data"), XDG_CONFIG_HOME: join(root, "config"),
XDG_CACHE_HOME: join(root, "cache"), XDG_STATE_HOME: join(root, "state"), TJWATER_AGENT_MCP_CONNECTION_PATH: join(root, "no-connection") },
});
const client = native.client;
const session = await client.session.create({ title: "Inbox smoke fixture", location: { directory: root } });
await client.session.switchModel({ sessionID: session.id, model: { providerID: "fixture", id: "fake" } });
const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { ensureClient: async () => client }) as OpencodeRuntimeAdapter;
const consumed: string[] = [];
const tracker = new ResponseInputTracker(runtime, session.id, (input) => { if (input.status === "delivered") consumed.push(input.id); });
const tokens: string[] = [];
let finished = false;
const running = streamPromptResponse({ runtime, sessionId: session.id, clientSessionId: session.id, message: "Original local task", model: "fixture/fake",
beforePrompt: () => tracker.restore(), onPromptDispatched: () => tracker.markReady(),
onInboxEvent: (type, data) => tracker.observe(type, data), waitForUserInput: async () => !await tracker.canClose(),
write: (event, data) => { if (event === "token") tokens.push(String(data.content)); } });
void running.catch(() => {});
void running.then(() => { finished = true; }, () => { finished = true; });
await wait("first local generation", () => prompts.length >= 1);
const item = await tracker.submit({ id: `msg_${"1".repeat(32)}`, text: "Additional local user input", delivery: "queue", status: "pending" });
assert.equal(item.delivery, "queue");
assert.equal((await client.session.inbox.list({ sessionID: session.id })).find((entry) => entry.id === item.id)?.delivery, "queue");
await client.session.inbox.update({ sessionID: session.id, inboxID: item.id, delivery: "steer" });
assert.equal((await client.session.inbox.list({ sessionID: session.id })).find((entry) => entry.id === item.id)?.delivery, "steer");
await client.session.inbox.update({ sessionID: session.id, inboxID: item.id, delivery: "queue" });
release();
await wait("queued continuation", () => prompts.length >= 2);
await Bun.sleep(100);
assert.equal(finished, false, "managed stream closed while queued generation was running");
releaseSecond();
let finishTimer: ReturnType<typeof setTimeout> | undefined;
const result = await Promise.race([running, new Promise<never>((_resolve, reject) => {
finishTimer = setTimeout(() => reject(new Error("managed stream did not finish")), 10_000);
finishTimer.unref();
})]).finally(() => clearTimeout(finishTimer));
assert.equal(result.failed, false);
assert.equal(result.aborted, false);
assert.ok(consumed.includes(item.id));
assert.ok(tokens.join("").split("Local completed.").length >= 3, "managed stream closed before queued answer");
assert.ok(JSON.stringify(prompts[1]).includes("Additional local user input"));
assert.ok(!(await client.session.inbox.list({ sessionID: session.id })).some((entry) => entry.id === item.id));
await client.session.remove({ sessionID: session.id });
console.log("Native queue delivery, pending steer/queue changes and automatic continuation passed; local fixture only.");
} finally {
release();
releaseSecond();
native?.close();
if (native) await native.exited;
fixture.stop(true);
await rm(root, { recursive: true, force: true });
}
+74
View File
@@ -0,0 +1,74 @@
// 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 });
}
+76
View File
@@ -0,0 +1,76 @@
import assert from "node:assert/strict";
import { randomBytes } from "node:crypto";
import { lstat, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { bindScopedConversationWorkspace, createConversationWorkspace } from "../../src/runtime/conversationWorkspace.js";
import { SessionWorkspaceStore } from "../../src/sessions/workspace-store.js";
import { serviceOpencodeConfig } from "../../src/runtime/opencode2-config.js";
import { startOpencode2 } from "../../src/runtime/opencode2-server.js";
import { OpencodeRuntimeAdapter } from "../../src/runtime/opencode.js";
import { config } from "../../src/config.js";
import { isSessionNotFoundError } from "@opencode/client";
// Native permissions only; uses temporary data and no model inference.
const root = await mkdtemp(join(tmpdir(), "tjwater-workspace-smoke-"));
const workspaceRoot = join(root, "data", "session-workspaces");
const previousWorkspaceRoot = config.CONVERSATION_WORKSPACE_ROOT;
config.CONVERSATION_WORKSPACE_ROOT = workspaceRoot;
let directory = await createConversationWorkspace(workspaceRoot);
let other = await createConversationWorkspace(workspaceRoot);
await mkdir(join(root, "data", "session-metadata"));
let native: Awaited<ReturnType<typeof startOpencode2>> | undefined;
let sessionID: string | undefined;
let otherSessionID: string | undefined;
try {
native = await startOpencode2({
command: process.env.OPENCODE_COMMAND ?? "opencode2", hostname: "127.0.0.1", port: 0, timeout: 30_000,
config: serviceOpencodeConfig(JSON.parse(await readFile("opencode.json", "utf8")), undefined, workspaceRoot),
env: { AGENT_INTERNAL_TOKEN: randomBytes(32).toString("hex"), TJWATER_AGENT_WORKSPACE_ROOT: workspaceRoot,
WATERNETWORK_API_KEY: "WORKSPACE_TEST_ONLY", XDG_DATA_HOME: join(root, "native-data"),
OPENCODE_DB: join(root, "native-data", "opencode", "opencode.db"),
XDG_CONFIG_HOME: join(root, "native-config"), XDG_CACHE_HOME: join(root, "native-cache"), XDG_STATE_HOME: join(root, "native-state") },
});
const session = await native.client.session.create({ agent: "instruction", location: { directory } });
sessionID = session.id;
const owner = { actorKey: "actor-smoke", projectKey: "project-smoke", sessionId: session.id };
directory = await bindScopedConversationWorkspace(directory, workspaceRoot, owner);
await new SessionWorkspaceStore(workspaceRoot).ensure(owner);
await native.client.session.move({ sessionID, directory });
const otherSession = await native.client.session.create({ agent: "instruction", location: { directory: other } });
otherSessionID = otherSession.id;
other = await bindScopedConversationWorkspace(other, workspaceRoot,
{ actorKey: "actor-other", projectKey: "personal-actor-other", sessionId: otherSession.id });
await native.client.session.move({ sessionID: otherSessionID, directory: other });
await native.client.rpc.call({ rpcID: "tjwater.tools", method: "list", input: {}, location: { directory } });
const decision = async (action: string, resource: string) => (await native!.client.permission.create({ sessionID: session.id, action, resources: [resource], save: [] })).effect;
const file = join(directory, "files", "result.json");
await writeFile(file, '{"value":7}');
assert.equal(await decision("read", file), "allow", "own workspace results must be readable under data/");
assert.equal(await decision("edit", file), "ask", "own workspace writes must require approval");
assert.equal(await decision("read", join(root, "data", "session-metadata", "private.json")), "deny");
assert.equal(await decision("read", join(other, "files", "result.json")), "deny");
assert.equal(await decision("read", join(directory, "files", ".local.env")), "deny");
assert.equal(await decision("read", join(directory, "..", "chat", "owner.json")), "deny", "chat records must stay outside the model workspace");
assert.equal(await decision("edit", join(directory, "..", "chat", "messages.json")), "deny");
await native.client.rpc.call({ rpcID: "tjwater.tools", method: "list", input: {}, location: { directory: other } });
await native.client.session.move({ sessionID, directory: other });
assert.equal(await decision("read", join(other, "files", "result.json")), "deny", "a moved session cannot impersonate the owner of another workspace");
await native.client.session.move({ sessionID, directory });
const runtime = Object.assign(new OpencodeRuntimeAdapter(), { ensureClient: async () => native!.client });
await runtime.removeSession(sessionID, owner);
await assert.rejects(native.client.session.get({ sessionID }), isSessionNotFoundError);
await assert.rejects(lstat(directory), { code: "ENOENT" });
assert.equal((await native.client.session.get({ sessionID: otherSessionID })).id, otherSessionID);
assert.equal((await lstat(other)).isDirectory(), true);
assert.equal(JSON.parse(await readFile(join(directory, "..", "chat", "owner.json"), "utf8")).sessionId, sessionID);
await runtime.removeSession(sessionID, owner);
console.log("Native workspace permissions and session cleanup passed; other sessions and host chat records preserved. No inference requests.");
} finally {
if (native && sessionID) await native.client.session.remove({ sessionID }).catch(() => {});
if (native && otherSessionID) await native.client.session.remove({ sessionID: otherSessionID }).catch(() => {});
native?.close();
if (native) await native.exited;
config.CONVERSATION_WORKSPACE_ROOT = previousWorkspaceRoot;
await rm(root, { recursive: true, force: true });
}