Files
PaperClipAI/server/src/services/remote-workspaces.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

711 lines
55 KiB
TypeScript

import { createHash, randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdtemp, rm, statfs } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { and, eq, inArray, sql } from "drizzle-orm";
import {
environmentLeases, executionWorkspaces, executionWorkspaceSnapshots, heartbeatRuns, issues, workspaceOperations, type Db,
} from "@paperclipai/db";
import {
readRemoteWorkspaceAuthority, remoteWorkspaceSourceSchema,
type RemoteWorkspaceAuthority, type RemoteWorkspaceRunReference, type RemoteWorkspaceSource,
type EnvironmentLease,
} from "@paperclipai/shared";
import type { AdapterExecutionTarget } from "@paperclipai/adapter-utils/execution-target";
import { getStorageService, type StorageService } from "../storage/index.js";
import { loadConfig } from "../config.js";
import { conflict, forbidden, unprocessable } from "../errors.js";
import { logger } from "../middleware/logger.js";
import { createGitRemoteAuthProvider } from "./git-credentials.js";
import { notifyCancelledWorkspacePreservation } from "./cancelled-workspace-preservation-notifications.js";
import { assertExecutionWorkspaceRecordAccess, assertTaskWorkspaceExecutionAccess } from "./task-workspace-access.js";
import { hasRemoteTerminationReceipt } from "./remote-execution-termination.js";
import { persistActivity } from "./activity-log.js";
import {
captureRemoteWorkspaceArchive, cleanupRemoteWorkspaceArchive, prepareRemoteWorkspaceDirectory, prepareRemoteRepository, remoteWorkspaceRepositoryPath,
requireRemoteWorkspaceTarget, restoreRemoteWorkspaceDirectory,
} from "./remote-workspace-transport.js";
import {
beginWorkspaceSnapshot, getWorkspaceSnapshot, loadWorkspaceSnapshotArchive, uploadWorkspaceSnapshot,
} from "./remote-workspace-snapshot-storage.js";
import { restoreRemoteWorkspaceArchive } from "./remote-workspace-snapshot-archive.js";
import { removeTaskDirectoryTree } from "./isolated-task-directory-snapshot.js";
import { attachPreservedWorkspaceSnapshots } from "./workspace-recovery-snapshot.js";
import { notifyRemoteWorkspaceSnapshots } from "./remote-workspace-snapshot-retention.js";
import { notifyTerminalWorkspaceWork } from "./terminal-workspace-work.js";
import { preserveRemoteWorkspacePreparationFailure } from "./native-runtime/native-workspace-finalization-state.js";
export { readRemoteWorkspaceAuthority, type RemoteWorkspaceAuthority, type RemoteWorkspaceSource } from "@paperclipai/shared";
export { remoteWorkspaceRepositoryPath } from "./remote-workspace-transport.js";
export { clearRemoteWorkspaceLocationForReleasedLease } from "./remote-workspace-snapshot-retention.js";
type Workspace = Pick<typeof executionWorkspaces.$inferSelect, "id" | "companyId" | "metadata" | "sourceIssueId" | "projectId" | "projectWorkspaceId">;
type Transaction = Parameters<Parameters<Db["transaction"]>[0]>[0];
const object = (value: unknown): Record<string, unknown> => value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
const SAVE_WORK_SCHEMA = "paperclip.remote-workspace-save-work.v1";
const RETIRED_SAVE_SCHEMA = "paperclip.remote-workspace-retired-save.v1";
type SaveWork = { schema: typeof SAVE_WORK_SCHEMA; activeIds: string[]; unknown: boolean; quiescedAt: string | null };
function readSaveWork(metadata: Record<string, unknown> | null | undefined): SaveWork | null {
const work = object(metadata?.saveWork);
return work.schema === SAVE_WORK_SCHEMA && Array.isArray(work.activeIds)
&& work.activeIds.every(id => typeof id === "string") && new Set(work.activeIds).size === work.activeIds.length
&& typeof work.unknown === "boolean" && (work.quiescedAt === null
|| typeof work.quiescedAt === "string" && Number.isFinite(Date.parse(work.quiescedAt)))
? work as SaveWork : null;
}
function sameExportLocation(value: unknown, location: NonNullable<RemoteWorkspaceAuthority["location"]>) {
const recorded = object(value);
return (["environmentId", "leaseId", "providerLeaseId", "remoteCwd"] as const).every(key => recorded[key] === location[key]);
}
export function shouldUseRemoteWorkspaces(input: {
adapterType: string; environmentDriver: string | null; experimentalEnabled: boolean; existingWorkspace?: { metadata?: Record<string, unknown> | null } | null;
}) {
const enrolled = readRemoteWorkspaceAuthority(input.existingWorkspace?.metadata) !== null;
if (enrolled && (input.adapterType !== "paperclip_runner" || input.environmentDriver !== "sandbox")) {
throw conflict("This workspace requires its native sandbox execution environment.", { code: "remote_workspace_execution_required" });
}
if (enrolled) return true;
// Existing host authority changes only through the explicit import workflow.
if (input.existingWorkspace || !input.experimentalEnabled || input.adapterType !== "paperclip_runner"
|| input.environmentDriver !== "sandbox") return false;
return configuredRemoteWorkspacePersistence() !== null;
}
function configuredRemoteWorkspacePersistence(storage?: StorageService) {
const config = loadConfig();
const databaseUrl = config.databaseUrl?.trim();
const keyFilePath = process.env.PAPERCLIP_SECRETS_MASTER_KEY?.trim() ? null : config.secretsMasterKeyFilePath;
if ((storage?.provider ?? config.storageProvider) !== "s3" || !databaseUrl || !/^postgres(?:ql)?:\/\//.test(databaseUrl)
|| (keyFilePath !== null && !existsSync(keyFilePath))) {
return null;
}
return { databaseUrl, keyFilePath };
}
export function assertRemoteWorkspacePersistence(storage: StorageService = getStorageService()) {
const persistence = configuredRemoteWorkspacePersistence(storage);
if (!persistence) throw conflict("Remote workspace storage requires external Postgres, object storage, and a stable configured encryption key.",
{ code: "remote_workspace_persistence_unavailable" });
return persistence;
}
function reference(workspaceId: string, authority: RemoteWorkspaceAuthority, state: "prepared" | "saved"): RemoteWorkspaceRunReference {
if (!authority.location) throw new Error("remote_workspace_location_missing");
return { schema: "paperclip.remote-workspace-run.v1", workspaceId, epoch: authority.epoch,
leaseId: authority.location.leaseId, providerLeaseId: authority.location.providerLeaseId,
remoteCwd: authority.location.remoteCwd, state, snapshotId: authority.snapshotId };
}
async function currentWorkspace(tx: Transaction, companyId: string, workspaceId: string) {
const [workspace] = await tx.select().from(executionWorkspaces).where(and(eq(executionWorkspaces.id, workspaceId),
eq(executionWorkspaces.companyId, companyId))).for("update");
if (!workspace) throw new Error("remote_workspace_missing");
return workspace;
}
export async function reserveRemoteWorkspace(input: {
db: Db; companyId: string; issueId: string; runId: string; agentId: string; environmentId: string;
projectId: string | null; projectWorkspaceId: string | null; existingWorkspaceId: string | null;
source: RemoteWorkspaceSource; storage?: StorageService;
mode?: string;
fresh?: { attemptId: string; previousWorkspaceId: string | null; expectedBindingRevision: number };
recoverySourceRunIds?: string[];
}) {
assertRemoteWorkspacePersistence(input.storage ?? getStorageService());
const source = remoteWorkspaceSourceSchema.parse(input.source);
return input.db.transaction(async tx => {
const [issue] = await tx.select().from(issues).where(and(eq(issues.companyId, input.companyId), eq(issues.id, input.issueId))).for("update");
const [run] = await tx.select().from(heartbeatRuns).where(and(eq(heartbeatRuns.companyId, input.companyId), eq(heartbeatRuns.id, input.runId)));
if (!issue || !run || run.agentId !== input.agentId || run.status !== "running" ||
![run.nativeIssueId, run.issueId, object(run.contextSnapshot).issueId].includes(input.issueId)) throw forbidden("Workspace preparation requires its current task execution.");
if (input.fresh && (issue.executionWorkspaceId !== input.fresh.previousWorkspaceId
|| issue.workspaceBindingRevision !== input.fresh.expectedBindingRevision)) throw conflict("The task workspace selection changed.");
let selected = input.fresh ? input.fresh.attemptId : input.existingWorkspaceId ?? issue.executionWorkspaceId;
let selectedSharedSource = false;
if (!selected && input.mode === "shared_workspace" && input.projectWorkspaceId) {
await tx.execute(sql`select pg_advisory_xact_lock(hashtextextended(${`remote-shared-workspace:${input.companyId}:${input.projectWorkspaceId}`}, 0))`);
const [shared] = await tx.select({ id: executionWorkspaces.id }).from(executionWorkspaces).where(and(
eq(executionWorkspaces.companyId, input.companyId), eq(executionWorkspaces.projectWorkspaceId, input.projectWorkspaceId),
eq(executionWorkspaces.mode, "shared_workspace"), eq(executionWorkspaces.status, "active"),
sql`${executionWorkspaces.metadata}->'remoteWorkspace'->>'schema' = 'paperclip.remote-workspace.v1'`,
)).limit(1);
selected = shared?.id ?? null;
selectedSharedSource = selected !== null;
}
const [selectedRow] = selected ? await tx.select().from(executionWorkspaces).where(and(eq(executionWorkspaces.companyId, input.companyId), eq(executionWorkspaces.id, selected))).for("update") : [];
const existing = selectedRow ?? null;
if (selected && !existing && !input.fresh) throw new Error("remote_workspace_missing");
const authority = readRemoteWorkspaceAuthority(existing?.metadata);
if (existing && !authority) throw conflict("Import this existing workspace before enabling remote storage.", { code: "remote_workspace_migration_required" });
if (existing && authority) {
if ((existing.sourceIssueId !== input.issueId && existing.mode !== "shared_workspace") || ["archived", "cleanup_failed"].includes(existing.status)) throw conflict("Workspace ownership changed.");
if (existing.sourceIssueId !== input.issueId) {
const actor = { type: "agent", source: "agent_jwt", companyId: input.companyId,
agentId: input.agentId, runId: input.runId,
onBehalfOfUserId: run.responsibleUserId === "local-board" ? null : run.responsibleUserId } as const;
await assertTaskWorkspaceExecutionAccess(tx, actor, input.companyId, selectedSharedSource
? { issueId: input.issueId, projectWorkspaceId: input.projectWorkspaceId }
: { issueId: input.issueId, workspaceId: existing.id });
await assertExecutionWorkspaceRecordAccess(tx, actor, input.companyId, existing.id);
}
const [uncertain] = await tx.select({ id: workspaceOperations.id }).from(workspaceOperations).where(and(
eq(workspaceOperations.companyId, input.companyId), eq(workspaceOperations.executionWorkspaceId, existing.id),
eq(workspaceOperations.phase, "remote_workspace_provision"), sql`${workspaceOperations.metadata}->>'uncertain' = 'true'`,
sql`${workspaceOperations.heartbeatRunId} is distinct from ${input.runId}::uuid`,
)).limit(1);
if (uncertain) throw conflict("The prior provisioning operation needs verification before this workspace can run again.", { code: "remote_workspace_provision_unverified" });
if (authority.writerRunId && authority.writerRunId !== input.runId) {
const [prior] = await tx.select({ status: heartbeatRuns.status }).from(heartbeatRuns).where(and(
eq(heartbeatRuns.companyId, input.companyId), eq(heartbeatRuns.id, authority.writerRunId)));
const leases = await tx.select().from(environmentLeases).where(and(eq(environmentLeases.companyId, input.companyId),
eq(environmentLeases.heartbeatRunId, authority.writerRunId)));
const unusedReservation = !authority.dirty && prior && ["failed", "cancelled", "interrupted", "timed_out", "succeeded"].includes(prior.status)
&& leases.every(hasRemoteTerminationReceipt);
if (!unusedReservation) throw conflict("The workspace still has an unfinished writer or save operation.", { code: "remote_workspace_writer_pending" });
}
if (authority.writerRunId === input.runId) return existing;
const next: RemoteWorkspaceAuthority = { ...authority, source, epoch: authority.epoch + 1, writerRunId: input.runId };
const [saved] = await tx.update(executionWorkspaces).set({ metadata: { ...existing.metadata, remoteWorkspace: next }, updatedAt: new Date() })
.where(eq(executionWorkspaces.id, existing.id)).returning();
if (issue.executionWorkspaceId !== existing.id) await tx.update(issues).set({ executionWorkspaceId: existing.id,
executionWorkspacePreference: "reuse_existing", workspaceBindingRevision: issue.workspaceBindingRevision + 1,
updatedAt: new Date() }).where(eq(issues.id, issue.id));
return saved!;
}
const id = input.fresh?.attemptId ?? randomUUID();
const initial: RemoteWorkspaceAuthority = { schema: "paperclip.remote-workspace.v1", epoch: 1,
snapshotId: null, source, location: null, writerRunId: input.runId, dirty: false };
const [workspace] = await tx.insert(executionWorkspaces).values({ id, companyId: input.companyId, sourceIssueId: input.issueId,
projectId: input.projectId, projectWorkspaceId: input.projectWorkspaceId, mode: input.mode ?? "isolated_workspace",
strategyType: source.kind === "git" ? "git_worktree" : "project_primary", name: "Task workspace", status: "active",
providerType: "cloud_sandbox", cwd: `/workspaces/${id}`, repoUrl: source.kind === "git" ? source.repoUrl : null,
baseRef: source.kind === "git" ? source.ref : null, branchName: source.kind === "git" ? source.branchName ?? null : null,
metadata: { remoteWorkspace: initial, ...(input.recoverySourceRunIds?.length
? { remoteWorkspaceRecoverySourceRunIds: input.recoverySourceRunIds } : {}) },
}).returning();
await tx.update(issues).set({ executionWorkspaceId: id, executionWorkspacePreference: "reuse_existing",
workspaceBindingRevision: issue.workspaceBindingRevision + 1, updatedAt: new Date() }).where(eq(issues.id, issue.id));
await persistActivity(tx as unknown as Db, { companyId: input.companyId, actorType: "system", actorId: "remote-workspaces",
action: "workspace.remote_created", entityType: "execution_workspace", entityId: id, runId: input.runId,
issueId: input.issueId, details: { authority: "sandbox", epoch: initial.epoch } });
return workspace!;
});
}
async function persistRunReference(tx: Transaction, companyId: string, runId: string, value: RemoteWorkspaceRunReference) {
const [run] = await tx.select({ profile: heartbeatRuns.runnerProfileJson, nativeIssueId: heartbeatRuns.nativeIssueId,
issueId: heartbeatRuns.issueId, contextIssueId: sql<unknown>`${heartbeatRuns.contextSnapshot}->'issueId'` }).from(heartbeatRuns)
.where(and(eq(heartbeatRuns.companyId, companyId), eq(heartbeatRuns.id, runId))).for("update");
if (!run) throw new Error("remote_workspace_run_missing");
await notifyCancelledWorkspacePreservation(tx, { companyId, runId });
await tx.update(heartbeatRuns).set({ runnerProfileJson: { ...run.profile, remoteWorkspace: value }, updatedAt: new Date() })
.where(and(eq(heartbeatRuns.companyId, companyId), eq(heartbeatRuns.id, runId)));
return run.nativeIssueId ?? run.issueId ?? (typeof run.contextIssueId === "string" ? run.contextIssueId : null);
}
/** Retry acknowledged or explicitly retired exports, never active captures. */
async function cleanupSavedExports(input: { db: Db; companyId: string; workspaceId: string;
location: NonNullable<RemoteWorkspaceAuthority["location"]>; target: ReturnType<typeof requireRemoteWorkspaceTarget>;
assertOwned: () => Promise<void> }) {
for (;;) {
const rows = await input.db.select({ id: workspaceOperations.id, metadata: workspaceOperations.metadata,
phase: workspaceOperations.phase }).from(workspaceOperations).where(and(
eq(workspaceOperations.companyId, input.companyId), eq(workspaceOperations.executionWorkspaceId, input.workspaceId),
inArray(workspaceOperations.phase, ["remote_workspace_snapshot", "remote_workspace_restore"]),
sql`(${workspaceOperations.status} = 'succeeded' or (${workspaceOperations.status} = 'failed'
and ${workspaceOperations.metadata}->'retiredSave'->>'schema' = ${RETIRED_SAVE_SCHEMA}))`,
sql`(${workspaceOperations.metadata}->'saveWork'->>'schema' is distinct from ${SAVE_WORK_SCHEMA}
or (${workspaceOperations.metadata}#>'{saveWork,unknown}' = 'false'::jsonb and case when jsonb_typeof(${workspaceOperations.metadata}#>'{saveWork,activeIds}') = 'array'
then jsonb_array_length(${workspaceOperations.metadata}#>'{saveWork,activeIds}') = 0 else false end))`,
sql`${workspaceOperations.metadata}->'exportCleanup'->>'state' = 'pending'`,
sql`${workspaceOperations.metadata}->'exportCleanup'->>'environmentId' = ${input.location.environmentId}`,
sql`${workspaceOperations.metadata}->'exportCleanup'->>'providerLeaseId' = ${input.location.providerLeaseId}`,
sql`${workspaceOperations.metadata}->'exportCleanup'->>'remoteCwd' = ${input.location.remoteCwd}`,
)).limit(20);
if (!rows.length) return true;
for (const row of rows) {
await input.assertOwned();
let cleaned = false;
try {
await cleanupRemoteWorkspaceArchive({ target: input.target,
remoteCwd: row.phase === "remote_workspace_restore" ? path.posix.dirname(input.location.remoteCwd) : input.location.remoteCwd,
operationId: row.id });
cleaned = true;
} catch { /* Keep a durable retry obligation; the acknowledged snapshot is safe. */ }
await input.assertOwned();
await input.db.update(workspaceOperations).set({ metadata: sql`jsonb_set(coalesce(${workspaceOperations.metadata}, '{}'::jsonb),
'{exportCleanup}', coalesce(${workspaceOperations.metadata}->'exportCleanup', '{}'::jsonb) ||
${JSON.stringify({ state: cleaned ? "cleaned" : "pending", lastError: cleaned ? null : "remote_workspace_export_cleanup_failed" })}::jsonb)`,
updatedAt: new Date() })
.where(and(eq(workspaceOperations.id, row.id), eq(workspaceOperations.companyId, input.companyId),
inArray(workspaceOperations.status, ["succeeded", "failed"])));
if (!cleaned) return false;
}
}
}
/** The caller retains execution admission and provider ownership; this owns file authority. */
function isSavedWorkspaceRepository(authority: RemoteWorkspaceAuthority,
repository: NonNullable<RemoteWorkspaceSource["repositories"]>[number]) {
return authority.snapshotId !== null && (authority.source.repositories ?? []).some(saved =>
saved.id === repository.id && saved.relativePath === repository.relativePath && saved.repoUrl === repository.repoUrl
&& (saved.ref ?? "HEAD") === (repository.ref ?? "HEAD") && saved.pinnedCommit === repository.pinnedCommit
&& (saved.readOnly ?? false) === (repository.readOnly ?? false));
}
export async function prepareRemoteWorkspace(input: {
db: Db; workspace: Workspace; runId: string; lease: EnvironmentLease; target: AdapterExecutionTarget | null;
source?: RemoteWorkspaceSource; assertOwned?: () => Promise<void>;
provisionCommand?: string | null; runtimeProvisionCommand?: string | null; storage?: StorageService;
}) {
const target = requireRemoteWorkspaceTarget(input.target);
const initial = readRemoteWorkspaceAuthority(input.workspace.metadata);
if ((input.source ?? initial?.source)?.repositories?.some(repository => repository.readOnly)) {
throw conflict("The selected native sandbox cannot enforce read-only referenced project roots.",
{ code: "remote_workspace_readonly_sources_unsupported" });
}
if (!initial || initial.writerRunId !== input.runId || !input.lease.environmentId || !input.lease.providerLeaseId
|| input.lease.companyId !== input.workspace.companyId || input.lease.heartbeatRunId !== input.runId) throw new Error("remote_workspace_binding_mismatch");
await input.assertOwned?.();
const sameAllocation = initial.location?.providerLeaseId === input.lease.providerLeaseId
&& initial.location?.environmentId === input.lease.environmentId;
if (initial.location && !sameAllocation && initial.dirty) throw conflict("The prior workspace has unsaved work; recover it before replacing its allocation.");
const remoteRoot = sameAllocation ? initial.location!.remoteCwd : path.posix.join(target.remoteCwd, ".paperclip-workspaces", input.workspace.id);
const preparationIdentity = createHash("sha256").update(JSON.stringify([input.workspace.companyId, input.workspace.id,
input.runId, initial.epoch, input.lease.id, input.lease.providerLeaseId, remoteRoot, initial.source])).digest("hex");
let restoredExport: Awaited<ReturnType<typeof restoreRemoteWorkspaceDirectory>> | null = null;
if (sameAllocation && !await cleanupSavedExports({ db: input.db, companyId: input.workspace.companyId,
workspaceId: input.workspace.id, location: initial.location!, target,
assertOwned: input.assertOwned ?? (async () => undefined) })) throw new Error("remote_workspace_export_cleanup_pending");
if (!sameAllocation) {
if (initial.snapshotId) {
const snapshot = await getWorkspaceSnapshot(input.db, { companyId: input.workspace.companyId, workspaceId: input.workspace.id, snapshotId: initial.snapshotId });
if (!snapshot) throw new Error("remote_workspace_snapshot_missing");
const scratch = await mkdtemp(path.join(os.tmpdir(), "pc-workspace-restore-"));
try {
const archivePath = path.join(scratch, "workspace.pcws");
await loadWorkspaceSnapshotArchive({ storage: input.storage ?? getStorageService(), reference: snapshot, targetPath: archivePath });
restoredExport = await restoreRemoteWorkspaceDirectory({ target, remoteCwd: remoteRoot, archivePath, manifestDigest: snapshot.manifestDigest,
restorationIdentity: preparationIdentity });
} finally { await rm(scratch, { recursive: true, force: true }); }
} else {
await prepareRemoteWorkspaceDirectory({ target, remoteCwd: remoteRoot, source: initial.source,
preparationIdentity,
resolveGitAuth: await createRemoteWorkspaceGitAuth(input.db, input.workspace.companyId, input.runId) });
}
}
for (const repository of (input.source ?? initial.source).repositories ?? []) {
await input.assertOwned?.();
// Only repositories already owned by the saved authority may rely on the
// archive when their old local origin was intentionally omitted. New source
// descriptors still follow normal acquisition and origin verification.
await prepareRemoteRepository({ target, remoteCwd: remoteWorkspaceRepositoryPath(remoteRoot, repository),
repoUrl: repository.repoUrl, ref: repository.ref, pinnedCommit: repository.pinnedCommit,
savedArchive: isSavedWorkspaceRepository(initial, repository),
resolveGitAuth: await createRemoteWorkspaceGitAuth(input.db, input.workspace.companyId, input.runId) });
}
const recoveredFiles: Array<{ operationId: string; sourceRunId: string; path: string }> = [];
const recoverySourceRunIds = input.workspace.metadata?.remoteWorkspaceRecoverySourceRunIds;
if (!sameAllocation && Array.isArray(recoverySourceRunIds) && recoverySourceRunIds.length && input.workspace.sourceIssueId) {
const scratch = await mkdtemp(path.join(os.tmpdir(), "pc-workspace-recovery-import-"));
try {
const recovered = await attachPreservedWorkspaceSnapshots({ db: input.db, companyId: input.workspace.companyId,
issueId: input.workspace.sourceIssueId, sourceRunIds: recoverySourceRunIds as string[], cwd: scratch });
for (const item of recovered) {
const relative = `.paperclip-recovered/${item.operationId}`;
await target.runner!.syncIn!([{ operationId: randomUUID(), files: [{
kind: "directory", sourcePath: item.path, targetPath: path.posix.join(remoteRoot, relative),
}] }]);
recoveredFiles.push({ operationId: item.operationId, sourceRunId: item.sourceRunId, path: relative });
}
} finally { await rm(scratch, { recursive: true, force: true }); }
}
await input.assertOwned?.();
const prepared = await input.db.transaction(async tx => {
const workspace = await currentWorkspace(tx, input.workspace.companyId, input.workspace.id);
const current = readRemoteWorkspaceAuthority(workspace.metadata);
if (!current || current.epoch !== initial.epoch || current.writerRunId !== input.runId) throw new Error("remote_workspace_authority_changed");
// The same run can retry with a different allocation and stale preparation
// input. Its controller lease alone cannot replace already-published dirty files.
if (current.dirty && current.location && (current.location.environmentId !== input.lease.environmentId
|| current.location.providerLeaseId !== input.lease.providerLeaseId || current.location.leaseId !== input.lease.id
|| current.location.remoteCwd !== remoteRoot)) throw new Error("remote_workspace_authority_changed");
const authority: RemoteWorkspaceAuthority = { ...current, dirty: true, location: { environmentId: input.lease.environmentId!,
leaseId: input.lease.id, providerLeaseId: input.lease.providerLeaseId!, remoteCwd: remoteRoot } };
const [lease] = await tx.select().from(environmentLeases).where(and(eq(environmentLeases.id, input.lease.id),
eq(environmentLeases.companyId, workspace.companyId))).for("update");
if (!lease || lease.status !== "active" || lease.environmentId !== input.lease.environmentId
|| lease.providerLeaseId !== authority.location!.providerLeaseId || lease.heartbeatRunId !== input.runId
|| (lease.executionWorkspaceId && lease.executionWorkspaceId !== workspace.id)) throw new Error("remote_workspace_lease_changed");
const [savedLease] = await tx.update(environmentLeases).set({ metadata: { ...lease.metadata, remoteCwd: remoteRoot, remoteWorkspaceId: workspace.id }, updatedAt: new Date() })
.where(eq(environmentLeases.id, lease.id)).returning();
const [savedWorkspace] = await tx.update(executionWorkspaces).set({ cwd: remoteRoot, providerRef: remoteRoot,
metadata: { ...workspace.metadata, remoteWorkspace: authority,
...(recoveredFiles.length ? { remoteWorkspaceRecoveredFiles: recoveredFiles } : {}) }, updatedAt: new Date() }).where(eq(executionWorkspaces.id, workspace.id)).returning();
const runReference = reference(workspace.id, authority, "prepared");
const issueId = await persistRunReference(tx, workspace.companyId, input.runId, runReference);
if (restoredExport) {
const [existing] = await tx.select().from(workspaceOperations).where(eq(workspaceOperations.id, restoredExport.operationId)).for("update");
if (existing && (existing.companyId !== workspace.companyId || existing.executionWorkspaceId !== workspace.id
|| existing.heartbeatRunId !== input.runId || existing.phase !== "remote_workspace_restore")) throw new Error("remote_workspace_restore_receipt_changed");
const metadata = { epoch: authority.epoch, snapshotId: initial.snapshotId,
exportCleanup: { ...authority.location, stagingRoot: restoredExport.stagingRoot,
state: restoredExport.cleanupPending ? "pending" : "cleaned" } };
if (existing) await tx.update(workspaceOperations).set({ status: "succeeded", metadata, finishedAt: new Date(), updatedAt: new Date() })
.where(eq(workspaceOperations.id, existing.id));
else await tx.insert(workspaceOperations).values({ id: restoredExport.operationId, companyId: workspace.companyId,
executionWorkspaceId: workspace.id, heartbeatRunId: input.runId, issueId,
phase: "remote_workspace_restore", status: "succeeded", metadata, finishedAt: new Date() });
}
return { workspace: savedWorkspace!, lease: savedLease! as EnvironmentLease, remoteRoot, reference: runReference };
});
target.remoteCwd = remoteRoot;
if (restoredExport?.cleanupPending) await cleanupSavedExports({ db: input.db, companyId: input.workspace.companyId,
workspaceId: input.workspace.id, location: readRemoteWorkspaceAuthority(prepared.workspace.metadata)!.location!, target,
assertOwned: input.assertOwned ?? (async () => undefined) });
try {
await provisionRemoteWorkspace({ db: input.db, companyId: input.workspace.companyId, workspaceId: input.workspace.id,
runId: input.runId, epoch: initial.epoch, remoteCwd: remoteRoot, target, assertOwned: input.assertOwned,
commands: [!initial.snapshotId ? input.provisionCommand : null, input.runtimeProvisionCommand] });
if (!initial.snapshotId) {
await saveRemoteWorkspace({ ...input, workspace: prepared.workspace, lease: prepared.lease, target,
assertOwned: input.assertOwned ?? (async () => undefined), boundary: "initial" });
const [workspace] = await input.db.select().from(executionWorkspaces).where(and(eq(executionWorkspaces.id, input.workspace.id), eq(executionWorkspaces.companyId, input.workspace.companyId)));
prepared.workspace = workspace!;
prepared.reference = reference(workspace!.id, readRemoteWorkspaceAuthority(workspace!.metadata)!, "prepared");
}
} catch (error) {
// A first save can fail before runtime selection. Its already-published
// authority is still a file obligation, independent of native admission.
await preserveRemoteWorkspacePreparationFailure(input.db, prepared.lease, { assertOwned: input.assertOwned })
.catch(preservationError => logger.error({ err: preservationError, runId: input.runId }, "Remote workspace preparation recovery was deferred; source remains protected"));
throw error;
}
return prepared;
}
/** The workspace lock fences new invocations while completed final authority
* retires only attempts whose every registered invocation has settled. */
async function retireQuiescedSaves(tx: Transaction, workspace: Workspace, runId: string, epoch: number,
location: NonNullable<RemoteWorkspaceAuthority["location"]>) {
const authority = readRemoteWorkspaceAuthority(workspace.metadata);
if (!authority || authority.epoch !== epoch || authority.dirty || authority.writerRunId !== null
|| !authority.snapshotId || !sameExportLocation(authority.location, location)) return;
const [final] = await tx.select({ id: executionWorkspaceSnapshots.id, generation: executionWorkspaceSnapshots.generation,
operationId: workspaceOperations.id, metadata: workspaceOperations.metadata }).from(executionWorkspaceSnapshots)
.innerJoin(workspaceOperations, and(eq(workspaceOperations.id, executionWorkspaceSnapshots.sourceOperationId),
eq(workspaceOperations.companyId, workspace.companyId), eq(workspaceOperations.executionWorkspaceId, workspace.id),
eq(workspaceOperations.heartbeatRunId, runId), eq(workspaceOperations.phase, "remote_workspace_snapshot"),
eq(workspaceOperations.status, "succeeded"), sql`${workspaceOperations.metadata}->>'boundary' = 'final'`,
sql`${workspaceOperations.metadata}->>'epoch' = ${String(epoch)}`))
.where(and(eq(executionWorkspaceSnapshots.companyId, workspace.companyId), eq(executionWorkspaceSnapshots.executionWorkspaceId, workspace.id),
eq(executionWorkspaceSnapshots.id, authority.snapshotId), eq(executionWorkspaceSnapshots.sourceRunId, runId),
eq(executionWorkspaceSnapshots.epoch, epoch), eq(executionWorkspaceSnapshots.status, "committed"))).limit(1);
if (!final || final.metadata?.snapshotId !== final.id || !sameExportLocation(final.metadata?.exportCleanup, location)) return;
const candidates = await tx.select().from(workspaceOperations).where(and(
eq(workspaceOperations.companyId, workspace.companyId), eq(workspaceOperations.executionWorkspaceId, workspace.id),
eq(workspaceOperations.heartbeatRunId, runId), eq(workspaceOperations.phase, "remote_workspace_snapshot"),
inArray(workspaceOperations.status, ["running", "pending"]), sql`${workspaceOperations.metadata}->>'epoch' = ${String(epoch)}`)).for("update");
for (const candidate of candidates) {
const work = readSaveWork(candidate.metadata), generation = Number(candidate.metadata?.generation);
if (!work || work.unknown || work.activeIds.length || !work.quiescedAt || candidate.metadata?.boundary === "final"
|| !Number.isSafeInteger(generation) || generation >= final.generation
|| !sameExportLocation(candidate.metadata?.exportCleanup, location)) continue;
const [snapshot] = await tx.select({ status: executionWorkspaceSnapshots.status, generation: executionWorkspaceSnapshots.generation,
epoch: executionWorkspaceSnapshots.epoch, runId: executionWorkspaceSnapshots.sourceRunId }).from(executionWorkspaceSnapshots).where(and(
eq(executionWorkspaceSnapshots.companyId, workspace.companyId), eq(executionWorkspaceSnapshots.executionWorkspaceId, workspace.id),
eq(executionWorkspaceSnapshots.sourceOperationId, candidate.id))).for("update");
if (snapshot && (!["pending", "uploaded"].includes(snapshot.status) || snapshot.generation !== generation
|| snapshot.epoch !== epoch || snapshot.runId !== runId)) continue;
const at = new Date();
await notifyRemoteWorkspaceSnapshots(tx, `workspace:${workspace.id}`);
await tx.update(workspaceOperations).set({ status: "failed", finishedAt: at, updatedAt: at,
metadata: { ...candidate.metadata, retiredSave: { schema: RETIRED_SAVE_SCHEMA, snapshotId: final.id,
operationId: final.operationId, retiredAt: at.toISOString() } } }).where(eq(workspaceOperations.id, candidate.id));
}
}
async function settleSaveInvocation(input: { db: Db; workspace: Workspace; runId: string; epoch: number;
location: NonNullable<RemoteWorkspaceAuthority["location"]>; operationId: string; invocationId: string }) {
return input.db.transaction(async tx => {
const workspace = await currentWorkspace(tx, input.workspace.companyId, input.workspace.id);
const [operation] = await tx.select().from(workspaceOperations).where(and(eq(workspaceOperations.id, input.operationId),
eq(workspaceOperations.companyId, workspace.companyId), eq(workspaceOperations.executionWorkspaceId, workspace.id),
eq(workspaceOperations.heartbeatRunId, input.runId), sql`${workspaceOperations.metadata}->>'epoch' = ${String(input.epoch)}`)).for("update");
const work = readSaveWork(operation?.metadata);
if (!operation || !work || !work.activeIds.includes(input.invocationId)) return false;
const activeIds = work.activeIds.filter(id => id !== input.invocationId);
if (!activeIds.length && !work.unknown) await notifyRemoteWorkspaceSnapshots(tx, `workspace:${workspace.id}`);
await tx.update(workspaceOperations).set({ metadata: { ...operation.metadata,
saveWork: { ...work, activeIds, quiescedAt: activeIds.length ? null : new Date().toISOString() } }, updatedAt: new Date() })
.where(eq(workspaceOperations.id, operation.id));
await retireQuiescedSaves(tx, workspace, input.runId, input.epoch, input.location);
return true;
});
}
export async function saveRemoteWorkspace(input: {
db: Db; workspace: Workspace; runId: string; lease: EnvironmentLease; target: AdapterExecutionTarget | null;
assertOwned: () => Promise<void>; storage?: StorageService; boundary?: string;
}) {
const target = requireRemoteWorkspaceTarget(input.target), boundary = input.boundary ?? "final";
const expected = readRemoteWorkspaceAuthority(input.workspace.metadata);
if (!expected?.location || expected.location.providerLeaseId !== input.lease.providerLeaseId || expected.location.leaseId !== input.lease.id) throw new Error("remote_workspace_save_binding_mismatch");
await input.assertOwned();
const invocationId = randomUUID();
const operation = await input.db.transaction(async tx => {
const workspace = await currentWorkspace(tx, input.workspace.companyId, input.workspace.id);
const current = readRemoteWorkspaceAuthority(workspace.metadata);
if (!current || current.epoch !== expected.epoch || (current.writerRunId !== input.runId && current.dirty)) throw new Error("remote_workspace_authority_changed");
const [previous] = await tx.select().from(workspaceOperations).where(and(eq(workspaceOperations.companyId, workspace.companyId),
eq(workspaceOperations.executionWorkspaceId, workspace.id), eq(workspaceOperations.heartbeatRunId, input.runId),
eq(workspaceOperations.phase, "remote_workspace_snapshot"), sql`${workspaceOperations.metadata}->>'boundary' = ${boundary}`,
sql`${workspaceOperations.metadata}->>'epoch' = ${String(current.epoch)}`)).limit(1).for("update");
if (previous) {
if (object(previous.metadata?.retiredSave).schema === RETIRED_SAVE_SCHEMA) throw new Error("remote_workspace_snapshot_retired");
const work = readSaveWork(previous.metadata);
const [registered] = await tx.update(workspaceOperations).set({ metadata: { ...previous.metadata,
saveWork: { schema: SAVE_WORK_SCHEMA, activeIds: [...(work?.activeIds ?? []), invocationId],
unknown: work?.unknown ?? true, quiescedAt: null } }, updatedAt: new Date() }).where(eq(workspaceOperations.id, previous.id)).returning();
return registered!;
}
const [run] = await tx.select({ nativeIssueId: heartbeatRuns.nativeIssueId, issueId: heartbeatRuns.issueId,
contextSnapshot: heartbeatRuns.contextSnapshot }).from(heartbeatRuns).where(and(
eq(heartbeatRuns.companyId, workspace.companyId), eq(heartbeatRuns.id, input.runId)));
if (!run) throw new Error("remote_workspace_run_missing");
const issueId = run.nativeIssueId ?? run.issueId ?? (typeof run.contextSnapshot?.issueId === "string" ? run.contextSnapshot.issueId : null);
// The workspace lock serializes generation reservations. Failed/deleted
// snapshots still own their generation, and an operation can be committed
// before beginWorkspaceSnapshot creates its ledger row. Count both journals
// across epochs while retaining the last committed snapshot as the parent.
const [reservation] = await tx.execute(sql`select greatest(
coalesce((select max(${executionWorkspaceSnapshots.generation}) from ${executionWorkspaceSnapshots}
where ${executionWorkspaceSnapshots.companyId} = ${workspace.companyId}
and ${executionWorkspaceSnapshots.executionWorkspaceId} = ${workspace.id}), 0),
coalesce((select max(case when ${workspaceOperations.metadata}->>'generation' ~ '^[0-9]+$'
then (${workspaceOperations.metadata}->>'generation')::numeric else 0 end) from ${workspaceOperations}
where ${workspaceOperations.companyId} = ${workspace.companyId}
and ${workspaceOperations.executionWorkspaceId} = ${workspace.id}
and ${workspaceOperations.phase} = 'remote_workspace_snapshot'), 0)
) as generation`);
const generation = Number(reservation?.generation) + 1;
if (!Number.isSafeInteger(generation) || generation < 1) throw new Error("remote_workspace_snapshot_generation_invalid");
const [created] = await tx.insert(workspaceOperations).values({ companyId: workspace.companyId, executionWorkspaceId: workspace.id,
heartbeatRunId: input.runId, issueId, phase: "remote_workspace_snapshot", status: "running",
metadata: { boundary, epoch: current.epoch, parentSnapshotId: current.snapshotId, generation,
saveWork: { schema: SAVE_WORK_SCHEMA, activeIds: [invocationId], unknown: false, quiescedAt: null },
exportCleanup: { ...current.location, state: "pending" } },
}).returning();
return created!;
});
let captured: Awaited<ReturnType<typeof captureRemoteWorkspaceArchive>> | null = null;
try {
const snapshot = await beginWorkspaceSnapshot(input.db, { companyId: input.workspace.companyId, workspaceId: input.workspace.id,
operationId: operation.id, runId: input.runId, parentSnapshotId: operation.metadata?.parentSnapshotId as string | null,
generation: operation.metadata?.generation as number, epoch: expected.epoch });
if (operation.status === "succeeded" && ["committed", "superseded"].includes(snapshot.status)) {
return { snapshotId: snapshot.id, generation: snapshot.generation, sha256: snapshot.sha256!, savedAt: snapshot.updatedAt.toISOString() };
}
const existingUpload = snapshot.status === "uploaded" ? await getWorkspaceSnapshot(input.db, { companyId: input.workspace.companyId,
workspaceId: input.workspace.id, snapshotId: snapshot.id }) : null;
captured = existingUpload ? null : await captureRemoteWorkspaceArchive({ target, remoteCwd: expected.location.remoteCwd, operationId: operation.id });
await input.assertOwned();
const saved = await uploadWorkspaceSnapshot({ db: input.db, storage: input.storage ?? getStorageService(), snapshotId: snapshot.id,
companyId: input.workspace.companyId, workspaceId: input.workspace.id, archivePath: captured?.archivePath ?? "",
manifestDigest: existingUpload?.manifestDigest ?? captured!.manifestDigest });
await input.assertOwned();
const at = new Date();
await input.db.transaction(async tx => {
const workspace = await currentWorkspace(tx, input.workspace.companyId, input.workspace.id);
const current = readRemoteWorkspaceAuthority(workspace.metadata);
if (!current || current.epoch !== expected.epoch || current.writerRunId !== input.runId || current.snapshotId !== snapshot.parentSnapshotId) {
if (current?.epoch === expected.epoch && current.snapshotId === saved.id) return;
throw new Error("remote_workspace_publication_fenced");
}
const authority: RemoteWorkspaceAuthority = { ...current, snapshotId: saved.id,
dirty: boundary !== "final", writerRunId: boundary !== "final" ? input.runId : null };
const [committed] = await tx.update(executionWorkspaceSnapshots).set({ status: "committed", updatedAt: at }).where(and(
eq(executionWorkspaceSnapshots.id, saved.id), eq(executionWorkspaceSnapshots.companyId, workspace.companyId),
eq(executionWorkspaceSnapshots.executionWorkspaceId, workspace.id), eq(executionWorkspaceSnapshots.status, "uploaded"))).returning({ id: executionWorkspaceSnapshots.id });
if (!committed) throw new Error("remote_workspace_snapshot_publication_changed");
await notifyRemoteWorkspaceSnapshots(tx, `workspace:${workspace.id}`);
if (boundary === "final") await notifyTerminalWorkspaceWork(tx, `workspace:${workspace.id}`);
await tx.update(executionWorkspaces).set({ metadata: { ...workspace.metadata, remoteWorkspace: authority }, updatedAt: at }).where(eq(executionWorkspaces.id, workspace.id));
await tx.update(workspaceOperations).set({ status: "succeeded", finishedAt: at, updatedAt: at,
metadata: sql`coalesce(${workspaceOperations.metadata}, '{}'::jsonb) || ${JSON.stringify({ snapshotId: saved.id, sha256: saved.sha256 })}::jsonb` })
.where(eq(workspaceOperations.id, operation.id));
await persistRunReference(tx, workspace.companyId, input.runId, reference(workspace.id, authority, boundary === "final" ? "saved" : "prepared"));
await persistActivity(tx as unknown as Db, { companyId: workspace.companyId, actorType: "system", actorId: "remote-workspaces",
action: "workspace.snapshot_saved", entityType: "execution_workspace", entityId: workspace.id, runId: input.runId,
details: { snapshotId: saved.id, generation: saved.generation, epoch: current.epoch, boundary } });
if (snapshot.parentSnapshotId) await tx.update(executionWorkspaceSnapshots).set({ status: "superseded",
deleteAfter: new Date(at.getTime() + 7 * 86400_000), updatedAt: at }).where(and(eq(executionWorkspaceSnapshots.id, snapshot.parentSnapshotId),
eq(executionWorkspaceSnapshots.companyId, workspace.companyId), eq(executionWorkspaceSnapshots.executionWorkspaceId, workspace.id)));
});
return { snapshotId: saved.id, generation: saved.generation, sha256: saved.sha256, savedAt: at.toISOString() };
} finally {
// Every upload/capture promise has settled before its durable token clears.
// Missing completion evidence remains pinned; cleanup never masks the save error.
try { await captured?.cleanup(); }
catch { logger.warn({ runId: input.runId, operationId: operation.id }, "Remote workspace local archive cleanup failed"); }
try {
if (await settleSaveInvocation({ db: input.db, workspace: input.workspace, runId: input.runId, epoch: expected.epoch,
location: expected.location, operationId: operation.id, invocationId })) {
await cleanupSavedExports({ db: input.db, companyId: input.workspace.companyId, workspaceId: input.workspace.id,
location: expected.location, target, assertOwned: input.assertOwned });
}
} catch { logger.warn({ runId: input.runId, operationId: operation.id }, "Remote workspace save settlement or export cleanup remains pending"); }
}
}
export async function prepareRemoteWorkspaceRepository(input: {
db: Db; workspace: Workspace; runId: string; lease: EnvironmentLease; target: AdapterExecutionTarget | null;
repository: { id: string; relativePath: string; repoUrl: string; ref: string | null; pinnedCommit?: string; readOnly?: boolean };
assertOwned?: () => Promise<void>; resolveGitAuth?: ReturnType<typeof createGitRemoteAuthProvider>; verifyOnly?: boolean; storage?: StorageService;
}) {
const authority = readRemoteWorkspaceAuthority(input.workspace.metadata);
if (!authority?.location || authority.writerRunId !== input.runId || authority.location.leaseId !== input.lease.id
|| authority.location.providerLeaseId !== input.lease.providerLeaseId) throw new Error("remote_workspace_repository_binding_mismatch");
await input.assertOwned?.();
const target = requireRemoteWorkspaceTarget(input.target);
const result = await prepareRemoteRepository({ target, remoteCwd: remoteWorkspaceRepositoryPath(authority.location.remoteCwd, input.repository),
...input.repository, verifyOnly: input.verifyOnly, savedArchive: isSavedWorkspaceRepository(authority, input.repository),
resolveGitAuth: input.resolveGitAuth ?? await createRemoteWorkspaceGitAuth(input.db, input.workspace.companyId, input.runId) });
await input.assertOwned?.();
await saveRemoteWorkspace({ ...input, target, boundary: `repository:${input.repository.id}:${result.pinnedCommit}`,
assertOwned: input.assertOwned ?? (async () => undefined) });
return result;
}
type SavedWorkspaceFileLimits = {
maxOpen: number; maxArchiveBytes: number; maxScratchBytes: number; maxEntries: number; maxManifestBytes: number; freeSpaceMarginBytes: number;
};
const SAVED_WORKSPACE_FILE_LIMITS: SavedWorkspaceFileLimits = {
maxOpen: 2, maxArchiveBytes: 512 * 1024 * 1024, maxScratchBytes: 2 * 1024 * 1024 * 1024,
maxEntries: 10_000, maxManifestBytes: 4 * 1024 * 1024, freeSpaceMarginBytes: 256 * 1024 * 1024,
};
let savedWorkspaceFileOpen = 0, savedWorkspaceFileReservedBytes = 0;
export function createSavedWorkspaceLoader(db: Db, storage: StorageService = getStorageService(), options: {
limits?: Partial<SavedWorkspaceFileLimits>; availableBytes?: () => Promise<bigint>;
} = {}) {
const limits = { ...SAVED_WORKSPACE_FILE_LIMITS, ...options.limits };
for (const key of Object.keys(limits) as Array<keyof SavedWorkspaceFileLimits>) {
if (!Number.isSafeInteger(limits[key]) || limits[key] < (key === "freeSpaceMarginBytes" ? 0 : 1)
|| limits[key] > SAVED_WORKSPACE_FILE_LIMITS[key]) throw new Error("invalid_saved_workspace_file_limits");
}
const availableBytes = options.availableBytes ?? (async () => {
const disk = await statfs(os.tmpdir(), { bigint: true });
return disk.bavail * disk.bsize;
});
return async (input: { companyId: string; workspaceId: string; snapshotId: string; signal?: AbortSignal }) => {
input.signal?.throwIfAborted();
const [row] = await db.select().from(executionWorkspaceSnapshots).where(and(eq(executionWorkspaceSnapshots.companyId, input.companyId),
eq(executionWorkspaceSnapshots.executionWorkspaceId, input.workspaceId), eq(executionWorkspaceSnapshots.id, input.snapshotId),
eq(executionWorkspaceSnapshots.status, "committed")));
const saved = row ? await getWorkspaceSnapshot(db, input) : null;
input.signal?.throwIfAborted();
if (!saved) throw new Error("remote_workspace_snapshot_unavailable");
if (saved.byteSize > limits.maxArchiveBytes) throw unprocessable("Saved workspace is too large for Files", { code: "too_large" });
// Decryption can hold ciphertext, plaintext, and its atomic publication at
// once. Bound filesystem allocation overhead even for many tiny entries.
const reservation = 3 * saved.byteSize + limits.maxEntries * 16 * 1024;
if (savedWorkspaceFileOpen >= limits.maxOpen || reservation > limits.maxScratchBytes - savedWorkspaceFileReservedBytes) {
throw unprocessable("Saved workspace file extraction capacity is unavailable", { code: "workspace_unavailable" });
}
savedWorkspaceFileOpen++; savedWorkspaceFileReservedBytes += reservation;
let scratch: string | undefined, released = false, cleaning: Promise<void> | undefined;
// Source modes stay intact while reading. Cleanup changes only private directories and skips symlinks.
const cleanup = async () => {
if (released) return;
if (!cleaning) cleaning = (async () => {
if (scratch) await removeTaskDirectoryTree(scratch);
released = true; savedWorkspaceFileOpen--; savedWorkspaceFileReservedBytes -= reservation;
})();
try { await cleaning; } catch (error) { cleaning = undefined; throw error; }
};
try {
// Existing reservations may not have written their bytes yet. Keep them
// deducted from free space until their owned directories are removed.
if (await availableBytes() < BigInt(savedWorkspaceFileReservedBytes + limits.freeSpaceMarginBytes)) {
throw unprocessable("Insufficient temporary disk space for saved workspace Files", { code: "workspace_unavailable" });
}
input.signal?.throwIfAborted();
scratch = await mkdtemp(path.join(os.tmpdir(), "pc-workspace-files-"));
const archive = path.join(scratch, "archive.pcws"), root = path.join(scratch, "files");
await loadWorkspaceSnapshotArchive({ storage, reference: saved, targetPath: archive, signal: input.signal, temporaryParent: scratch });
await restoreRemoteWorkspaceArchive({ archivePath: archive, targetDir: root, expectedManifestDigest: saved.manifestDigest,
signal: input.signal, maxEntries: limits.maxEntries, maxManifestBytes: limits.maxManifestBytes });
return { root, savedAt: row!.updatedAt.toISOString(), cleanup };
} catch (error) {
try { await cleanup(); } catch {
logger.warn({ workspaceId: input.workspaceId }, "Failed to remove saved workspace extraction; capacity remains reserved");
}
if (error instanceof Error && ["remote_workspace_archive_entry_limit", "remote_workspace_archive_manifest_too_large"].includes(error.message)) {
throw unprocessable("Saved workspace manifest exceeds Files extraction limits", { code: "too_large" });
}
throw error;
}
};
}
/** Bind credentials to the current run's identity, including shared workspace users. */
export async function createRemoteWorkspaceGitAuth(db: Db, companyId: string, runId: string,
create: typeof createGitRemoteAuthProvider = createGitRemoteAuthProvider) {
const [run] = await db.select({ agentId: heartbeatRuns.agentId, responsibleUserId: heartbeatRuns.responsibleUserId,
nativeIssueId: heartbeatRuns.nativeIssueId, issueId: heartbeatRuns.issueId, status: heartbeatRuns.status,
contextIssueId: sql<unknown>`${heartbeatRuns.contextSnapshot}->'issueId'`,
}).from(heartbeatRuns).where(and(eq(heartbeatRuns.companyId, companyId), eq(heartbeatRuns.id, runId)));
const issueId = run?.nativeIssueId ?? run?.issueId ?? run?.contextIssueId;
if (!run || run.status !== "running" || typeof issueId !== "string"
|| !/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(issueId)) throw new Error("remote_workspace_git_run_unavailable");
return create(db, companyId, { issueId, heartbeatRunId: runId, agentId: run.agentId,
responsibleUserId: run.responsibleUserId === "local-board" ? null : run.responsibleUserId });
}
/** Arbitrary provisioning commands cannot be replayed after an ambiguous reply. */
export async function provisionRemoteWorkspace(input: {
db: Db; companyId: string; workspaceId: string; runId: string; epoch: number;
remoteCwd: string; target: ReturnType<typeof requireRemoteWorkspaceTarget>; commands: Array<string | null | undefined>;
assertOwned?: () => Promise<void>;
}) {
for (const [index, command] of input.commands.entries()) {
if (!command?.trim()) continue;
await input.assertOwned?.();
const digest = createHash("sha256").update(command).digest("hex");
const operation = await input.db.transaction(async tx => {
const [workspace] = await tx.select().from(executionWorkspaces).where(and(eq(executionWorkspaces.companyId, input.companyId), eq(executionWorkspaces.id, input.workspaceId))).for("update");
const authority = readRemoteWorkspaceAuthority(workspace?.metadata);
const [run] = await tx.select({ status: heartbeatRuns.status }).from(heartbeatRuns).where(and(eq(heartbeatRuns.companyId, input.companyId), eq(heartbeatRuns.id, input.runId)));
if (!authority || authority.epoch !== input.epoch || authority.writerRunId !== input.runId || run?.status !== "running") throw new Error("remote_workspace_provision_fenced");
const [existing] = await tx.select().from(workspaceOperations).where(and(eq(workspaceOperations.companyId, input.companyId),
eq(workspaceOperations.executionWorkspaceId, input.workspaceId), eq(workspaceOperations.heartbeatRunId, input.runId),
eq(workspaceOperations.phase, "remote_workspace_provision"), sql`${workspaceOperations.metadata}->>'index' = ${String(index)}`,
sql`${workspaceOperations.metadata}->>'epoch' = ${String(input.epoch)}`)).limit(1);
if (existing) {
if (existing.metadata?.commandSha256 !== digest || existing.status !== "succeeded") throw new Error("remote_workspace_provision_unverified");
return null;
}
const [created] = await tx.insert(workspaceOperations).values({ companyId: input.companyId, executionWorkspaceId: input.workspaceId,
heartbeatRunId: input.runId, issueId: workspace!.sourceIssueId, phase: "remote_workspace_provision", status: "running",
metadata: { epoch: input.epoch, index, commandSha256: digest, uncertain: true } }).returning();
return created!;
});
if (!operation) continue;
await input.assertOwned?.();
const result = await input.target.runner!.execute({ command: "bash", args: ["-lc", command], cwd: input.remoteCwd,
timeoutMs: 300_000, bypassSession: true });
if (result.exitCode !== 0 || result.timedOut) throw new Error("remote_workspace_provision_unverified");
await input.assertOwned?.();
await input.db.transaction(async tx => {
const [workspace] = await tx.select({ metadata: executionWorkspaces.metadata }).from(executionWorkspaces)
.where(and(eq(executionWorkspaces.companyId, input.companyId), eq(executionWorkspaces.id, input.workspaceId))).for("update");
const authority = readRemoteWorkspaceAuthority(workspace?.metadata);
if (authority?.epoch !== input.epoch || authority.writerRunId !== input.runId) throw new Error("remote_workspace_provision_fenced");
await tx.update(workspaceOperations).set({ status: "succeeded", finishedAt: new Date(), updatedAt: new Date(),
metadata: { ...operation.metadata, uncertain: false } }).where(eq(workspaceOperations.id, operation.id));
});
}
}