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
+28
View File
@@ -0,0 +1,28 @@
import type { Part } from "@opencode-ai/sdk/v2";
const labels = { reference: "查找所需资料", data: "获取业务数据", analysis: "分析与核查", visualization: "更新地图与图表", other: "执行操作" };
const classify = (tool: string): keyof typeof labels => {
if (["skill", "read", "glob", "grep", "session_search", "memory_manager", "skill_manager"].includes(tool)) return "reference";
if (["query_project_data", "query_operational_task_data", "get_operational_task_context"].includes(tool)) return "data";
if (["render_scada_analysis", "clear_scada_analysis", "locate_features", "zoom_to_map", "show_chart", "view_history"].includes(tool)) return "visualization";
if (["publish_execution_plan", "publish_task_report", "propose_task_action", "add_task_human_evidence"].includes(tool)) return "analysis";
return "other";
};
export const createBusinessStageTracker = (write: (event: string, data: Record<string, unknown>) => void) => {
const stages = new Map<string, { startedAt: number; actions: Map<string, string> }>();
return (part: Extract<Part, { type: "tool" }>) => {
if (["final_answer", "todowrite", "todoread", "question"].includes(part.tool)) return;
const kind = classify(part.tool);
const stage = stages.get(kind) ?? { startedAt: Date.now(), actions: new Map() };
stage.actions.set(part.id, part.state.status);
stages.set(kind, stage);
const statuses = [...stage.actions.values()];
const running = statuses.some((status) => status === "pending" || status === "running");
const status = running ? "running" : statuses.includes("error") ? "error" : "completed";
write("progress", {
id: `business-${kind}`, phase: kind, status, title: labels[kind],
detail: `${statuses.filter((s) => s === "completed").length}/${statuses.length} 项操作已完成`,
started_at: stage.startedAt, ...(running ? { elapsed_ms: Date.now() - stage.startedAt } : { ended_at: Date.now(), duration_ms: Date.now() - stage.startedAt }),
});
};
};