import type { Environment, EnvironmentLease, ExecutionWorkspaceConfig, WorkspaceRealizationRecord, WorkspaceRealizationRequest, } from "@paperclipai/shared"; import type { RealizedExecutionWorkspace } from "./workspace-runtime.js"; import { ENVIRONMENT_DRIVER_TRAITS, getEnvironmentDriverTraits } from "./environment-driver-traits.js"; function parseObject(value: unknown): Record { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; } function readString(value: unknown): string | null { return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; } function readNumber(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; } function readStringArray(value: unknown): string[] { return Array.isArray(value) ? value.map(readString).filter((entry): entry is string => entry !== null) : []; } function readPathAliases(value: unknown): Array<{ path: string; target: string }> { if (!Array.isArray(value)) return []; return value.flatMap((entry) => { const parsed = parseObject(entry); const aliasPath = readString(parsed.path); const target = readString(parsed.target); return aliasPath && target ? [{ path: aliasPath, target }] : []; }); } // Read the additional referenced (mentioned) project sources. Legacy payloads omit the field, so // this defaults to an empty array. Each source needs a localPath; entries without one are dropped. function readAdditionalSources( value: unknown, ): NonNullable { if (!Array.isArray(value)) return []; return value.flatMap((entry) => { const parsed = parseObject(entry); const localPath = readString(parsed.localPath); if (!localPath) return []; return [ { localPath, projectId: readString(parsed.projectId), projectWorkspaceId: readString(parsed.projectWorkspaceId), repoUrl: readString(parsed.repoUrl), repoRef: readString(parsed.repoRef), }, ]; }); } export function readWorkspaceRealizationRequest(value: unknown): WorkspaceRealizationRequest | null { const parsed = parseObject(value); if (parsed.version !== 1) return null; const source = parseObject(parsed.source); const runtimeOverlay = parseObject(parsed.runtimeOverlay); const localPath = readString(source.localPath); const companyId = readString(parsed.companyId); const environmentId = readString(parsed.environmentId); const heartbeatRunId = readString(parsed.heartbeatRunId); const adapterType = readString(parsed.adapterType); if (!localPath || !companyId || !environmentId || !heartbeatRunId || !adapterType) return null; return { version: 1, adapterType, companyId, environmentId, executionWorkspaceId: readString(parsed.executionWorkspaceId), issueId: readString(parsed.issueId), heartbeatRunId, requestedMode: readString(parsed.requestedMode), source: { kind: source.kind === "task_session" || source.kind === "agent_home" ? source.kind : "project_primary", localPath, projectId: readString(source.projectId), projectWorkspaceId: readString(source.projectWorkspaceId), repoUrl: readString(source.repoUrl), repoRef: readString(source.repoRef), strategy: source.strategy === "git_worktree" ? "git_worktree" : "project_primary", branchName: readString(source.branchName), worktreePath: readString(source.worktreePath), }, additionalSources: readAdditionalSources(parsed.additionalSources), runtimeOverlay: { provisionCommand: readString(runtimeOverlay.provisionCommand), runtimeProvisionCommand: readString(runtimeOverlay.runtimeProvisionCommand), teardownCommand: readString(runtimeOverlay.teardownCommand), cleanupCommand: readString(runtimeOverlay.cleanupCommand), workspaceRuntime: Object.keys(parseObject(runtimeOverlay.workspaceRuntime)).length > 0 ? parseObject(runtimeOverlay.workspaceRuntime) : null, }, }; } export function buildWorkspaceRealizationRequest(input: { adapterType: string; companyId: string; environmentId: string; executionWorkspaceId: string | null; issueId: string | null; heartbeatRunId: string; requestedMode: string | null; workspace: RealizedExecutionWorkspace; workspaceConfig: ExecutionWorkspaceConfig | null; }): WorkspaceRealizationRequest { return { version: 1, adapterType: input.adapterType, companyId: input.companyId, environmentId: input.environmentId, executionWorkspaceId: input.executionWorkspaceId, issueId: input.issueId, heartbeatRunId: input.heartbeatRunId, requestedMode: input.requestedMode, source: { kind: input.workspace.source, localPath: input.workspace.cwd, projectId: input.workspace.projectId, projectWorkspaceId: input.workspace.workspaceId, repoUrl: input.workspace.repoUrl, repoRef: input.workspace.repoRef, strategy: input.workspace.strategy, branchName: input.workspace.branchName, worktreePath: input.workspace.worktreePath, }, // The additional (referenced) sources carry the read-only referenced-project workspaces. Run // preparation resolves them for a local execution target only and exposes each local path to // the agent through the workspace-hints channel (`PAPERCLIP_WORKSPACES_JSON`). A remote target // never receives a referenced source: run preparation skips referenced-project resolution on a // remote target, so this array is empty there. The `sync` block below therefore realizes only // the anchor source; a remote-transport sync of the referenced trees is not implemented yet. additionalSources: (input.workspace.additionalWorkspaces ?? []).map((additional) => ({ localPath: additional.cwd, projectId: additional.projectId, projectWorkspaceId: additional.workspaceId, repoUrl: additional.repoUrl, repoRef: additional.repoRef, })), runtimeOverlay: { provisionCommand: input.workspaceConfig?.provisionCommand ?? null, runtimeProvisionCommand: input.workspaceConfig?.runtimeProvisionCommand ?? null, teardownCommand: input.workspaceConfig?.teardownCommand ?? null, cleanupCommand: input.workspaceConfig?.cleanupCommand ?? null, workspaceRuntime: input.workspaceConfig?.workspaceRuntime ?? null, }, }; } export function buildWorkspaceRealizationRecord(input: { environment: Environment; lease: EnvironmentLease; request: WorkspaceRealizationRequest; realizedCwd?: string | null; providerMetadata?: Record | null; }): WorkspaceRealizationRecord { const leaseMetadata = input.lease.metadata ?? {}; const providerMetadata = input.providerMetadata ?? {}; // An unknown or absent driver reads the "local" row, same as today's fallback. const traits = getEnvironmentDriverTraits(input.environment.driver) ?? ENVIRONMENT_DRIVER_TRAITS.local; const transport = traits.driver; const remotePath = readString(providerMetadata.remoteCwd) ?? readString(leaseMetadata.remoteCwd) ?? readString(providerMetadata.remotePath) ?? null; const host = readString(leaseMetadata.host); const port = readNumber(leaseMetadata.port); const username = readString(leaseMetadata.username); const sandboxId = readString(leaseMetadata.sandboxId) ?? readString(providerMetadata.sandboxId); const realizationMetadata = { ...parseObject(leaseMetadata.workspaceRealization), ...parseObject(providerMetadata.workspaceRealization), ...providerMetadata, }; const mode = realizationMetadata.mode === "in_place" || realizationMetadata.realizationMode === "in_place" ? "in_place" as const : "copy" as const; const authoritativeRoot = readString(realizationMetadata.authoritativeRoot) ?? (mode === "in_place" ? remotePath : null) ?? input.request.source.localPath; const pathAliases = readPathAliases(realizationMetadata.pathAliases ?? realizationMetadata.workspaceAliases); const outboundRestorePaths = readStringArray(realizationMetadata.outboundRestorePaths); const provider = input.lease.provider ?? (transport === "ssh" ? "ssh" : transport === "local" ? "local" : null); const localPath = input.request.source.localPath; const summary = transport === "local" ? `Local workspace realized at ${localPath}.` : transport === "ssh" ? `SSH workspace realized at ${username ?? "user"}@${host ?? "host"}:${port ?? 22}:${remotePath ?? input.request.source.localPath}.` : transport === "sandbox" ? `Sandbox workspace realized at ${remotePath ?? "/"}${sandboxId ? ` in ${sandboxId}` : ""}.` : `Plugin workspace realized at ${input.realizedCwd ?? remotePath ?? localPath}.`; return { version: 1, mode, authoritativeRoot, pathAliases, outboundRestorePaths, provider, environmentId: input.environment.id, leaseId: input.lease.id, providerLeaseId: input.lease.providerLeaseId, local: { path: localPath, source: input.request.source.kind, strategy: input.request.source.strategy, projectId: input.request.source.projectId, projectWorkspaceId: input.request.source.projectWorkspaceId, repoUrl: input.request.source.repoUrl, repoRef: input.request.source.repoRef, branchName: input.request.source.branchName, worktreePath: input.request.source.worktreePath, }, additional: (input.request.additionalSources ?? []).map((additional) => ({ path: additional.localPath, projectId: additional.projectId, projectWorkspaceId: additional.projectWorkspaceId, repoUrl: additional.repoUrl, repoRef: additional.repoRef, })), remote: { path: remotePath, ...(host ? { host } : {}), ...(port ? { port } : {}), ...(username ? { username } : {}), ...(sandboxId ? { sandboxId } : {}), }, bootstrap: { command: input.request.runtimeOverlay.provisionCommand, }, rebuild: { executionWorkspaceId: input.request.executionWorkspaceId, mode: input.request.requestedMode, repoUrl: input.request.source.repoUrl, repoRef: input.request.source.repoRef, localPath, remotePath, providerLeaseId: input.lease.providerLeaseId, metadata: { source: input.request.source, runtimeOverlay: input.request.runtimeOverlay, environmentDriver: input.environment.driver, provider, providerMetadata, }, }, summary, }; } /** * Build the workspace-realization record from the run request. The server owns the record; * a driver realize handler (built-in or plugin) returns only a realized cwd and provider * metadata. Every `realizeWorkspace` exit must route through this helper, so the record carries * the referenced (mentioned) project sources in `additional`. The adapter reads `additional` to * stage each referenced tree into the target; a realize exit that returns a raw provider result * without this helper drops the mentioned projects. */ export function buildWorkspaceRealizationRecordFromDriverInput(input: { environment: Environment; lease: EnvironmentLease; workspace: { localPath?: string; remotePath?: string; mode?: string; metadata?: Record; }; cwd?: string | null; providerMetadata?: Record | null; }): WorkspaceRealizationRecord { const request = readWorkspaceRealizationRequest(input.workspace.metadata?.workspaceRealizationRequest) ?? readWorkspaceRealizationRequest(input.workspace.metadata?.request) ?? buildWorkspaceRealizationRequest({ adapterType: "unknown", companyId: input.lease.companyId, environmentId: input.environment.id, executionWorkspaceId: input.lease.executionWorkspaceId, issueId: input.lease.issueId, heartbeatRunId: input.lease.heartbeatRunId ?? "unknown", requestedMode: input.workspace.mode ?? null, workspace: { baseCwd: input.workspace.localPath ?? input.cwd ?? input.workspace.remotePath ?? "/", source: "task_session", projectId: null, workspaceId: null, repoUrl: null, repoRef: null, strategy: "project_primary", cwd: input.workspace.localPath ?? input.cwd ?? input.workspace.remotePath ?? "/", branchName: null, worktreePath: null, warnings: [], created: false, branchCreatedByRuntime: false, }, workspaceConfig: null, }); return buildWorkspaceRealizationRecord({ environment: input.environment, lease: input.lease, request, realizedCwd: input.cwd ?? null, providerMetadata: input.providerMetadata, }); }