Files
PaperClipAI/packages/shared/src/work-folders.ts
T
Dotta a13bb6b395 Add sandbox work-folder persistence and shared lifecycle
Introduce company-scoped task, agent, user and project files, recoverable trash, object-backed repository checkpoints, and a shared 180-second lifecycle for sandbox execution. Wire file browsing and save status into the owner surfaces. This is an implementation checkpoint; staging acceptance and the complete merge gate remain outstanding.
2026-09-07 11:52:58 -05:00

64 lines
1.8 KiB
TypeScript

/** Durable Paperclip files; CLI homes and caches are deliberately not scopes. */
export const WORK_FOLDER_SCOPES = ["task", "agent", "user", "project"] as const;
export type WorkFolderScope = (typeof WORK_FOLDER_SCOPES)[number];
export const WORK_FOLDER_SYNC_INTERVAL_MS = 180_000;
export interface WorkFolderOwner {
companyId: string;
scope: WorkFolderScope;
ownerId: string;
}
export interface WorkFile {
id: string;
path: string;
kind: "file" | "directory";
byteSize: number;
sha256: string | null;
executable: boolean;
contentType: string;
deletedAt: string | null;
updatedAt: string;
}
export interface WorkFolderListing {
id: string;
owner: WorkFolderOwner;
files: WorkFile[];
nextCursor: string | null;
}
export interface WorkFolderSyncStatus {
runId: string;
state: "starting" | "saved" | "saving" | "failed";
lastSavedAt: string | null;
error: string | null;
refreshRequested: boolean;
active: boolean;
}
export interface SandboxWorkFolderManifest {
version: 1;
companyId: string;
runId: string;
taskId: string | null;
agentId: string;
responsibleUserId: string | null;
projectId: string | null;
leaseId: string;
sandboxKey: string;
home: string;
finalCheckpointAt?: string;
folders: Record<WorkFolderScope, string | null>;
repositories: Array<{ bindingId: string; workspaceId: string; name: string; primary: boolean }>;
}
/** Reject ambiguous paths instead of normalizing traversal into a valid path. */
export function validateWorkFilePath(value: string): string {
if (!value || value.length > 1024 || /[\\\x00-\x1f\x7f]/.test(value)
|| value.split("/").some((part) => !part || part === "." || part === ".." || part === ".paperclip-runtime")) {
throw new Error("Invalid work file path");
}
return value;
}