import { afterEach, expect, it } from "bun:test"; import { createHash } from "node:crypto"; import { mkdtemp, mkdir, readFile, rm, symlink } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { config } from "../../src/config.js"; import { saveMcpArtifact, openArtifactDirectory } from "../../src/mcp/artifact-files.js"; import { PRODUCT_MCP_WRITE_TOOL_IDS } from "../../src/runtime/product-mcp-tools.js"; import { serviceOpencodeConfig } from "../../src/runtime/opencode2-config.js"; import { allowedQueryTools } from "../../src/runtime/query-mcp-policy.js"; import type { RuntimeSessionContext } from "../../src/runtime/sessionContext.js"; const roots: string[] = []; const originalRoot = config.CONVERSATION_WORKSPACE_ROOT; afterEach(async () => { config.CONVERSATION_WORKSPACE_ROOT = originalRoot; await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); }); const owner: RuntimeSessionContext = { sessionId: "export-native", clientSessionId: "export-client", actorKey: "actor-a", projectKey: "project-a", projectId: "p", purpose: "ordinary_chat", traceId: "test" }; async function workspace() { const root = await mkdtemp(join(tmpdir(), "artifact-test-")); roots.push(root); config.CONVERSATION_WORKSPACE_ROOT = root; const directory = join(root, "actor-a", "project-a", `session-${createHash("sha256").update(owner.sessionId).digest("hex")}`, "workspace"); await mkdir(join(directory, "files"), { recursive: true }); return directory; } function result() { const bytes = Buffer.from("export fixture"); return { content: [{ type: "text" as const, text: "binary" }], structuredContent: { source: "export_project_model", evidence_id: "E1", data: { file: { name: "model.zip", mime_type: "application/zip", encoding: "base64", data: bytes.toString("base64"), size: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex"), } } } }; } it("writes verified exports only to the owning workspace and returns no binary or host path", async () => { const directory = await workspace(); const saved = await saveMcpArtifact(result(), owner, directory); const data = saved.structuredContent as { data: { file: { relative_path: string; download_url: string } } }; expect(await readFile(join(directory, data.data.file.relative_path), "utf8")).toBe("export fixture"); expect(data.data.file.download_url).toStartWith("/api/v1/session/export-client/files/mcp-"); expect(JSON.stringify(saved)).not.toContain(directory); expect(JSON.stringify(saved)).not.toContain(Buffer.from("export fixture").toString("base64")); expect(saved.structuredContent).toHaveProperty("evidence_id", "E1"); }); it("rejects tampered file contents, cross-session workspaces and symlink directories", async () => { const directory = await workspace(); const tampered = result(); tampered.structuredContent.data.file.sha256 = "0".repeat(64); await expect(saveMcpArtifact(tampered, owner, directory)).rejects.toThrow("integrity"); await expect(openArtifactDirectory(directory, "other-session")).rejects.toThrow("owner"); await rm(join(directory, "files"), { recursive: true }); await symlink(tmpdir(), join(directory, "files")); await expect(saveMcpArtifact(result(), owner, directory)).rejects.toThrow(); }); it("keeps all product writes behind native ask permissions and out of task, reader and review policies", () => { const configuration = serviceOpencodeConfig({}); for (const tool of PRODUCT_MCP_WRITE_TOOL_IDS) { expect(configuration.permissions.filter((rule) => rule.action === tool).at(-1)?.effect).toBe("ask"); expect(allowedQueryTools(owner)).toContain(tool); expect(allowedQueryTools({ ...owner, purpose: "task_execution" })).not.toContain(tool); expect(allowedQueryTools({ ...owner, purpose: "task_collaboration" })).not.toContain(tool); expect(allowedQueryTools(owner, true)).not.toContain(tool); expect(allowedQueryTools({ ...owner, learningMode: "review" })).not.toContain(tool); } });