Q123: files shared in a private conversation stay with its members

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:52:37 -04:00
1 parent 69b83d6b77
commit 91fa6b5f1e
11 files changed
+121 -10

No files matched your search

@@ -25,6 +25,11 @@ function harness() {
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, ?, ?, 'employee', null, ?, ?)
`).run(fileId, owner, name, createHash("sha256").update(content).digest("hex"), path, new Date().toISOString(), scanState);
// As every recorded file is, it is readable across its company's workspace.
connection.db.prepare(`
insert into file_access_grants(id, org_id, file_id, granted_to_type, granted_to_id, granted_by_type, access_level, created_at)
values (?, ?, ?, 'org_group', ?, 'system', 'manage', ?)
`).run(`fgr_${randomUUID()}`, owner, fileId, `${owner}:workspace`, new Date().toISOString());
return fileId;
};
const ctx: AgentToolContext = {
+2 -2
View File
@@ -483,8 +483,8 @@ describe("domain services", () => {
goal: "Ship a verified control plane.",
});
});
const cleanFileId = fileService.importPath(orgId, cleanPath);
const blockedFileId = fileService.importPath(orgId, secretPath);
const cleanFileId = fileService.importPath(orgId, cleanPath, null);
const blockedFileId = fileService.importPath(orgId, secretPath, null);
const state = snapshot.load();
const cleanFile = state.files.find((file) => file.id === cleanFileId);
+53
View File
@@ -0,0 +1,53 @@
import { randomUUID } from "node:crypto";
import { mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test, vi } from "vitest";
vi.mock("electron", () => ({ dialog: {}, shell: {} }));
const { DatabaseConnection } = await import("../../src/main/database/connection");
const { OrganizationService } = await import("../../src/main/services/organization-service");
const { ChatService } = await import("../../src/main/services/chat-service");
const { FileService } = await import("../../src/main/services/file-service");
const { dispatchAgentTool } = await import("../../src/main/services/agent-tools");
describe("files shared in a private conversation", () => {
test("a file imported into a direct conversation is readable by its members only", () => {
const connection = new DatabaseConnection(join(tmpdir(), `ac-private-${randomUUID()}.db`));
const organization = new OrganizationService(connection.db);
const chat = new ChatService(connection.db);
let orgId = "";
let danaId = "";
let eveId = "";
connection.transaction(() => {
orgId = organization.createOrganization({ organizationName: "Acme", employeeName: "Yang", mission: "m", goal: "g" });
const providerId = (connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(orgId) as { id: string }).id;
connection.db.prepare("update providers set enabled = 1 where id = ?").run(providerId);
danaId = organization.createAgent({ orgId, name: "Dana", role: "Engineer", department: "Engineering", modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 1000, managerAgentId: null });
eveId = organization.createAgent({ orgId, name: "Eve", role: "Engineer", department: "Engineering", modelProviderId: providerId, modelId: "gpt-5.4", budgetCents: 1000, managerAgentId: null });
});
const employeeId = (connection.db.prepare("select id from employees where org_id = ?").get(orgId) as { id: string }).id;
const dm = connection.transaction(() => chat.ensureDirectSpace(orgId, { type: "employee", id: employeeId }, { type: "agent", id: danaId }));
const source = join(tmpdir(), `ac-private-src-${randomUUID()}`);
mkdirSync(source, { recursive: true });
writeFileSync(join(source, "salary.md"), "Dana's raise: 10%");
const artifacts = join(tmpdir(), `ac-private-art-${randomUUID()}`);
const fileId = new FileService(connection.db, artifacts).importPath(orgId, join(source, "salary.md"), dm);
const deps = { db: connection.db, organization, chat };
const as = (agentId: string) => ({ orgId, agent: { id: agentId, modelProviderId: null, modelId: null }, taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null });
expect(dispatchAgentTool(deps, as(danaId), { id: "c1", name: "read_attachment", arguments: { file_id: fileId } })).toContain("Dana's raise");
expect(dispatchAgentTool(deps, as(eveId), { id: "c2", name: "read_attachment", arguments: { file_id: fileId } }))
.toBe("ERROR: You have no access to salary.md; it was shared in a conversation you are not part of.");
expect(dispatchAgentTool(deps, as(eveId), { id: "c3", name: "create_task", arguments: { title: "Leak", description: "d", input_file_ids: [fileId] } }))
.toBe("ERROR: You have no access to salary.md; it was shared in a conversation you are not part of.");
// A member cannot hand the file on to someone outside the conversation either.
expect(dispatchAgentTool(deps, as(danaId), { id: "c4", name: "create_task", arguments: { title: "Pass on", description: "d", assignee_agent_name: "Eve", input_file_ids: [fileId] } }))
.toBe("ERROR: Eve has no access to salary.md; it was shared in a conversation they are not part of. Share it with them or assign someone who has it.");
expect(connection.db.prepare("select count(*) as n from tasks where title in ('Leak', 'Pass on')").get()).toEqual({ n: 0 });
expect(connection.db.prepare("select granted_to_type as type from file_access_grants where file_id = ? order by granted_to_type").all(fileId))
.toEqual([{ type: "agent" }, { type: "employee" }]);
connection.close();
});
});