diff --git a/AgentCompany-New/src/main/services/file-service.ts b/AgentCompany-New/src/main/services/file-service.ts
index b8c6edc7..138a764f 100644
--- a/AgentCompany-New/src/main/services/file-service.ts
+++ b/AgentCompany-New/src/main/services/file-service.ts
@@ -4,16 +4,32 @@ import { basename, extname, join } from "node:path";
import type { DatabaseSync } from "node:sqlite";
import { dialog } from "electron";
import type { ImportFileInput } from "../../shared/commands";
-import { FILE_SCAN_STATE } from "../../shared/domain";
+import { FILE_SCAN_STATE, type FileScanState } from "../../shared/domain";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
interface FileScanResult {
- state: typeof FILE_SCAN_STATE.APPROVED;
+ state: FileScanState;
labels: string[];
scannedAt: string;
}
+interface TextScanPattern {
+ label: string;
+ pattern: RegExp;
+ block: boolean;
+}
+
+const TEXT_SCAN_PATTERNS: TextScanPattern[] = [
+ { label: "dlp:private-key", pattern: /-----BEGIN (?:RSA |DSA |EC |OPENSSH |PRIVATE )?PRIVATE KEY-----/i, block: true },
+ { label: "dlp:aws-access-key", pattern: /\bAKIA[0-9A-Z]{16}\b/, block: true },
+ { label: "dlp:github-token", pattern: /\bgh[pousr]_[A-Za-z0-9_]{30,}\b/, block: true },
+ { label: "dlp:provider-secret", pattern: /\bsk-[A-Za-z0-9]{32,}\b/, block: true },
+ { label: "dlp:credential", pattern: /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*["']?[A-Za-z0-9_\-./+=]{20,}/i, block: true },
+ { label: "dlp:ssn", pattern: /\b\d{3}-\d{2}-\d{4}\b/, block: true },
+ { label: "dlp:email", pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i, block: false },
+];
+
function fileKind(path: string): string {
const ext = extname(path).toLowerCase().replace(".", "");
if (!ext) return "file";
@@ -91,7 +107,7 @@ export class FileService {
actorType: "system",
actorId: null,
action: "imported",
- payload: { sha256: hash, sizeBytes: stats.size, scanState: scan.state },
+ payload: { sha256: hash, sizeBytes: stats.size, scanState: scan.state, scanLabels: scan.labels, objectRef: target },
});
this.db.exec("commit");
} catch (error) {
@@ -141,13 +157,26 @@ export class FileService {
}
function scanFile(sourcePath: string, bytes: Buffer): FileScanResult {
- const sample = bytes.subarray(0, Math.min(bytes.length, 1024 * 1024)).toString("latin1");
- if (sample.includes("EICAR-STANDARD-ANTIVIRUS-TEST-FILE")) {
- throw new Error("File scan blocked a known malware signature.");
+ const textSample = sampleText(bytes);
+ const labels = new Set(scanLabels(sourcePath, bytes.length));
+ let blocked = false;
+ if (bytes.subarray(0, Math.min(bytes.length, 1024 * 1024)).toString("latin1").includes("EICAR-STANDARD-ANTIVIRUS-TEST-FILE")) {
+ labels.add("av:eicar");
+ blocked = true;
}
+ for (const scan of TEXT_SCAN_PATTERNS) {
+ if (!scan.pattern.test(textSample)) continue;
+ labels.add(scan.label);
+ if (scan.block) blocked = true;
+ }
+ if (containsPaymentCard(textSample)) {
+ labels.add("dlp:payment-card");
+ blocked = true;
+ }
+ labels.add(blocked ? "classification:restricted" : labels.has("dlp:email") ? "classification:confidential" : "classification:internal");
return {
- state: FILE_SCAN_STATE.APPROVED,
- labels: scanLabels(sourcePath, bytes.length),
+ state: blocked ? FILE_SCAN_STATE.BLOCKED : FILE_SCAN_STATE.APPROVED,
+ labels: [...labels].sort(),
scannedAt: new Date().toISOString(),
};
}
@@ -158,3 +187,28 @@ function scanLabels(sourcePath: string, sizeBytes: number): string[] {
if (sizeBytes > 100 * 1024 * 1024) labels.add("large");
return [...labels].sort();
}
+
+function sampleText(bytes: Buffer): string {
+ return bytes.subarray(0, Math.min(bytes.length, 1024 * 1024)).toString("utf8");
+}
+
+function containsPaymentCard(text: string): boolean {
+ const matches = text.match(/\b(?:\d[ -]?){13,19}\b/g) ?? [];
+ return matches.some((match) => luhnValid(match.replace(/[ -]/g, "")));
+}
+
+function luhnValid(value: string): boolean {
+ if (!/^\d{13,19}$/.test(value)) return false;
+ let sum = 0;
+ let doubleDigit = false;
+ for (let index = value.length - 1; index >= 0; index -= 1) {
+ let digit = Number(value[index]);
+ if (doubleDigit) {
+ digit *= 2;
+ if (digit > 9) digit -= 9;
+ }
+ sum += digit;
+ doubleDigit = !doubleDigit;
+ }
+ return sum % 10 === 0;
+}
diff --git a/AgentCompany-New/src/renderer/src/pages/FilesPage.tsx b/AgentCompany-New/src/renderer/src/pages/FilesPage.tsx
index d6d7123a..baa35da8 100644
--- a/AgentCompany-New/src/renderer/src/pages/FilesPage.tsx
+++ b/AgentCompany-New/src/renderer/src/pages/FilesPage.tsx
@@ -305,7 +305,12 @@ function FileRow({
>
-
{file.name}
+
+ {file.name}
+
+ sha:{shortChecksum(file.sha256)} · obj:{shortObjectRef(file.storagePath)}
+
+
{file.pinned &&
Pinned}
{formatBytes(file.sizeBytes)}
@@ -340,7 +345,7 @@ function PinnedCard({
{file.name}
+
+ sha:{shortChecksum(file.sha256)} · obj:{shortObjectRef(file.storagePath)}
+
{file.scanState === "approved" ? "Clean" : "Blocked"}
{grantCount}
+
@@ -377,6 +386,18 @@ function PinnedCard({
);
}
+function ScanLabelStrip({ file }: { file: FileObjectRecord }) {
+ const labels = parseScanLabels(file).slice(0, 2);
+ if (labels.length === 0) return null;
+ return (
+
+ {labels.map((label) => (
+
{label}
+ ))}
+
+ );
+}
+
function FileIcon({ kind, big = false }: { kind: string; big?: boolean }) {
const map = FILE_COLOR_MAP[kind] ?? FILE_COLOR_MAP.doc;
if (!map) throw new Error("File color map default missing.");
@@ -443,6 +464,25 @@ function fileGrantCount(fileId: string, grants: FileAccessGrantRecord[]): number
return grants.filter((grant) => grant.fileId === fileId).length;
}
+function parseScanLabels(file: FileObjectRecord): string[] {
+ const parsed: unknown = JSON.parse(file.scanLabelsJson);
+ if (!Array.isArray(parsed) || parsed.some((label) => typeof label !== "string")) {
+ throw new Error("File scan labels are invalid.");
+ }
+ return parsed;
+}
+
+function shortChecksum(value: string): string {
+ if (!/^[a-f0-9]{8,}$/i.test(value)) throw new Error("File checksum is invalid.");
+ return value.slice(0, 8);
+}
+
+function shortObjectRef(value: string): string {
+ const parts = value.split(/[\\/]/).filter(Boolean);
+ if (parts.length === 0) throw new Error("File object reference is invalid.");
+ return parts.slice(-2).join("/");
+}
+
function isRecent(iso: string): boolean {
const sevenDays = 7 * 24 * 60 * 60 * 1000;
return Date.now() - new Date(iso).getTime() <= sevenDays;
diff --git a/AgentCompany-New/tests/unit/domain-services.test.ts b/AgentCompany-New/tests/unit/domain-services.test.ts
index c7fc6625..8dc9c098 100644
--- a/AgentCompany-New/tests/unit/domain-services.test.ts
+++ b/AgentCompany-New/tests/unit/domain-services.test.ts
@@ -1,10 +1,11 @@
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
-import { randomUUID } from "node:crypto";
+import { createHash, randomUUID } from "node:crypto";
import { DatabaseSync } from "node:sqlite";
import { describe, expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
+import { FileService } from "../../src/main/services/file-service";
import { OrganizationService } from "../../src/main/services/organization-service";
import { SnapshotService } from "../../src/main/services/snapshot-service";
import { SkillService } from "../../src/main/services/skill-service";
@@ -396,6 +397,65 @@ describe("domain services", () => {
connection.close();
});
+ test("imports files with checksum, scan labels, object refs, and workspace grants", () => {
+ const connection = createConnection();
+ const organization = new OrganizationService(connection.db);
+ const snapshot = new SnapshotService(connection.db);
+ const artifactRoot = join(tmpdir(), `agentcompany-file-artifacts-${randomUUID()}`);
+ const fileService = new FileService(connection.db, artifactRoot);
+ const sourceDir = join(tmpdir(), `agentcompany-file-sources-${randomUUID()}`);
+ mkdirSync(sourceDir, { recursive: true });
+ const cleanBody = "Release checklist for dana@example.com\n";
+ const secretBody = "-----BEGIN OPENSSH PRIVATE KEY-----\nabc123\n-----END OPENSSH PRIVATE KEY-----\n";
+ const cleanPath = join(sourceDir, "release-notes.md");
+ const secretPath = join(sourceDir, "deploy.env");
+ writeFileSync(cleanPath, cleanBody);
+ writeFileSync(secretPath, secretBody);
+ let orgId = "";
+
+ connection.transaction(() => {
+ orgId = organization.createOrganization({
+ organizationName: "Acme Operations",
+ employeeName: "Dana Lee",
+ mission: "Coordinate agents with auditable approvals.",
+ goal: "Ship a verified control plane.",
+ });
+ });
+ const cleanFileId = fileService.importPath(orgId, cleanPath);
+ const blockedFileId = fileService.importPath(orgId, secretPath);
+
+ const state = snapshot.load();
+ const cleanFile = state.files.find((file) => file.id === cleanFileId);
+ const blockedFile = state.files.find((file) => file.id === blockedFileId);
+ if (!cleanFile || !blockedFile) throw new Error("Imported files were not recorded.");
+ expect(cleanFile.sha256).toBe(createHash("sha256").update(cleanBody).digest("hex"));
+ expect(cleanFile.storagePath).toContain(cleanFileId);
+ expect(cleanFile.scanState).toBe("approved");
+ expect(JSON.parse(cleanFile.scanLabelsJson)).toEqual(expect.arrayContaining(["classification:confidential", "dlp:email", "kind:md"]));
+ expect(blockedFile.scanState).toBe("blocked");
+ expect(JSON.parse(blockedFile.scanLabelsJson)).toEqual(expect.arrayContaining(["classification:restricted", "dlp:private-key", "kind:env"]));
+ expect(state.fileAccessGrants).toContainEqual(expect.objectContaining({
+ fileId: cleanFileId,
+ grantedToType: "org_group",
+ grantedToId: `${orgId}:workspace`,
+ accessLevel: "manage",
+ }));
+ expect(state.fileAccessGrants).toContainEqual(expect.objectContaining({
+ fileId: blockedFileId,
+ grantedToType: "org_group",
+ grantedToId: `${orgId}:workspace`,
+ accessLevel: "manage",
+ }));
+ const importAudit = state.auditEvents.find((event) => event.entityId === blockedFileId && event.action === "imported");
+ if (!importAudit) throw new Error("File import audit event was not recorded.");
+ expect(importAudit.payloadJson).toContain("\"scanState\":\"blocked\"");
+ expect(importAudit.payloadJson).toContain("dlp:private-key");
+ expect(importAudit.payloadJson).toContain("objectRef");
+ rmSync(sourceDir, { recursive: true, force: true });
+ rmSync(artifactRoot, { recursive: true, force: true });
+ connection.close();
+ });
+
test("creates approval and waiting presence from chat escalation", () => {
const connection = createConnection();
const organization = new OrganizationService(connection.db);