feat(agent): migrate OpenCode 2 runtime and sandbox tools

Upgrade the runtime and native plugin API to OpenCode 2.0.24. Restore
session-scoped learning, credential renewal, typed project queries and
controlled frontend actions from the legacy Agent.

Replace host Shell execution with an approved Landlock/seccomp executor
and persistent per-session workspaces. Add typed Bocha search and Tianditu
geocoding bridges using the owning session credential. Require a working
kernel sandbox before startup and persist workspace files in deployments.

Validation completed before this commit: TypeScript checks, 150 Bun tests,
Docker build and UID 10001 sandbox execution, native Shell approval and
session recovery. Tianditu succeeded; Bocha currently reports exhausted
account quota. Unrelated investigation-report contract work is excluded.
This commit is contained in:
2026-10-08 15:46:37 +08:00
parent 1a9b0cafc1
commit 7383ed6d13
93 changed files with 8065 additions and 1098 deletions
+78 -49
View File
@@ -1,6 +1,9 @@
import { describe, expect, it } from "bun:test";
import { type OpencodeClient } from "@opencode-ai/sdk/v2";
import { type OpenCodeClient as OpencodeClient } from "@opencode/client";
import { createServer } from "node:net";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { config } from "../../src/config.js";
import {
@@ -62,7 +65,7 @@ describe("OpencodeRuntimeAdapter.revertToUserMessage", () => {
expect(calls).toEqual({
reverted: ["user-2"],
removed: ["assistant-2", "user-2"],
removed: ["user-2"],
});
});
});
@@ -94,19 +97,9 @@ describe("OpencodeRuntimeAdapter.ensureClient", () => {
describe("OpencodeRuntimeAdapter.resolveAvailableModel", () => {
it("selects the first preferred model actually exposed by OpenCode", async () => {
const client = {
config: {
providers: async () => ({
data: {
providers: [{
id: "deepseek",
models: {
"deepseek-flash": {},
"deepseek-v4-pro": {},
},
}],
default: {},
},
}),
rpc: { call: async () => ({ output: [] }) },
model: {
list: async () => ({ data: [{ providerID: "deepseek", id: "deepseek-v4-pro", enabled: true, capabilities: { tools: true } }] }),
},
} as unknown as OpencodeClient;
const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), {
@@ -114,7 +107,7 @@ describe("OpencodeRuntimeAdapter.resolveAvailableModel", () => {
}) as OpencodeRuntimeAdapter;
await expect(runtime.resolveAvailableModel([
"deepseek/deepseek-v4-flash",
"deepseek/deepseek-flash",
"deepseek/deepseek-v4-pro",
])).resolves.toBe("deepseek/deepseek-v4-pro");
});
@@ -147,45 +140,81 @@ describe("resolveEmbeddedOpencodePort", () => {
});
describe("OpencodeRuntimeAdapter.warmup", () => {
it("initializes the project session and model tools before reporting ready", async () => {
const toolIds = ["shell", "web_search", "geocode", "query_project_data", "final_answer", "locate_features", "zoom_to_map", "show_chart", "view_history", "session_search", "memory_manager", "skill_manager", "render_scada_analysis", "clear_scada_analysis", "get_operational_task_context", "query_operational_task_data", "publish_execution_plan", "publish_task_report", "propose_task_action", "add_task_human_evidence", "request_task_data_access", "todoread", "todowrite"];
async function createWarmupRuntime(missing: string[] = []) {
const root = await mkdtemp(join(tmpdir(), "warmup-workspace-test-"));
const previousRoot = config.CONVERSATION_WORKSPACE_ROOT;
config.CONVERSATION_WORKSPACE_ROOT = root;
const calls: string[] = [];
const [providerID, id] = config.OPENCODE_MODEL.split("/");
const client = {
global: {
health: async () => {
calls.push("health");
return { data: { healthy: true, version: "test" } };
},
},
server: { info: async () => { calls.push("health"); return { version: "2.0.24" }; } },
session: {
create: async () => {
calls.push("session.create");
return { data: { id: "warmup-session" } };
},
delete: async ({ sessionID }: { sessionID: string }) => {
calls.push(`session.delete:${sessionID}`);
return { data: true };
},
},
tool: {
list: async (model: { provider: string; model: string }) => {
calls.push(`tool.list:${model.provider}/${model.model}`);
return { data: [] };
},
create: async (input: { location: { directory: string } }) => { calls.push("session.create"); return { id: "warmup-session", location: input.location }; },
remove: async () => { calls.push("session.remove"); },
},
model: { list: async () => { calls.push("model.list"); return { data: [{ providerID, id, enabled: true, capabilities: { tools: true } }] }; } },
rpc: { call: async () => { calls.push("tool.list"); return { output: toolIds.filter((id) => !missing.includes(id)).map((id) => ({ id, direct: true, sandboxed: id === "shell", workspaceRoot: root })) }; } },
} as unknown as OpencodeClient;
const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), {
clientPromise: null,
closeServer: null,
ensureClient: async () => client,
}) as OpencodeRuntimeAdapter;
const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { ensureClient: async () => client }) as OpencodeRuntimeAdapter;
return { runtime, calls, cleanup: async () => { config.CONVERSATION_WORKSPACE_ROOT = previousRoot; await rm(root, { recursive: true, force: true }); } };
}
it("verifies business tools and model before reporting ready and removes the warmup session", async () => {
const { runtime, calls, cleanup } = await createWarmupRuntime();
try { await runtime.warmup();
expect(calls).toEqual(["health", "session.create", "tool.list", "model.list", "session.remove"]); } finally { await cleanup(); }
});
it("fails readiness if a business tool was lost and still cleans up", async () => {
const { runtime, calls, cleanup } = await createWarmupRuntime(["render_scada_analysis"]);
try { await expect(runtime.warmup()).rejects.toThrow("business tools missing: render_scada_analysis");
expect(calls.at(-1)).toBe("session.remove"); } finally { await cleanup(); }
});
});
await runtime.warmup();
describe("conversation workspace binding", () => {
it("coalesces old-session migration and restores its persisted directory", async () => {
const previousRoot = config.CONVERSATION_WORKSPACE_ROOT;
const root = await mkdtemp(join(tmpdir(), "runtime-workspace-test-"));
config.CONVERSATION_WORKSPACE_ROOT = root;
let directory = process.cwd();
let moves = 0;
const client = {
session: {
get: async () => ({ id: "old-session", location: { directory } }),
move: async (input: { directory: string }) => { moves++; directory = input.directory; },
},
} as unknown as OpencodeClient;
const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { ensureClient: async () => client }) as OpencodeRuntimeAdapter;
try {
const [first, second] = await Promise.all([runtime.ensureSessionWorkspace("old-session"), runtime.ensureSessionWorkspace("old-session")]);
expect(first).toBe(second);
expect(first).not.toBe(process.cwd());
expect(moves).toBe(1);
const restored = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { ensureClient: async () => client }) as OpencodeRuntimeAdapter;
expect(await restored.ensureSessionWorkspace("old-session")).toBe(first);
expect(moves).toBe(1);
directory = "/tmp";
await expect(restored.ensureSessionWorkspace("foreign-session")).rejects.toThrow("inside");
} finally { config.CONVERSATION_WORKSPACE_ROOT = previousRoot; await rm(root, { recursive: true }); }
});
});
expect(calls).toEqual([
"health",
"session.create",
`tool.list:${config.OPENCODE_MODEL}`,
"session.delete:warmup-session",
]);
describe("explicit native Shell approval", () => {
it("ties the pending decision to the full command and originating native call", async () => {
const events = [
{ type: "connected" },
{ type: "permission.replied", data: { sessionID: "other", requestID: "perm-test", reply: "once" } },
{ type: "permission.replied", data: { sessionID: "session-test", requestID: "perm-test", reply: "once" } },
];
let created: unknown;
const client = {
event: { subscribe: () => ({ async *[Symbol.asyncIterator]() { for (const event of events) yield event; } }) },
permission: { create: async (input: unknown) => { created = input; return { id: "perm-test", effect: "ask" }; } },
} as unknown as OpencodeClient;
const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { ensureClient: async () => client }) as OpencodeRuntimeAdapter;
await runtime.approveSandboxShell({ sessionId: "session-test", command: "printf hello", callId: "call-test", messageId: "message-test" }, new AbortController().signal);
expect(created).toMatchObject({ action: "shell", resources: ["printf hello"], save: [], source: { type: "tool", id: "call-test", messageID: "message-test" } });
events[2] = { type: "permission.replied", data: { sessionID: "session-test", requestID: "perm-test", reply: "reject" } };
await expect(runtime.approveSandboxShell({ sessionId: "session-test", command: "printf hello", callId: "call-test", messageId: "message-test" }, new AbortController().signal)).rejects.toThrow("rejected");
});
});