import type { CapturedTenantContext } from "../types.js"; /** * Workspace configuration */ export interface WorkspaceConfig { /** Base directory for workspaces (default: `/claude-code-workspaces`) */ baseDir?: string; /** Run ID for unique workspace isolation */ runId: string; /** Tenant context for API access */ tenant: CapturedTenantContext; /** File patterns to include (glob-like, default: all) */ include?: string[]; /** File patterns to exclude (glob-like) */ exclude?: string[]; /** Maximum file size to sync (bytes, default: 10MB) */ maxFileSize?: number; /** Enable debug logging */ debug?: boolean; } /** * File change tracking */ export interface FileChange { path: string; type: "created" | "modified" | "deleted"; originalChecksum?: string; newChecksum?: string; } /** * Workspace sync result */ export interface WorkspaceSyncResult { /** Local workspace directory */ workspaceDir: string; /** Number of files downloaded */ filesDownloaded: number; /** Total bytes downloaded */ bytesDownloaded: number; /** Files that were skipped for benign reasons (too large, excluded by pattern) */ skippedFiles: string[]; /** * Files that failed to download (read threw). Kept separate from skippedFiles * so a fetch/permission failure isn't silently indistinguishable from an * intentional skip. */ downloadErrors: Array<{ path: string; error: string; }>; /** Duration in ms */ duration: number; } /** * Upload result */ export interface UploadResult { /** Files that were actually uploaded via the onUpload handler */ uploaded: FileChange[]; /** * Files that were NOT uploaded because no onUpload handler was provided. * Distinct from `uploaded` so callers don't mistake a dry run for a real one. */ skipped: FileChange[]; /** Files that failed to upload */ failed: Array<{ path: string; error: string; }>; /** Duration in ms */ duration: number; } /** * Workspace manager for Claude Code execution */ export declare class WorkspaceSync { private config; private fileChecksums; private initialized; constructor(config: WorkspaceConfig); /** * Get the workspace directory path */ get workspaceDir(): string; /** * Initialize workspace by downloading project files */ initialize(): Promise; /** * Detect changes in the workspace */ detectChanges(): Promise; /** * Recursively walk directory and detect changes. * * SECURITY: Uses lstat (not stat) and skips any symlink it finds, so a * symlink planted inside the workspace cannot cause us to descend into — * or read the contents of — files outside the workspace (VULN-FS-4). */ private walkAndDetect; /** * Upload changes back to Veryfront API * * NOTE: This requires write API support. Currently returns pending changes * for manual review or future API implementation. */ uploadChanges(changes: FileChange[], options?: { /** Callback to get file content for upload */ onUpload?: (path: string, content: string, type: FileChange["type"]) => Promise; }): Promise; /** * Safely resolve a path within the workspace, preventing path traversal * and symlink-based escapes (VULN-FS-4). * * - Rejects NUL bytes outright. * - Rejects any intermediate path segment that is a symlink. * - Re-checks containment by realpath-ing the parent directory after the * segment walk, so a symlink that resolves through a non-symlink directory * chain still cannot escape the workspace. * * Note: this deliberately rejects ALL symlinks inside the workspace — even * those whose targets remain within it — because the race window between * resolution and use is not worth the complexity for our use-case. */ private resolveSafePath; /** * Read a file from the workspace */ readFile(path: string): Promise; /** * Write a file to the workspace */ writeFile(path: string, content: string): Promise; /** * Delete a file from the workspace */ deleteFile(path: string): Promise; /** * Check if a file exists in the workspace */ fileExists(path: string): Promise; /** * Clean up the workspace directory */ cleanup(): Promise; } /** * Create a workspace sync for a Claude Code run */ export declare function createWorkspaceSync(config: WorkspaceConfig): WorkspaceSync; /** * Execute a function with a synchronized workspace * * @example * ```typescript * const result = await withWorkspace( * { runId: "abc123", tenant }, * async (workspace) => { * // Workspace is initialized with project files * await runBashCommand("npm install", workspace.workspaceDir); * await runBashCommand("npm test", workspace.workspaceDir); * * // Return result * return { success: true }; * }, * ); * * // Changes are automatically detected and returned * console.log(result.changes); * ``` */ export declare function withWorkspace(config: WorkspaceConfig, fn: (workspace: WorkspaceSync) => Promise): Promise<{ result: T; changes: FileChange[]; syncResult: WorkspaceSyncResult; }>; //# sourceMappingURL=workspace-sync.d.ts.map