feat: add scoped history and artifacts
Generic Container CI/CD / test-build-publish (push) Failing after 8m25s
Agent CI/CD / build-test-publish-and-deploy (push) Failing after 8m25s

This commit is contained in:
2026-10-22 18:40:20 +08:00
parent bc4e9834e5
commit 7d47a2df86
48 changed files with 2632 additions and 78 deletions
+67
View File
@@ -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);
}
});