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:
@@ -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 });
|
||||
}
|
||||
Reference in New Issue
Block a user