68 lines
4.0 KiB
TypeScript
68 lines
4.0 KiB
TypeScript
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);
|
|
}
|
|
});
|