feat: add scoped history and artifacts
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user