Files
PaperClipAI/packages/shared/src/remote-workspace.ts
T
DottaandPaperclip 5e4cc01073 feat(workspaces): keep remote files off the webserver disk (#18403)
Keep native sandbox workspace files independent of the webserver disk. Use encrypted saved versions and Postgres ownership so a replacement server can recover files and continue work. Enable the path by default for new eligible workspaces when durable storage is configured.

Preserve explicit opt-outs and existing host workspaces. Include safe snapshot repair, Files access, import, migration and retention. Bound saved-Files resources, preserve imported Git recovery, reject unfinished Git operations, publish immutable concurrent captures, and verify permanent object-version deletion.

Validated 471 targeted local controls, build/typechecks, all 40 CI partitions, fresh Greptile 5/5 and security review. The PR records original failed attempts and scoped retries.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-31 14:54:01 -05:00

59 lines
2.6 KiB
TypeScript

import { z } from "zod";
const repository = z.object({
id: z.string().min(1),
relativePath: z.string().min(1),
repoUrl: z.string().min(1),
ref: z.string().nullable(),
pinnedCommit: z.string().optional(),
readOnly: z.boolean().optional(),
projectId: z.string().uuid().nullable().optional(),
projectWorkspaceId: z.string().uuid().nullable().optional(),
}).strict();
export const remoteWorkspaceSourceSchema = z.discriminatedUnion("kind", [
z.object({ kind: z.literal("empty"), repositories: z.array(repository).optional() }).strict(),
z.object({
kind: z.literal("git"), repoUrl: z.string().min(1), ref: z.string().nullable(),
branchName: z.string().nullable().optional(),
// Older recipes create a task branch from ref when branchName is present.
branchMode: z.enum(["create", "existing"]).optional(),
repositories: z.array(repository).optional(),
}).strict(),
]);
export type RemoteWorkspaceSource = z.infer<typeof remoteWorkspaceSourceSchema>;
/** Paths in this record belong to the named remote allocation, never the controller. */
export const remoteWorkspaceAuthoritySchema = z.object({
schema: z.literal("paperclip.remote-workspace.v1"),
epoch: z.number().int().positive(),
snapshotId: z.string().uuid().nullable(),
source: remoteWorkspaceSourceSchema,
location: z.object({
environmentId: z.string().uuid(), leaseId: z.string().uuid(),
providerLeaseId: z.string().min(1), remoteCwd: z.string().min(1),
}).strict().nullable(),
writerRunId: z.string().uuid().nullable(),
dirty: z.boolean(),
}).strict();
export type RemoteWorkspaceAuthority = z.infer<typeof remoteWorkspaceAuthoritySchema>;
export const remoteWorkspaceRunReferenceSchema = z.object({
schema: z.literal("paperclip.remote-workspace-run.v1"),
workspaceId: z.string().uuid(), leaseId: z.string().uuid(), providerLeaseId: z.string().min(1),
remoteCwd: z.string().min(1), epoch: z.number().int().positive(),
state: z.enum(["prepared", "saved"]), snapshotId: z.string().uuid().nullable(),
}).strict();
export type RemoteWorkspaceRunReference = z.infer<typeof remoteWorkspaceRunReferenceSchema>;
export function readRemoteWorkspaceRunReference(value: unknown): RemoteWorkspaceRunReference | null {
if (value === undefined || value === null) return null;
return remoteWorkspaceRunReferenceSchema.parse(value);
}
export function readRemoteWorkspaceAuthority(metadata: Record<string, unknown> | null | undefined): RemoteWorkspaceAuthority | null {
const value = metadata?.remoteWorkspace;
if (value === undefined || value === null) return null;
return remoteWorkspaceAuthoritySchema.parse(value);
}