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
+10 -124
View File
@@ -1,12 +1,5 @@
import { tool } from "@opencode-ai/plugin";
import { MemoryStore } from "../../src/memory/store.js";
import {
getRuntimeSessionContext,
setRuntimeSessionContext,
} from "../../src/runtime/sessionContext.js";
const memoryStore = new MemoryStore();
const initializePromise = memoryStore.initialize();
import { tool } from "../tool.js";
import { getInternalBridgeConfig } from "../internalBridge.js";
export default tool({
description:
@@ -35,121 +28,14 @@ export default tool({
.describe("Stable memory entry id used by replace/remove."),
},
async execute(args, context) {
await initializePromise;
const sessionContext = getRuntimeSessionContext(context.sessionID);
if (!sessionContext) {
throw new Error(`session context not found for ${context.sessionID}`);
}
const scope =
args.scope === "user"
? "user"
: args.scope === "workspace"
? "workspace"
: null;
if (!scope) {
return JSON.stringify({
ok: true,
kind: "memory",
decision: "rejected",
detail: `unsupported scope: ${args.scope}; use exact keyword 'user' or 'workspace'`,
});
}
if (sessionContext.allowLearningWrite === false && args.action !== "list") {
return JSON.stringify({
ok: true,
kind: "memory",
decision: "rejected",
detail: "memory writes are disabled for this session",
});
}
const scopeKey =
scope === "user" ? sessionContext.actorKey : sessionContext.projectKey;
if (args.action === "list") {
const readScopes = {
...(sessionContext.memoryListReadScopes ?? {}),
[scope]: true,
};
setRuntimeSessionContext({
...sessionContext,
memoryListReadScopes: readScopes,
});
return JSON.stringify({
ok: true,
kind: "memory",
decision: "accepted",
detail: "memory listed",
items: await memoryStore.list(scope, scopeKey),
target: scope,
});
}
if (args.action === "add") {
if (sessionContext.memoryListReadScopes?.[scope] !== true) {
return JSON.stringify({
ok: true,
kind: "memory",
decision: "rejected",
detail: `must list ${scope} memory and review existing entries before add`,
target: scope,
});
}
const result = await memoryStore.upsert(scope, scopeKey, {
content: args.content ?? "",
sessionId: sessionContext.clientSessionId,
source: "tool",
traceId: sessionContext.traceId,
});
if (!result.entry) {
return JSON.stringify({
ok: true,
kind: "memory",
decision: "rejected",
detail: "content rejected by persistence policy",
});
}
return JSON.stringify({
ok: true,
kind: "memory",
decision: result.changed ? "accepted" : "deduped",
detail: result.detail,
entry: result.entry,
target: scope,
});
}
if (args.action === "replace") {
const result = await memoryStore.replace(
scope,
scopeKey,
args.target_id ?? "",
{
content: args.content ?? "",
sessionId: sessionContext.clientSessionId,
source: "tool",
traceId: sessionContext.traceId,
},
);
return JSON.stringify({
ok: true,
kind: "memory",
decision: result.changed ? "accepted" : "rejected",
detail: result.detail,
target: scope,
});
}
const result = await memoryStore.remove(
scope,
scopeKey,
args.target_id ?? "",
);
return JSON.stringify({
ok: true,
kind: "memory",
decision: result.changed ? "accepted" : "rejected",
detail: result.detail,
target: scope,
const { baseUrl, internalToken } = getInternalBridgeConfig();
const response = await fetch(`${baseUrl}/internal/tools/memory-manager`, {
method: "POST",
headers: { "Content-Type": "application/json", "x-agent-internal-token": internalToken },
body: JSON.stringify({ session_id: context.sessionID, input: args }),
});
const text = await response.text();
if (!response.ok) throw new Error(`learning tool failed (${response.status}): ${text}`);
return text;
},
});