import path from "node:path"; import type { SshRemoteExecutionSpec } from "./ssh.js"; import { prepareCommandManagedRuntime, type CommandManagedRuntimeRunner, } from "./command-managed-runtime.js"; import { buildRemoteExecutionSessionIdentity, prepareRemoteManagedRuntime, remoteExecutionSessionMatches, type RemoteManagedRuntimeAsset, } from "./remote-managed-runtime.js"; import { createCommandManagedSandboxCallbackBridgeQueueClient, createSandboxCallbackBridgeAsset, createSandboxCallbackBridgeToken, DEFAULT_SANDBOX_CALLBACK_BRIDGE_MAX_BODY_BYTES, startSandboxCallbackBridgeServer, startSandboxCallbackBridgeWorker, } from "./sandbox-callback-bridge.js"; import { createSshCommandManagedRuntimeRunner, parseSshRemoteExecutionSpec, runSshCommand, shellQuote } from "./ssh.js"; import { ensureCommandResolvable, resolveCommandForLogs, runChildProcess, type RunProcessResult, type TerminalResultCleanupOptions, } from "./server-utils.js"; import { preferredShellForSandbox } from "./sandbox-shell.js"; export interface AdapterLocalExecutionTarget { kind: "local"; environmentId?: string | null; leaseId?: string | null; } export interface AdapterSshExecutionTarget { kind: "remote"; transport: "ssh"; environmentId?: string | null; leaseId?: string | null; remoteCwd: string; spec: SshRemoteExecutionSpec; } export interface AdapterSandboxExecutionTarget { kind: "remote"; transport: "sandbox"; providerKey?: string | null; shellCommand?: "bash" | "sh" | null; environmentId?: string | null; leaseId?: string | null; remoteCwd: string; timeoutMs?: number | null; runner?: CommandManagedRuntimeRunner; } export type AdapterExecutionTarget = | AdapterLocalExecutionTarget | AdapterSshExecutionTarget | AdapterSandboxExecutionTarget; export type AdapterRemoteExecutionSpec = SshRemoteExecutionSpec; export type AdapterManagedRuntimeAsset = RemoteManagedRuntimeAsset; export interface PreparedAdapterExecutionTargetRuntime { target: AdapterExecutionTarget; runtimeRootDir: string | null; assetDirs: Record; restoreWorkspace(): Promise; } export interface AdapterExecutionTargetProcessOptions { cwd: string; env: Record; stdin?: string; timeoutSec: number; graceSec: number; onLog: (stream: "stdout" | "stderr", chunk: string) => Promise; onSpawn?: (meta: { pid: number; processGroupId: number | null; startedAt: string }) => Promise; terminalResultCleanup?: TerminalResultCleanupOptions; } export interface AdapterExecutionTargetShellOptions { cwd: string; env: Record; timeoutSec?: number; graceSec?: number; onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise; } export interface AdapterExecutionTargetPaperclipBridgeHandle { env: Record; stop(): Promise; } 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 readStringMeta(parsed: Record, key: string): string | null { return readString(parsed[key]); } function resolveHostForUrl(rawHost: string): string { const host = rawHost.trim(); if (!host || host === "0.0.0.0" || host === "::") return "localhost"; if (host.includes(":") && !host.startsWith("[") && !host.endsWith("]")) return `[${host}]`; return host; } function resolveDefaultPaperclipApiUrl(): string { const runtimeHost = resolveHostForUrl( process.env.PAPERCLIP_LISTEN_HOST ?? process.env.HOST ?? "localhost", ); // 3100 matches the default Paperclip dev server port when the runtime does not provide one. const runtimePort = process.env.PAPERCLIP_LISTEN_PORT ?? process.env.PORT ?? "3100"; return `http://${runtimeHost}:${runtimePort}`; } function isBridgeDebugEnabled(env: NodeJS.ProcessEnv): boolean { const value = env.PAPERCLIP_BRIDGE_DEBUG?.trim().toLowerCase(); return value === "1" || value === "true" || value === "yes"; } function isAdapterExecutionTargetInstance(value: unknown): value is AdapterExecutionTarget { const parsed = parseObject(value); if (parsed.kind === "local") return true; if (parsed.kind !== "remote") return false; if (parsed.transport === "ssh") return parseSshRemoteExecutionSpec(parseObject(parsed.spec)) !== null; if (parsed.transport !== "sandbox") return false; return readStringMeta(parsed, "remoteCwd") !== null; } export function adapterExecutionTargetToRemoteSpec( target: AdapterExecutionTarget | null | undefined, ): AdapterRemoteExecutionSpec | null { return target?.kind === "remote" && target.transport === "ssh" ? target.spec : null; } export function adapterExecutionTargetIsRemote( target: AdapterExecutionTarget | null | undefined, ): boolean { return target?.kind === "remote"; } export function adapterExecutionTargetUsesManagedHome( target: AdapterExecutionTarget | null | undefined, ): boolean { return target?.kind === "remote" && target.transport === "sandbox"; } export function adapterExecutionTargetRemoteCwd( target: AdapterExecutionTarget | null | undefined, localCwd: string, ): string { return target?.kind === "remote" ? target.remoteCwd : localCwd; } export function resolveAdapterExecutionTargetCwd( target: AdapterExecutionTarget | null | undefined, configuredCwd: string | null | undefined, localFallbackCwd: string, ): string { if (typeof configuredCwd === "string" && configuredCwd.trim().length > 0) { return configuredCwd; } return adapterExecutionTargetRemoteCwd(target, localFallbackCwd); } export function adapterExecutionTargetUsesPaperclipBridge( target: AdapterExecutionTarget | null | undefined, ): boolean { return target?.kind === "remote"; } export function describeAdapterExecutionTarget( target: AdapterExecutionTarget | null | undefined, ): string { if (!target || target.kind === "local") return "local environment"; if (target.transport === "ssh") { return `SSH environment ${target.spec.username}@${target.spec.host}:${target.spec.port}`; } return `sandbox environment${target.providerKey ? ` (${target.providerKey})` : ""}`; } function requireSandboxRunner(target: AdapterSandboxExecutionTarget): CommandManagedRuntimeRunner { if (target.runner) return target.runner; throw new Error( "Sandbox execution target is missing its provider runtime runner. Sandbox commands must execute through the environment runtime.", ); } function preferredSandboxShell(target: AdapterSandboxExecutionTarget): "bash" | "sh" { return preferredShellForSandbox(target.shellCommand); } type AdapterCommandCapableExecutionTarget = AdapterSshExecutionTarget | AdapterSandboxExecutionTarget; function adapterExecutionTargetCommandRunner(target: AdapterCommandCapableExecutionTarget): CommandManagedRuntimeRunner { if (target.transport === "ssh") { return createSshCommandManagedRuntimeRunner({ spec: target.spec, defaultCwd: target.remoteCwd, maxBufferBytes: DEFAULT_SANDBOX_CALLBACK_BRIDGE_MAX_BODY_BYTES * 4, }); } return requireSandboxRunner(target); } function adapterExecutionTargetShellCommand(target: AdapterCommandCapableExecutionTarget): "bash" | "sh" { return target.transport === "ssh" ? "sh" : preferredSandboxShell(target); } function adapterExecutionTargetTimeoutMs( target: AdapterCommandCapableExecutionTarget, ): number | null | undefined { return target.transport === "sandbox" ? target.timeoutMs : undefined; } export async function ensureAdapterExecutionTargetCommandResolvable( command: string, target: AdapterExecutionTarget | null | undefined, cwd: string, env: NodeJS.ProcessEnv, ) { if (target?.kind === "remote" && target.transport === "sandbox") { return; } await ensureCommandResolvable(command, cwd, env, { remoteExecution: adapterExecutionTargetToRemoteSpec(target), }); } export async function resolveAdapterExecutionTargetCommandForLogs( command: string, target: AdapterExecutionTarget | null | undefined, cwd: string, env: NodeJS.ProcessEnv, ): Promise { if (target?.kind === "remote" && target.transport === "sandbox") { return `sandbox://${target.providerKey ?? "provider"}/${target.leaseId ?? "lease"}/${target.remoteCwd} :: ${command}`; } return await resolveCommandForLogs(command, cwd, env, { remoteExecution: adapterExecutionTargetToRemoteSpec(target), }); } export async function runAdapterExecutionTargetProcess( runId: string, target: AdapterExecutionTarget | null | undefined, command: string, args: string[], options: AdapterExecutionTargetProcessOptions, ): Promise { if (target?.kind === "remote" && target.transport === "sandbox") { const runner = requireSandboxRunner(target); return await runner.execute({ command, args, cwd: target.remoteCwd, env: options.env, stdin: options.stdin, timeoutMs: options.timeoutSec > 0 ? options.timeoutSec * 1000 : target.timeoutMs ?? undefined, onLog: options.onLog, onSpawn: options.onSpawn ? async (meta) => options.onSpawn?.({ ...meta, processGroupId: null }) : undefined, }); } return await runChildProcess(runId, command, args, { cwd: options.cwd, env: options.env, stdin: options.stdin, timeoutSec: options.timeoutSec, graceSec: options.graceSec, onLog: options.onLog, onSpawn: options.onSpawn, terminalResultCleanup: options.terminalResultCleanup, remoteExecution: adapterExecutionTargetToRemoteSpec(target), }); } export async function runAdapterExecutionTargetShellCommand( runId: string, target: AdapterExecutionTarget | null | undefined, command: string, options: AdapterExecutionTargetShellOptions, ): Promise { const onLog = options.onLog ?? (async () => {}); if (target?.kind === "remote") { const startedAt = new Date().toISOString(); if (target.transport === "ssh") { try { const result = await runSshCommand(target.spec, `sh -lc ${shellQuote(command)}`, { timeoutMs: (options.timeoutSec ?? 15) * 1000, }); if (result.stdout) await onLog("stdout", result.stdout); if (result.stderr) await onLog("stderr", result.stderr); return { exitCode: 0, signal: null, timedOut: false, stdout: result.stdout, stderr: result.stderr, pid: null, startedAt, }; } catch (error) { const timedOutError = error as NodeJS.ErrnoException & { stdout?: string; stderr?: string; signal?: string | null; }; const stdout = timedOutError.stdout ?? ""; const stderr = timedOutError.stderr ?? ""; if (typeof timedOutError.code === "number") { if (stdout) await onLog("stdout", stdout); if (stderr) await onLog("stderr", stderr); return { exitCode: timedOutError.code, signal: timedOutError.signal ?? null, timedOut: false, stdout, stderr, pid: null, startedAt, }; } if (timedOutError.code !== "ETIMEDOUT") { throw error; } if (stdout) await onLog("stdout", stdout); if (stderr) await onLog("stderr", stderr); return { exitCode: null, signal: timedOutError.signal ?? null, timedOut: true, stdout, stderr, pid: null, startedAt, }; } } const shellCommand = preferredSandboxShell(target); return await requireSandboxRunner(target).execute({ command: shellCommand, args: ["-lc", command], cwd: target.remoteCwd, env: options.env, timeoutMs: (options.timeoutSec ?? 15) * 1000, onLog, }); } return await runAdapterExecutionTargetProcess( runId, target, "sh", ["-lc", command], { cwd: options.cwd, env: options.env, timeoutSec: options.timeoutSec ?? 15, graceSec: options.graceSec ?? 5, onLog, }, ); } export async function readAdapterExecutionTargetHomeDir( runId: string, target: AdapterExecutionTarget | null | undefined, options: AdapterExecutionTargetShellOptions, ): Promise { const result = await runAdapterExecutionTargetShellCommand( runId, target, 'printf %s "$HOME"', options, ); const homeDir = result.stdout.trim(); return homeDir.length > 0 ? homeDir : null; } export async function ensureAdapterExecutionTargetFile( runId: string, target: AdapterExecutionTarget | null | undefined, filePath: string, options: AdapterExecutionTargetShellOptions, ): Promise { await runAdapterExecutionTargetShellCommand( runId, target, `mkdir -p ${shellQuote(path.posix.dirname(filePath))} && : > ${shellQuote(filePath)}`, options, ); } /** * Ensure a working directory exists (and is a directory) on the execution target. * * For local targets this delegates to the local `ensureAbsoluteDirectory` helper * (Node fs). For remote (SSH/sandbox) targets it shells out and runs * `mkdir -p` (when allowed) followed by a `[ -d ]` check so the result reflects * the directory state inside the environment, not on the Paperclip host. * * Throws an Error with a human-readable message on failure. */ export async function ensureAdapterExecutionTargetDirectory( runId: string, target: AdapterExecutionTarget | null | undefined, cwd: string, options: AdapterExecutionTargetShellOptions & { createIfMissing?: boolean }, ): Promise { const createIfMissing = options.createIfMissing ?? false; if (!target || target.kind === "local") { const { ensureAbsoluteDirectory } = await import("./server-utils.js"); await ensureAbsoluteDirectory(cwd, { createIfMissing }); return; } // Remote (SSH or sandbox): both expect POSIX absolute paths inside the env. if (!cwd.startsWith("/")) { throw new Error(`Working directory must be an absolute POSIX path on the remote target: "${cwd}"`); } const quoted = shellQuote(cwd); const script = createIfMissing ? `mkdir -p ${quoted} && [ -d ${quoted} ]` : `[ -d ${quoted} ]`; const result = await runAdapterExecutionTargetShellCommand(runId, target, script, { cwd: target.kind === "remote" ? target.remoteCwd : cwd, env: options.env, timeoutSec: options.timeoutSec ?? 15, graceSec: options.graceSec ?? 5, onLog: options.onLog, }); if (result.timedOut) { throw new Error(`Timed out checking working directory on remote target: "${cwd}"`); } if ((result.exitCode ?? 1) !== 0) { const detail = (result.stderr || result.stdout || "").trim(); if (createIfMissing) { throw new Error( `Could not create working directory "${cwd}" on remote target${detail ? `: ${detail}` : "."}`, ); } throw new Error( `Working directory does not exist on remote target: "${cwd}"${detail ? ` (${detail})` : ""}`, ); } } export function adapterExecutionTargetSessionIdentity( target: AdapterExecutionTarget | null | undefined, ): Record | null { if (!target || target.kind === "local") return null; if (target.transport === "ssh") return buildRemoteExecutionSessionIdentity(target.spec); return { transport: "sandbox", providerKey: target.providerKey ?? null, environmentId: target.environmentId ?? null, leaseId: target.leaseId ?? null, remoteCwd: target.remoteCwd, }; } export function adapterExecutionTargetSessionMatches( saved: unknown, target: AdapterExecutionTarget | null | undefined, ): boolean { if (!target || target.kind === "local") { return Object.keys(parseObject(saved)).length === 0; } if (target.transport === "ssh") return remoteExecutionSessionMatches(saved, target.spec); const current = adapterExecutionTargetSessionIdentity(target); const parsedSaved = parseObject(saved); return ( readStringMeta(parsedSaved, "transport") === current?.transport && readStringMeta(parsedSaved, "providerKey") === current?.providerKey && readStringMeta(parsedSaved, "environmentId") === current?.environmentId && readStringMeta(parsedSaved, "leaseId") === current?.leaseId && readStringMeta(parsedSaved, "remoteCwd") === current?.remoteCwd ); } export function parseAdapterExecutionTarget(value: unknown): AdapterExecutionTarget | null { const parsed = parseObject(value); const kind = readStringMeta(parsed, "kind"); if (kind === "local") { return { kind: "local", environmentId: readStringMeta(parsed, "environmentId"), leaseId: readStringMeta(parsed, "leaseId"), }; } if (kind === "remote" && readStringMeta(parsed, "transport") === "ssh") { const spec = parseSshRemoteExecutionSpec(parseObject(parsed.spec)); if (!spec) return null; return { kind: "remote", transport: "ssh", environmentId: readStringMeta(parsed, "environmentId"), leaseId: readStringMeta(parsed, "leaseId"), remoteCwd: spec.remoteCwd, spec, }; } if (kind === "remote" && readStringMeta(parsed, "transport") === "sandbox") { const remoteCwd = readStringMeta(parsed, "remoteCwd"); if (!remoteCwd) return null; return { kind: "remote", transport: "sandbox", providerKey: readStringMeta(parsed, "providerKey"), environmentId: readStringMeta(parsed, "environmentId"), leaseId: readStringMeta(parsed, "leaseId"), remoteCwd, timeoutMs: typeof parsed.timeoutMs === "number" ? parsed.timeoutMs : null, }; } return null; } export function adapterExecutionTargetFromRemoteExecution( remoteExecution: unknown, metadata: Pick = {}, ): AdapterExecutionTarget | null { const parsed = parseObject(remoteExecution); const ssh = parseSshRemoteExecutionSpec(parsed); if (ssh) { return { kind: "remote", transport: "ssh", environmentId: metadata.environmentId ?? null, leaseId: metadata.leaseId ?? null, remoteCwd: ssh.remoteCwd, spec: ssh, }; } return null; } export function readAdapterExecutionTarget(input: { executionTarget?: unknown; legacyRemoteExecution?: unknown; }): AdapterExecutionTarget | null { if (isAdapterExecutionTargetInstance(input.executionTarget)) { return input.executionTarget; } return ( parseAdapterExecutionTarget(input.executionTarget) ?? adapterExecutionTargetFromRemoteExecution(input.legacyRemoteExecution) ); } export async function prepareAdapterExecutionTargetRuntime(input: { target: AdapterExecutionTarget | null | undefined; adapterKey: string; workspaceLocalDir: string; workspaceExclude?: string[]; preserveAbsentOnRestore?: string[]; assets?: AdapterManagedRuntimeAsset[]; installCommand?: string | null; }): Promise { const target = input.target ?? { kind: "local" as const }; if (target.kind === "local") { return { target, runtimeRootDir: null, assetDirs: {}, restoreWorkspace: async () => {}, }; } if (target.transport === "ssh") { const prepared = await prepareRemoteManagedRuntime({ spec: target.spec, adapterKey: input.adapterKey, workspaceLocalDir: input.workspaceLocalDir, assets: input.assets, }); return { target, runtimeRootDir: prepared.runtimeRootDir, assetDirs: prepared.assetDirs, restoreWorkspace: prepared.restoreWorkspace, }; } const prepared = await prepareCommandManagedRuntime({ runner: requireSandboxRunner(target), spec: { providerKey: target.providerKey, shellCommand: target.shellCommand, leaseId: target.leaseId, remoteCwd: target.remoteCwd, timeoutMs: target.timeoutMs, }, adapterKey: input.adapterKey, workspaceLocalDir: input.workspaceLocalDir, workspaceExclude: input.workspaceExclude, preserveAbsentOnRestore: input.preserveAbsentOnRestore, assets: input.assets, installCommand: input.installCommand, }); return { target, runtimeRootDir: prepared.runtimeRootDir, assetDirs: prepared.assetDirs, restoreWorkspace: prepared.restoreWorkspace, }; } export function runtimeAssetDir( prepared: Pick, key: string, fallbackRemoteCwd: string, ): string { return prepared.assetDirs[key] ?? path.posix.join(fallbackRemoteCwd, ".paperclip-runtime", key); } function buildBridgeResponseHeaders(response: Response): Record { const out: Record = {}; for (const key of ["content-type", "etag", "last-modified"]) { const value = response.headers.get(key); if (value && value.trim().length > 0) out[key] = value.trim(); } return out; } function buildBridgeForwardUrl(baseUrl: string, request: { path: string; query: string }): URL { const url = new URL(request.path, baseUrl); const query = request.query.trim(); url.search = query.startsWith("?") ? query.slice(1) : query; return url; } function bridgeResponseBodyLimitError(maxBodyBytes: number): Error { return new Error(`Bridge response body exceeded the configured size limit of ${maxBodyBytes} bytes.`); } async function readBridgeForwardResponseBody(response: Response, maxBodyBytes: number): Promise { const rawContentLength = response.headers.get("content-length"); if (rawContentLength) { const contentLength = Number.parseInt(rawContentLength, 10); if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) { throw bridgeResponseBodyLimitError(maxBodyBytes); } } if (!response.body) { return ""; } const reader = response.body.getReader(); const chunks: Buffer[] = []; let totalBytes = 0; while (true) { const { done, value } = await reader.read(); if (done) break; if (!value) continue; totalBytes += value.byteLength; if (totalBytes > maxBodyBytes) { await reader.cancel().catch(() => undefined); throw bridgeResponseBodyLimitError(maxBodyBytes); } chunks.push(Buffer.from(value)); } return Buffer.concat(chunks, totalBytes).toString("utf8"); } export async function startAdapterExecutionTargetPaperclipBridge(input: { runId: string; target: AdapterExecutionTarget | null | undefined; runtimeRootDir: string | null | undefined; adapterKey: string; hostApiToken: string | null | undefined; hostApiUrl?: string | null; onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise; maxBodyBytes?: number | null; }): Promise { if (!adapterExecutionTargetUsesPaperclipBridge(input.target)) { return null; } if (!input.target || input.target.kind !== "remote") { return null; } const target = input.target; const onLog = input.onLog ?? (async () => {}); const hostApiToken = input.hostApiToken?.trim() ?? ""; if (hostApiToken.length === 0) { throw new Error("Sandbox bridge mode requires a host-side Paperclip API token."); } const runtimeRootDir = input.runtimeRootDir?.trim().length ? input.runtimeRootDir.trim() : path.posix.join(target.remoteCwd, ".paperclip-runtime", input.adapterKey); const bridgeRuntimeDir = path.posix.join(runtimeRootDir, "paperclip-bridge"); const queueDir = path.posix.join(bridgeRuntimeDir, "queue"); const assetRemoteDir = path.posix.join(bridgeRuntimeDir, "server"); const bridgeToken = createSandboxCallbackBridgeToken(); const maxBodyBytes = typeof input.maxBodyBytes === "number" && Number.isFinite(input.maxBodyBytes) && input.maxBodyBytes > 0 ? Math.trunc(input.maxBodyBytes) : DEFAULT_SANDBOX_CALLBACK_BRIDGE_MAX_BODY_BYTES; const hostApiUrl = input.hostApiUrl?.trim() || process.env.PAPERCLIP_RUNTIME_API_URL?.trim() || process.env.PAPERCLIP_API_URL?.trim() || resolveDefaultPaperclipApiUrl(); const shellCommand = adapterExecutionTargetShellCommand(target); const runner = adapterExecutionTargetCommandRunner(target); await onLog( "stdout", `[paperclip] Starting sandbox callback bridge for ${input.adapterKey} in ${bridgeRuntimeDir}.\n`, ); const bridgeAsset = await createSandboxCallbackBridgeAsset(); let server: Awaited> | null = null; let worker: Awaited> | null = null; try { const client = createCommandManagedSandboxCallbackBridgeQueueClient({ runner, remoteCwd: target.remoteCwd, timeoutMs: adapterExecutionTargetTimeoutMs(target), shellCommand, }); // PAPERCLIP_BRIDGE_DEBUG opts into verbose stdout logs of every bridge // proxy request/response. The query string is logged verbatim, so callers // who pass auth tokens or other sensitive values as query parameters // should be aware those values appear in the host process's stdout when // this flag is enabled. Only intended for active debugging in trusted // environments. const bridgeDebugEnabled = isBridgeDebugEnabled(process.env); worker = await startSandboxCallbackBridgeWorker({ client, queueDir, maxBodyBytes, handleRequest: async (request) => { const method = request.method.trim().toUpperCase() || "GET"; if (bridgeDebugEnabled) { await onLog( "stdout", `[paperclip] Bridge proxy ${method} ${request.path}${request.query ? `?${request.query}` : ""}\n`, ); } const headers = new Headers(); for (const [key, value] of Object.entries(request.headers)) { if (value.trim().length === 0) continue; headers.set(key, value); } headers.set("authorization", `Bearer ${hostApiToken}`); headers.set("x-paperclip-run-id", input.runId); const response = await fetch(buildBridgeForwardUrl(hostApiUrl, request), { method, headers, ...(method === "GET" || method === "HEAD" ? {} : { body: request.body }), signal: AbortSignal.timeout(30_000), }); if (bridgeDebugEnabled) { await onLog( "stdout", `[paperclip] Bridge proxy response ${response.status} for ${method} ${request.path}${request.query ? `?${request.query}` : ""}\n`, ); } return { status: response.status, headers: buildBridgeResponseHeaders(response), body: await readBridgeForwardResponseBody(response, maxBodyBytes), }; }, }); server = await startSandboxCallbackBridgeServer({ runner, remoteCwd: target.remoteCwd, assetRemoteDir, queueDir, bridgeToken, bridgeAsset, timeoutMs: adapterExecutionTargetTimeoutMs(target), maxBodyBytes, shellCommand, }); } catch (error) { await Promise.allSettled([ server?.stop(), worker?.stop(), bridgeAsset.cleanup(), ]); throw error; } return { env: { PAPERCLIP_API_URL: server.baseUrl, PAPERCLIP_API_KEY: bridgeToken, PAPERCLIP_API_BRIDGE_MODE: "queue_v1", }, stop: async () => { await Promise.allSettled([ server?.stop(), ]); await Promise.allSettled([ worker?.stop(), bridgeAsset.cleanup(), ]); }, }; }