Q122: reading a recorded file checks its bytes against the recorded hash

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 19:44:51 -04:00
1 parent 8f192eabe7
commit 69b83d6b77
11 files changed
+95 -26

No files matched your search

+3 -3
View File
@@ -1,4 +1,4 @@
import { randomUUID } from "node:crypto";
import { createHash, randomUUID } from "node:crypto";
import { writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
@@ -23,8 +23,8 @@ function harness() {
const fileId = `fil_${randomUUID()}`;
connection.db.prepare(`
insert into file_objects(id, org_id, name, kind, size_bytes, sha256, storage_path, owner_type, owner_id, created_at, scan_state)
values (?, ?, ?, 'md', 1, 'x', ?, 'employee', null, ?, ?)
`).run(fileId, owner, name, path, new Date().toISOString(), scanState);
values (?, ?, ?, 'md', 1, ?, ?, 'employee', null, ?, ?)
`).run(fileId, owner, name, createHash("sha256").update(content).digest("hex"), path, new Date().toISOString(), scanState);
return fileId;
};
const ctx: AgentToolContext = {
+9
View File
@@ -86,6 +86,15 @@ describe("structured task handoff", () => {
h.connection.close();
});
test("an input whose bytes no longer match its record is refused, not pinned", () => {
const h = harness();
writeFileSync(h.specPath, "tampered");
expect(dispatchAgentTool(h.deps, h.ctx, call("create_task", { title: "Y", description: "d", input_file_ids: ["fil_spec"] })))
.toBe("ERROR: spec.md changed on disk since it was recorded; it is not the content that was handed over.");
expect(h.connection.db.prepare("select count(*) as n from tasks where title = 'Y'").get()).toEqual({ n: 0 });
h.connection.close();
});
test("an unknown input file id fails the call and creates nothing", () => {
const h = harness();
expect(dispatchAgentTool(h.deps, h.ctx, call("create_task", { title: "X", description: "d", input_file_ids: ["fil_missing"] })))
+11 -2
View File
@@ -1,4 +1,5 @@
import { randomUUID } from "node:crypto";
import { writeFileSync } from "node:fs";
import { createHash, randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
@@ -11,6 +12,14 @@ import { providerAdapters } from "../../src/main/services/provider-adapters";
import { SnapshotService } from "../../src/main/services/snapshot-service";
import { RISK_LEVEL } from "../../src/shared/domain";
/** A real file and its hash, as a recorded file needs: bytes are checked against the hash when read. */
function realFile(content: string): [string, string] {
const path = join(tmpdir(), `ac-file-${randomUUID()}.md`);
writeFileSync(path, content);
return [createHash("sha256").update(content).digest("hex"), path];
}
function createConnection(): DatabaseConnection {
return new DatabaseConnection(join(tmpdir(), `agentcompany-new-chat-${randomUUID()}.db`));
}
@@ -273,7 +282,7 @@ describe("chat governance", () => {
connection.db.prepare(`
insert into file_objects(id, org_id, name, kind, size_bytes, sha256, storage_path, owner_type, owner_id, created_at)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(fileId, orgId, "release-notes.md", "md", 24, "8f3a", "artifacts/release-notes.md", "employee", employeeId, now);
`).run(fileId, orgId, "release-notes.md", "md", 24, ...realFile("Release notes."), "employee", employeeId, now);
agentMessageId = chat.sendMessage({
orgId,
spaceId,
+17 -6
View File
@@ -15,6 +15,17 @@ import { OnboardingService } from "../../src/main/services/onboarding-service";
import { RISK_LEVEL, TASK_STATUS } from "../../src/shared/domain";
import { createId } from "../../src/shared/ids";
/** A real file and its hash, as a recorded file needs: bytes are checked against the hash when read. */
const runbook = realFile("Runbook steps.");
const releaseNotes = realFile("Release notes.");
function realFile(content: string): [string, string] {
const path = join(tmpdir(), `ac-file-${randomUUID()}.md`);
writeFileSync(path, content);
return [createHash("sha256").update(content).digest("hex"), path];
}
function createConnection(): DatabaseConnection {
return new DatabaseConnection(join(tmpdir(), `agentcompany-new-${randomUUID()}.db`));
}
@@ -287,7 +298,7 @@ describe("domain services", () => {
connection.db.prepare(`
insert into file_objects(id, org_id, name, kind, size_bytes, sha256, storage_path, owner_type, owner_id, created_at)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(fileId, orgId, "runbook.md", "text/markdown", 16, "8f3a", "artifacts/runbook.md", "employee", employee.id, new Date().toISOString());
`).run(fileId, orgId, "runbook.md", "text/markdown", 16, ...runbook, "employee", employee.id, new Date().toISOString());
chat.sendMessage({
orgId,
@@ -311,10 +322,10 @@ describe("domain services", () => {
fileId,
targetPrincipalType: "board",
targetPrincipalId: orgId,
checksum: "8f3a",
checksum: runbook[0],
scanState: "approved",
policyDecision: "approved_for_handoff",
objectRef: "artifacts/runbook.md",
objectRef: runbook[1],
});
expect(state.chatMentions[0]).toMatchObject({ targetType: "board", targetId: orgId });
expect(state.chatSlashInvocations[0]).toMatchObject({ command: "handoff" });
@@ -376,7 +387,7 @@ describe("domain services", () => {
owner_type, owner_id, space_id, pinned, scan_state, scan_labels_json, scanned_at, created_at, updated_at
)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(approvedFileId, orgId, "release-notes.md", "md", 24, "sha-approved", "artifacts/release-notes.md", "agent", adaId, null, 0, "approved", "[]", now, now, now);
`).run(approvedFileId, orgId, "release-notes.md", "md", 24, ...releaseNotes, "agent", adaId, null, 0, "approved", "[]", now, now, now);
connection.db.prepare(`
insert into file_objects(
id, org_id, name, kind, size_bytes, sha256, storage_path,
@@ -438,8 +449,8 @@ describe("domain services", () => {
sourcePrincipalType: "agent",
sourcePrincipalId: adaId,
targetPrincipalType: "agent",
checksum: "sha-approved",
objectRef: "artifacts/release-notes.md",
checksum: releaseNotes[0],
objectRef: releaseNotes[1],
scanState: "approved",
policyDecision: "approved_for_handoff",
});
@@ -6,6 +6,8 @@ import { describe, expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
import { OrganizationService } from "../../src/main/services/organization-service";
import { preserveRecordedFiles, recordWorkspaceFile } from "../../src/main/services/files/file-object-store";
import { ChatService } from "../../src/main/services/chat-service";
import { readAttachmentTool } from "../../src/main/services/agent-attachment-tools";
describe("recorded workspace files", () => {
test("a file id keeps the bytes it was recorded with after the workspace file is rewritten", () => {
@@ -58,3 +60,24 @@ describe("recorded workspace files", () => {
connection.close();
});
});
describe("opening a recorded file", () => {
test("bytes that no longer match the recorded hash are refused, not shown as the recorded file", () => {
const connection = new DatabaseConnection(join(tmpdir(), `ac-verify-${randomUUID()}.db`));
const root = join(tmpdir(), `ac-verify-ws-${randomUUID()}`);
mkdirSync(root, { recursive: true });
let orgId = "";
connection.transaction(() => {
orgId = new OrganizationService(connection.db).createOrganization({ organizationName: "Acme", employeeName: "Dana", mission: "m", goal: "g" });
});
writeFileSync(join(root, "plan.md"), "recorded");
const fileId = recordWorkspaceFile(connection.db, { orgId, ownerType: "agent", ownerId: null, workspaceRoot: root, relativePath: "plan.md" });
const deps = { db: connection.db, organization: new OrganizationService(connection.db), chat: new ChatService(connection.db) };
const ctx = { orgId, agent: { id: "agt_x", modelProviderId: null, modelId: null }, taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null };
expect(readAttachmentTool(deps, ctx, { file_id: fileId })).toContain("recorded");
const stored = (connection.db.prepare("select storage_path from file_objects where id = ?").get(fileId) as { storage_path: string }).storage_path;
writeFileSync(stored, "tampered");
expect(() => readAttachmentTool(deps, ctx, { file_id: fileId })).toThrow("plan.md changed on disk since it was recorded; it is not the content that was handed over.");
connection.close();
});
});