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

77 lines
5.5 KiB
TypeScript

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 });
}