Register agent deliverables as file objects so they appear on the Files page

This commit is contained in:
ZacharyZhang-NY committed 2026-05-29 23:31:53 -04:00
1 parent e7fa60d6b6
commit 54ad9d1909
4 files changed
+264 -139

No files matched your search

@@ -2,6 +2,7 @@ import type { DatabaseSync } from "node:sqlite";
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS, type RiskLevel } from "../../shared/domain";
import type { OrganizationService } from "./organization-service";
import { writeDeliverable } from "./deliverable-writer";
import { recordWorkspaceFile } from "./files/file-object-store";
import type { JsonRecord } from "./provider-http";
import type { ToolCall, ToolDefinition } from "./provider-tool-turn";
@@ -93,7 +94,7 @@ export function dispatchAgentTool(deps: AgentToolDeps, ctx: AgentToolContext, ca
case "update_task_status":
return updateTaskStatus(deps, ctx, call.arguments);
case "write_deliverable":
return writeDeliverableTool(ctx, call.arguments);
return writeDeliverableTool(deps, ctx, call.arguments);
default:
return `ERROR: unknown tool "${call.name}".`;
}
@@ -154,11 +155,18 @@ function updateTaskStatus(deps: AgentToolDeps, ctx: AgentToolContext, args: Json
return `Task ${taskId} moved to ${status}.`;
}
function writeDeliverableTool(ctx: AgentToolContext, args: JsonRecord): string {
function writeDeliverableTool(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord): string {
if (!ctx.workspace) throw new Error("No workspace is bound to this task; cannot write deliverables.");
const path = requireString(args.path, "path");
const content = typeof args.content === "string" ? args.content : "";
const written = writeDeliverable({ workspaceRoot: ctx.workspace.localPath, relativePath: path, content });
recordWorkspaceFile(deps.db, {
orgId: ctx.orgId,
ownerType: "agent",
ownerId: ctx.agent.id,
workspaceRoot: ctx.workspace.localPath,
relativePath: written,
});
return `Wrote deliverable ${written} (${content.length} chars).`;
}
@@ -1,45 +1,13 @@
import { createHash } from "node:crypto";
import { copyFileSync, mkdirSync, readFileSync, statSync } from "node:fs";
import { basename, extname, join } from "node:path";
import { basename, join } from "node:path";
import type { DatabaseSync } from "node:sqlite";
import { dialog, shell } from "electron";
import type { ImportFileInput, OpenFileInput } from "../../shared/commands";
import { FILE_SCAN_STATE, type FileScanState } from "../../shared/domain";
import { FILE_SCAN_STATE } from "../../shared/domain";
import { createId } from "../../shared/ids";
import { AuditService } from "./audit-service";
interface FileScanResult {
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";
if (["md", "markdown"].includes(ext)) return "md";
if (["pdf"].includes(ext)) return "pdf";
if (["png", "jpg", "jpeg", "gif", "webp", "svg"].includes(ext)) return "img";
if (["csv", "xlsx", "xls"].includes(ext)) return "sheet";
if (["doc", "docx", "txt"].includes(ext)) return "doc";
return ext;
}
import { fileKind, scanFileContent, upsertFileObject } from "./files/file-object-store";
export class FileService {
private readonly audit: AuditService;
@@ -66,40 +34,29 @@ export class FileService {
const now = new Date().toISOString();
const stats = statSync(sourcePath);
if (!stats.isFile()) throw new Error("Selected path is not a file.");
const name = basename(sourcePath);
const bytes = readFileSync(sourcePath);
const hash = createHash("sha256").update(bytes).digest("hex");
const scan = scanFile(sourcePath, bytes);
const scan = scanFileContent(name, bytes);
const orgDir = join(this.artifactRoot, orgId);
mkdirSync(orgDir, { recursive: true });
const target = join(orgDir, `${id}-${basename(sourcePath)}`);
const target = join(orgDir, `${id}-${name}`);
copyFileSync(sourcePath, target);
this.db.exec("begin immediate");
try {
this.db.prepare(`
insert into file_objects(
id, org_id, name, kind, size_bytes, sha256, storage_path,
owner_type, owner_id, space_id, pinned, scan_state, scan_labels_json, scanned_at, created_at, updated_at
)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
upsertFileObject(this.db, {
id,
orgId,
basename(sourcePath),
fileKind(sourcePath),
stats.size,
hash,
target,
"system",
null,
null,
0,
scan.state,
JSON.stringify(scan.labels),
scan.scannedAt,
name,
kind: fileKind(name),
sizeBytes: stats.size,
sha256: hash,
storagePath: target,
ownerType: "system",
ownerId: null,
scan,
now,
now,
);
this.grantWorkspaceAccess(orgId, id, now);
});
this.audit.record({
orgId,
entityType: "file_object",
@@ -143,82 +100,4 @@ export class FileService {
});
}
private grantWorkspaceAccess(orgId: string, fileId: string, now: string): void {
this.db.prepare(`
insert into file_access_grants(
id, org_id, file_id, granted_to_type, granted_to_id, granted_by_type, granted_by_id,
source_message_id, source_thread_id, access_level, created_at
)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
createId("fgr"),
orgId,
fileId,
"org_group",
`${orgId}:workspace`,
"system",
null,
null,
null,
"manage",
now,
);
}
}
function scanFile(sourcePath: string, bytes: Buffer): FileScanResult {
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: blocked ? FILE_SCAN_STATE.BLOCKED : FILE_SCAN_STATE.APPROVED,
labels: [...labels].sort(),
scannedAt: new Date().toISOString(),
};
}
function scanLabels(sourcePath: string, sizeBytes: number): string[] {
const labels = new Set<string>([`kind:${fileKind(sourcePath)}`]);
if (sizeBytes === 0) labels.add("empty");
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;
}
@@ -0,0 +1,231 @@
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { basename, extname, resolve } from "node:path";
import type { DatabaseSync } from "node:sqlite";
import { FILE_SCAN_STATE, type FileScanState } from "../../../shared/domain";
import { createId } from "../../../shared/ids";
export interface FileScanResult {
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 },
];
export function fileKind(path: string): string {
const ext = extname(path).toLowerCase().replace(".", "");
if (!ext) return "file";
if (["md", "markdown"].includes(ext)) return "md";
if (["pdf"].includes(ext)) return "pdf";
if (["png", "jpg", "jpeg", "gif", "webp", "svg"].includes(ext)) return "img";
if (["csv", "xlsx", "xls"].includes(ext)) return "sheet";
if (["doc", "docx", "txt"].includes(ext)) return "doc";
return ext;
}
export function scanFileContent(name: string, bytes: Buffer): FileScanResult {
const textSample = sampleText(bytes);
const labels = new Set(scanLabels(name, 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: blocked ? FILE_SCAN_STATE.BLOCKED : FILE_SCAN_STATE.APPROVED,
labels: [...labels].sort(),
scannedAt: new Date().toISOString(),
};
}
export interface FileObjectInput {
/** Optional explicit id; defaults to a generated `fil` id on insert. */
id?: string;
orgId: string;
name: string;
kind: string;
sizeBytes: number;
sha256: string;
storagePath: string;
ownerType: string;
ownerId: string | null;
scan: FileScanResult;
now: string;
}
/**
* Inserts (or refreshes, keyed on org_id + storage_path) a file object and, on
* first insert, grants the workspace org-group read/manage access so the file
* surfaces on the Files page and can be attached in chat. Re-writing the same
* path updates the existing row instead of creating a duplicate.
*/
export function upsertFileObject(db: DatabaseSync, input: FileObjectInput): string {
const existing = db.prepare("select id from file_objects where org_id = ? and storage_path = ?")
.get(input.orgId, input.storagePath) as { id: string } | undefined;
const labelsJson = JSON.stringify(input.scan.labels);
if (existing) {
db.prepare(`
update file_objects
set name = ?, kind = ?, size_bytes = ?, sha256 = ?, owner_type = ?, owner_id = ?,
scan_state = ?, scan_labels_json = ?, scanned_at = ?, updated_at = ?
where id = ?
`).run(
input.name,
input.kind,
input.sizeBytes,
input.sha256,
input.ownerType,
input.ownerId,
input.scan.state,
labelsJson,
input.scan.scannedAt,
input.now,
existing.id,
);
return existing.id;
}
const id = input.id ?? createId("fil");
db.prepare(`
insert into file_objects(
id, org_id, name, kind, size_bytes, sha256, storage_path,
owner_type, owner_id, space_id, pinned, scan_state, scan_labels_json, scanned_at, created_at, updated_at
)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id,
input.orgId,
input.name,
input.kind,
input.sizeBytes,
input.sha256,
input.storagePath,
input.ownerType,
input.ownerId,
null,
0,
input.scan.state,
labelsJson,
input.scan.scannedAt,
input.now,
input.now,
);
grantWorkspaceAccess(db, input.orgId, id, input.now);
return id;
}
/**
* Records a file an agent wrote into the bound workspace as a first-class file
* object so it appears under that agent's department on the Files page.
*/
export function recordWorkspaceFile(db: DatabaseSync, input: {
orgId: string;
ownerType: string;
ownerId: string | null;
workspaceRoot: string;
relativePath: string;
}): string {
const absolute = resolve(input.workspaceRoot, input.relativePath);
const bytes = readFileSync(absolute);
const name = basename(input.relativePath);
const scan = scanFileContent(name, bytes);
const now = new Date().toISOString();
db.exec("begin immediate");
try {
const id = upsertFileObject(db, {
orgId: input.orgId,
name,
kind: fileKind(name),
sizeBytes: bytes.byteLength,
sha256: createHash("sha256").update(bytes).digest("hex"),
storagePath: absolute,
ownerType: input.ownerType,
ownerId: input.ownerId,
scan,
now,
});
db.exec("commit");
return id;
} catch (error) {
db.exec("rollback");
throw error;
}
}
function grantWorkspaceAccess(db: DatabaseSync, orgId: string, fileId: string, now: string): void {
db.prepare(`
insert into file_access_grants(
id, org_id, file_id, granted_to_type, granted_to_id, granted_by_type, granted_by_id,
source_message_id, source_thread_id, access_level, created_at
)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
createId("fgr"),
orgId,
fileId,
"org_group",
`${orgId}:workspace`,
"system",
null,
null,
null,
"manage",
now,
);
}
function scanLabels(name: string, sizeBytes: number): string[] {
const labels = new Set<string>([`kind:${fileKind(name)}`]);
if (sizeBytes === 0) labels.add("empty");
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;
}
@@ -94,6 +94,13 @@ describe("autonomous bootstrap run", () => {
expect(existsSync(deliverable)).toBe(true);
expect(readFileSync(deliverable, "utf8")).toContain("OrbitOps Operating Plan");
// The deliverable is registered as a file object owned by the writing agent,
// so it appears on the Files page (previously it only existed on disk).
const ceo = state.agents.find((a) => a.orgId === orgId && a.name === "CEO");
const planFile = state.files.find((f) => f.orgId === orgId && f.name === "operating-plan.md");
expect(planFile).toBeTruthy();
expect(planFile).toMatchObject({ ownerType: "agent", ownerId: ceo?.id, kind: "md", scanState: "approved" });
const run = state.taskRuns.find((r) => r.id === claimed!.id);
expect(run?.state).toBe("completed");