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.
56 lines
4.0 KiB
TypeScript
56 lines
4.0 KiB
TypeScript
import { afterEach, describe, expect, it } from "bun:test";
|
|
import express from "express";
|
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { type Server } from "node:http";
|
|
import { registerLearningToolBridge } from "../../src/routes/learningToolBridge.js";
|
|
import { MemoryStore } from "../../src/memory/store.js";
|
|
import { SkillStore } from "../../src/skills/store.js";
|
|
import { getRuntimeSessionContext, removeRuntimeSessionContext, setRuntimeSessionContext } from "../../src/runtime/sessionContext.js";
|
|
|
|
let server: Server | undefined;
|
|
let directory: string | undefined;
|
|
afterEach(async () => {
|
|
if (server) await new Promise<void>((resolve) => server!.close(() => resolve()));
|
|
if (directory) await rm(directory, { recursive: true, force: true });
|
|
removeRuntimeSessionContext("learning-bridge-session");
|
|
});
|
|
describe("learning tools across process boundaries", () => {
|
|
it("uses the parent session scope for list-before-write and rejects forged scope or missing context", async () => {
|
|
directory = await mkdtemp(join(tmpdir(), "learning-bridge-"));
|
|
const memoryStore = new MemoryStore(join(directory, "memory"), join(directory, "backup"));
|
|
await memoryStore.initialize();
|
|
setRuntimeSessionContext({ sessionId: "learning-bridge-session", clientSessionId: "client", actorKey: "owner", projectKey: "project", traceId: "test", allowLearningWrite: true });
|
|
const app = express(); app.use(express.json());
|
|
registerLearningToolBridge(app, { internalToken: "TEST_ONLY", memoryStore, skillStore: new SkillStore(join(directory, "skills"), join(directory, "skills-backup")) });
|
|
server = app.listen(0, "127.0.0.1");
|
|
await new Promise<void>((resolve) => server!.once("listening", resolve));
|
|
const address = server.address() as { port: number };
|
|
const baseUrl = `http://127.0.0.1:${address.port}`;
|
|
const child = Bun.spawn([process.execPath, "-e", [
|
|
'import memory from "./.opencode/tools/memory_manager.ts";',
|
|
'import skill from "./.opencode/tools/skill_manager.ts";',
|
|
'const ctx = { sessionID: "learning-bridge-session" };',
|
|
'const call = (action, content) => memory.execute({ action, scope: "user", reason: "test", content }, ctx).then(JSON.parse);',
|
|
'const before = await call("add", "用户偏好简洁答复");',
|
|
'await call("list"); const added = await call("add", "用户偏好简洁答复");',
|
|
'const skills = JSON.parse(await skill.execute({ action: "list", skill_path: "__root__", reason: "test" }, ctx));',
|
|
'process.stdout.write(JSON.stringify({ before, added, skills }));',
|
|
].join("\n")], { cwd: process.cwd(), env: { ...process.env, AGENT_INTERNAL_TOKEN: "TEST_ONLY", TJWATER_AGENT_INTERNAL_BASE_URL: baseUrl }, stdout: "pipe", stderr: "pipe" });
|
|
const [exitCode, stdout, stderr] = await Promise.all([child.exited, new Response(child.stdout).text(), new Response(child.stderr).text()]);
|
|
expect({ exitCode, stderr }).toEqual({ exitCode: 0, stderr: "" });
|
|
const result = JSON.parse(stdout);
|
|
expect(result.before.decision).toBe("rejected");
|
|
expect(result.added.decision).toBe("accepted");
|
|
expect(result.skills.kind).toBe("skill");
|
|
expect(getRuntimeSessionContext("learning-bridge-session")?.memoryListReadScopes?.user).toBeTrue();
|
|
expect(await memoryStore.list("user", "owner")).toHaveLength(1);
|
|
expect(await memoryStore.list("user", "other-owner")).toHaveLength(0);
|
|
const post = (sessionId: string, input: unknown, token = "TEST_ONLY") => fetch(`${baseUrl}/internal/tools/memory-manager`, { method: "POST", headers: { "Content-Type": "application/json", "x-agent-internal-token": token }, body: JSON.stringify({ session_id: sessionId, input }) });
|
|
expect((await post("missing", { action: "list", scope: "user" })).status).toBe(404);
|
|
expect((await post("learning-bridge-session", { action: "list", scope: "user", actorKey: "other-owner" })).status).toBe(400);
|
|
expect((await post("learning-bridge-session", { action: "list", scope: "user" }, "wrong")).status).toBe(403);
|
|
});
|
|
});
|