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:
@@ -19,6 +19,26 @@ const createEventStream = (events: unknown[]) => ({
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
describe("streamPromptResponse", () => {
|
||||
it("keeps the first pending event when the V2 prompt acknowledgement arrives first", async () => {
|
||||
const emitted: { event: string; data: Record<string, unknown> }[] = [];
|
||||
const runtime = {
|
||||
prompt: async () => undefined,
|
||||
subscribeEvents: async () => ({
|
||||
async *[Symbol.asyncIterator]() {
|
||||
await sleep(5);
|
||||
yield { type: "message.part.updated", properties: { sessionID: "s", part: { id: "text", type: "text", text: "", sessionID: "s", messageID: "m" } } };
|
||||
yield { type: "message.part.delta", properties: { sessionID: "s", partID: "text", field: "text", delta: "流事件完整" } };
|
||||
yield { type: "session.idle", properties: { sessionID: "s" } };
|
||||
},
|
||||
}),
|
||||
messages: async () => { throw new Error("lost streamed events caused a fallback read"); },
|
||||
} as unknown as OpencodeRuntimeAdapter;
|
||||
const result = await streamPromptResponse({ runtime, sessionId: "s", clientSessionId: "client", message: "test", write: (event, data) => emitted.push({ event, data }) });
|
||||
expect(result.failed).toBeFalse();
|
||||
expect(emitted.filter((e) => e.event === "token").map((e) => e.data.content).join("")).toBe("流事件完整");
|
||||
expect(emitted.at(-1)?.event).toBe("done");
|
||||
});
|
||||
|
||||
it("does not split short Chinese text only because it has punctuation", () => {
|
||||
expect(detectTokenSmoothingChunk("SCADA 液位异常,需要继续分析", "zh")).toBeNull();
|
||||
expect(detectTokenSmoothingChunk("SCADA 液位异常需要继续分析。", "zh")).toBeNull();
|
||||
@@ -222,7 +242,7 @@ describe("streamPromptResponse", () => {
|
||||
} satisfies Partial<PermissionRequestPayload>);
|
||||
});
|
||||
|
||||
it("auto replies always when approval mode is always", async () => {
|
||||
it("auto approves ordinary business tools in always mode", async () => {
|
||||
const replies: Array<Record<string, unknown>> = [];
|
||||
const runtime = {
|
||||
subscribeEvents: async () =>
|
||||
@@ -232,7 +252,7 @@ describe("streamPromptResponse", () => {
|
||||
properties: {
|
||||
id: "perm-1",
|
||||
sessionID: "runtime-session-1",
|
||||
permission: "bash",
|
||||
permission: "web_search",
|
||||
patterns: ["npm test"],
|
||||
metadata: { command: "npm test" },
|
||||
always: ["npm test"],
|
||||
@@ -277,6 +297,51 @@ describe("streamPromptResponse", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("requires manual Shell approval even in always mode", async () => {
|
||||
const replies: Array<Record<string, unknown>> = [];
|
||||
const runtime = {
|
||||
subscribeEvents: async () =>
|
||||
createEventStream([
|
||||
{
|
||||
type: "permission.asked",
|
||||
properties: {
|
||||
id: "perm-1",
|
||||
sessionID: "runtime-session-1",
|
||||
permission: "bash",
|
||||
patterns: ["npm test"],
|
||||
metadata: { command: "npm test" },
|
||||
always: ["npm test"],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "session.idle",
|
||||
properties: {
|
||||
sessionID: "runtime-session-1",
|
||||
},
|
||||
},
|
||||
]),
|
||||
prompt: async () => undefined,
|
||||
messages: async () => [],
|
||||
replyPermission: async (options: Record<string, unknown>) => {
|
||||
replies.push(options);
|
||||
},
|
||||
} as unknown as OpencodeRuntimeAdapter;
|
||||
const events: Array<{ event: string; data: Record<string, unknown> }> = [];
|
||||
|
||||
await streamPromptResponse({
|
||||
runtime,
|
||||
sessionId: "runtime-session-1",
|
||||
clientSessionId: "client-session-1",
|
||||
message: "run tests",
|
||||
approvalMode: "always",
|
||||
write: (event, data) => events.push({ event, data }),
|
||||
});
|
||||
|
||||
expect(replies).toEqual([]);
|
||||
expect(events.some((item) => item.event === "permission_request")).toBe(true);
|
||||
expect(events.some((item) => item.event === "permission_response")).toBe(false);
|
||||
});
|
||||
|
||||
it("forwards opencode v2 permission requests as SSE payloads", async () => {
|
||||
const runtime = {
|
||||
subscribeEvents: async () =>
|
||||
|
||||
Reference in New Issue
Block a user