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
+31 -10
View File
@@ -1,3 +1,4 @@
import { credentialRefreshCoordinator } from "../product/credential-refresh.js";
import { type Router } from "express";
import { randomUUID } from "node:crypto";
import {
@@ -112,8 +113,10 @@ const isUiMessage = (value: unknown): value is UIMessage =>
(value.role === "system" || value.role === "user" || value.role === "assistant") &&
Array.isArray(value.parts);
const readUiMessageText = (message: UIMessage | undefined) =>
message?.parts
const readUiMessageText = (message: UIMessage | undefined) => {
const finalPart = message?.parts.slice().reverse().find((part) => isObjectRecord(part) && part.type === "data-final_answer" && isObjectRecord(part.data) && typeof part.data.content === "string");
if (finalPart && "data" in finalPart && isObjectRecord(finalPart.data)) return String(finalPart.data.content).trim();
return message?.parts
.flatMap((part) =>
isObjectRecord(part) && part.type === "text" && typeof part.text === "string"
? [part.text]
@@ -121,6 +124,7 @@ const readUiMessageText = (message: UIMessage | undefined) =>
)
.join("")
.trim() ?? "";
};
const readLatestUserText = (messages: UIMessage[]) => {
for (let index = messages.length - 1; index >= 0; index -= 1) {
@@ -142,10 +146,8 @@ const toUiMessages = (messages: unknown[] | undefined) =>
export const toCollaborationMessages = (messages: UIMessage[]) =>
messages.flatMap((message) => {
if (message.role !== "user" && message.role !== "assistant") return [];
const textParts = message.parts.filter(
(part) => isObjectRecord(part) && part.type === "text" && typeof part.text === "string",
);
return textParts.length > 0 ? [{ ...message, parts: textParts } as UIMessage] : [];
const content = readUiMessageText(message);
return content ? [{ ...message, parts: [{ type: "text" as const, text: content }] } as UIMessage] : [];
});
const toLegacyMessages = (messages: UIMessage[]) =>
@@ -651,6 +653,10 @@ export const registerChatStreamRoute = (
content: `${typeof message.content === "string" ? message.content : ""}${typeof data.content === "string" ? data.content : ""}`,
isError: false,
}));
} else if (event === "final_answer") {
activeRun.messages = updateLastAssistantMessage(activeRun.messages, (message) => ({
...message, content: typeof data.content === "string" ? data.content : message.content,
}));
} else if (event === "progress") {
activeRun.messages = updateLastAssistantMessage(activeRun.messages, (message) => ({
...message,
@@ -826,6 +832,17 @@ export const registerChatStreamRoute = (
const unregisterFrontendActionSink = parsed.data.capabilities.includes("frontend-action@1")
? frontendActionCoordinator.registerSink(clientSessionId, (request) => publish("frontend_action", request as unknown as Record<string, unknown>))
: undefined;
let credentialFailure: Error | undefined;
const unsubscribeCredentialRefresh = credentialRefreshCoordinator.subscribe(binding.sessionId, (event) => {
publish(event.type, { ...event, session_id: clientSessionId, request_id: event.requestId });
if (event.type === "credential_refresh_failed") {
credentialFailure = new Error(event.message);
abortController.abort();
}
});
if (sessionPurpose === "task_execution") credentialRefreshCoordinator.enableBackground(binding.sessionId);
const cancelCredentialRefresh = () => credentialRefreshCoordinator.cancelSession(binding.sessionId);
abortController.signal.addEventListener("abort", cancelCredentialRefresh, { once: true });
let taskHeartbeat: ReturnType<typeof setInterval> | undefined;
let taskHeartbeatActive = true;
let taskHeartbeatPromise: Promise<void> | undefined;
@@ -897,7 +914,9 @@ export const registerChatStreamRoute = (
publish(event, data);
},
});
if (taskLeaseFailure) {
if (credentialFailure) {
publish("error", { session_id: clientSessionId, message: "会话凭据续期失败,本次执行已停止。" });
} else if (taskLeaseFailure) {
publish("error", {
session_id: clientSessionId,
message: "工况任务租约已失效,本次执行已停止,等待任务重新领取。",
@@ -920,7 +939,7 @@ export const registerChatStreamRoute = (
activeSessionRecord.taskId,
activeSessionRecord.runId,
clientSessionId,
streamResult.aborted ? "AGENT_ABORTED" : "AGENT_STREAM_FAILED",
credentialFailure ? "CREDENTIAL_REFRESH_FAILED" : streamResult.aborted ? "AGENT_ABORTED" : "AGENT_STREAM_FAILED",
streamResult.aborted ? "Agent execution was aborted" : "Agent stream failed",
);
taskRunFinalized = true;
@@ -963,7 +982,7 @@ export const registerChatStreamRoute = (
const assistantMessage = [...messages]
.reverse()
.find((message) => message.info.role === "assistant");
const assistantText = collectTextContent(assistantMessage?.parts ?? []);
const assistantText = readUiMessageText(latestUiMessages.at(-1)) || collectTextContent(assistantMessage?.parts ?? []);
const latestSessionRecord =
(await sessionMetadataStore.get(
{ actorKey, projectId, projectKey, userId },
@@ -1046,6 +1065,8 @@ export const registerChatStreamRoute = (
}
throw error;
} finally {
unsubscribeCredentialRefresh();
abortController.signal.removeEventListener("abort", cancelCredentialRefresh);
await stopTaskHeartbeat();
await taskArtifactPublishQueue.catch(() => undefined);
unregisterFrontendActionSink?.();
@@ -1079,7 +1100,7 @@ export const registerChatStreamRoute = (
});
sessionBridge.finalizeRequest(clientSessionId);
releaseOperationalTaskCredential(binding.sessionId);
activeRun.status = taskLeaseFailure
activeRun.status = (taskLeaseFailure || credentialFailure)
? "error"
: abortController.signal.aborted
? "aborted"