Q125: imports stream through a bounded window and refuse files over 1 GiB
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7a9059fbea
commit
11cdc4af20
5 files changed
+131
-43
No files matched your search
@@ -459,7 +459,7 @@ describe("domain services", () => {
|
||||
connection.close();
|
||||
});
|
||||
|
||||
test("imports files with checksum, scan labels, object refs, and workspace grants", () => {
|
||||
test("imports files with checksum, scan labels, object refs, and workspace grants", async () => {
|
||||
const connection = createConnection();
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const snapshot = new SnapshotService(connection.db);
|
||||
@@ -483,8 +483,8 @@ describe("domain services", () => {
|
||||
goal: "Ship a verified control plane.",
|
||||
});
|
||||
});
|
||||
const cleanFileId = fileService.importPath(orgId, cleanPath, null);
|
||||
const blockedFileId = fileService.importPath(orgId, secretPath, null);
|
||||
const cleanFileId = await fileService.importPath(orgId, cleanPath, null);
|
||||
const blockedFileId = await fileService.importPath(orgId, secretPath, null);
|
||||
|
||||
const state = snapshot.load();
|
||||
const cleanFile = state.files.find((file) => file.id === cleanFileId);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { appendFileSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { scanFileContent } from "../../src/main/services/files/file-object-store";
|
||||
import { scanFileAt, scanFileContent } from "../../src/main/services/files/file-object-store";
|
||||
|
||||
const MIB = 1024 * 1024;
|
||||
|
||||
@@ -41,3 +45,30 @@ describe("the file scan", () => {
|
||||
expect(scanFileContent("plan.md", Buffer.from("# Plan")).labels).not.toContain("scan:contents-not-inspected");
|
||||
});
|
||||
});
|
||||
|
||||
describe("scanning a file on disk", () => {
|
||||
test("streams the same verdict and hash as scanning it in memory", async () => {
|
||||
const path = join(tmpdir(), `ac-scan-${randomUUID()}.bin`);
|
||||
const content = Buffer.concat([Buffer.from("PK\u0003\u0004"), Buffer.alloc(2 * MIB, "x"), Buffer.from(" AKIAABCDEFGHIJKLMNOP ")]);
|
||||
writeFileSync(path, content);
|
||||
const streamed = await scanFileAt(path, "bundle.zip");
|
||||
const inMemory = scanFileContent("bundle.zip", content);
|
||||
expect({ state: streamed.scan.state, labels: streamed.scan.labels }).toEqual({ state: inMemory.state, labels: inMemory.labels });
|
||||
expect(streamed.scan.labels).toEqual(expect.arrayContaining(["dlp:aws-access-key", "scan:contents-not-inspected"]));
|
||||
expect(streamed.sha256).toBe(createHash("sha256").update(content).digest("hex"));
|
||||
expect(streamed.size).toBe(content.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a file that changes while it is scanned", () => {
|
||||
test("is never approved without its new bytes having been scanned", async () => {
|
||||
const path = join(tmpdir(), `ac-grow-${randomUUID()}.txt`);
|
||||
writeFileSync(path, Buffer.alloc(8 * MIB, "x"));
|
||||
const scanning = scanFileAt(path, "grow.txt").then(
|
||||
(result) => (result.scan.state === "blocked" ? "blocked" : "approved"),
|
||||
(error: Error) => error.message,
|
||||
);
|
||||
appendFileSync(path, "\nAKIAABCDEFGHIJKLMNOP\n");
|
||||
expect(["blocked", "grow.txt changed size while it was being scanned."]).toContain(await scanning);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { mkdirSync, readdirSync, rmSync, truncateSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
@@ -13,7 +13,7 @@ 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", () => {
|
||||
test("a file imported into a direct conversation is readable by its members only", async () => {
|
||||
const connection = new DatabaseConnection(join(tmpdir(), `ac-private-${randomUUID()}.db`));
|
||||
const organization = new OrganizationService(connection.db);
|
||||
const chat = new ChatService(connection.db);
|
||||
@@ -33,7 +33,7 @@ describe("files shared in a private conversation", () => {
|
||||
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 fileId = await 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 });
|
||||
@@ -50,4 +50,20 @@ describe("files shared in a private conversation", () => {
|
||||
.toEqual([{ type: "agent" }, { type: "employee" }]);
|
||||
connection.close();
|
||||
});
|
||||
|
||||
test("a file over the import limit is refused before anything is copied", async () => {
|
||||
const connection = new DatabaseConnection(join(tmpdir(), `ac-import-limit-${randomUUID()}.db`));
|
||||
let orgId = "";
|
||||
connection.transaction(() => {
|
||||
orgId = new OrganizationService(connection.db).createOrganization({ organizationName: "Acme", employeeName: "Yang", mission: "m", goal: "g" });
|
||||
});
|
||||
const huge = join(tmpdir(), `ac-huge-${randomUUID()}.bin`);
|
||||
writeFileSync(huge, "");
|
||||
truncateSync(huge, 1024 * 1024 * 1024 + 1);
|
||||
const artifacts = join(tmpdir(), `ac-import-limit-art-${randomUUID()}`);
|
||||
await expect(new FileService(connection.db, artifacts).importPath(orgId, huge, null)).rejects.toThrow("files over 1024.0 MiB cannot be imported");
|
||||
expect(readdirSync(artifacts, { recursive: true })).toEqual([]);
|
||||
rmSync(huge);
|
||||
connection.close();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user