UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

891 lines (890 loc) 35 kB
import { a as addTimerTimeoutGraceMs } from "./number-coercion-CLj0HTDM.js"; import { s as sleepWithAbort } from "./src-BQ327IOM.js"; import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js"; import { t as createDeferredCore } from "./deferred-D0La5CRk.js"; import { a as formatNodeRunnerUpdateRequired, n as NODE_RUNNER_UPDATE_REQUIRED_ISSUE } from "./node-runner-inventory-CsPbvshW.js"; import { r as runCommandWithTimeout } from "./exec-BIE-3oLG.js"; import { D as NODE_WORKER_WORKSPACE_EXEC_COMMAND, x as NODE_WORKER_ENVIRONMENT_STOP_COMMAND } from "./node-commands-BC8PhxqU.js"; import "./backoff-BkMI1WEL.js"; import { t as sameWorkerBuild } from "./worker-build-identity-D_c48Wx_.js"; import { a as parseNodeWorkerWorkspaceExecResult } from "./node-workspace-protocol-BIrX07V5.js"; import { r as NodeWorkerWorkspaceTransferError } from "./node-workspace-transfer-protocol-DhJOhHIM.js"; import { T as serializeWorkerWorkspaceManifest, _ as withWorkspaceHashMemo, l as pruneWorkspaceHashMemo, s as measureLocalWorkspaceReconciliation } from "./workspace-actual-manifest-BGixPzCq.js"; import { t as boundedWorkerError } from "./worker-error-BQ2GkQ1M.js"; import { a as recoverWorkerWorkspaceReconciliation, s as assertWorkspaceResultStable, t as applyStagedWorkerWorkspace } from "./workspace-reconcile-U3HlbfL4.js"; import { d as workerWorkspaceResultStaging } from "./workspace-result-staging-CwYxWnxD.js"; import { n as REMOTE_WORKSPACE_MANIFEST_JS } from "./workspace-sync-scripts-Dyx_KHcJ.js"; import { n as measureNodeWorkerLaunchBytes, r as raceNodeWorkerOperation } from "./node-launch-adapter-CYClo3Jk.js"; import { i as joinWorkerTunnelStops, r as WorkerTunnelOwnerDisconnectedError } from "./tunnel-contract-CB5bcNwX.js"; import { f as validateWorkspaceSyncRequest, l as resolveWorkerWorkspaceGitAuthor, y as workspaceSyncError } from "./workspace-sync-helpers-DLkfl3bZ.js"; import { r as workerProjectSeedKey } from "./workspace-git-base-qAMhycVO.js"; import { t as nodeWorkerGatewayNamespace } from "./node-worker-gateway-namespace-kRiA3LFq.js"; import { n as runInstrumentedWorkspaceReconcile } from "./workspace-finalize-B4kcMRD7.js"; import { t as createWorkerWorkspaceQuiescence } from "./workspace-quiescence-DqfH4Upm.js"; import path from "node:path"; import fs from "node:fs/promises"; import os from "node:os"; import { createHash } from "node:crypto"; //#region src/gateway/worker-environments/node-worker-workspace-fallback.ts const GIT_TIMEOUT_MS = 6e4; const COMMIT_PATTERN = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u; const MANIFEST_REF_PATTERN = /^sha256:[a-f0-9]{64}$/u; const GIT_NONINTERACTIVE_ARGS = [ "-c", "credential.helper=", "-c", "core.askPass=" ]; const workspaceSyncLog = createSubsystemLogger("gateway/worker-workspace"); function recordNodeSyncPath(environmentId, sessionId, outcome, originStartedAt) { workspaceSyncLog.info("worker workspace sync path selected", { environmentId, sessionId, path: outcome.kind === "synced" ? "origin" : "gateway-push", reason: outcome.kind === "synced" ? outcome.seeded ? "published-origin-seeded" : "published-origin" : outcome.reason, originAttemptMs: performance.now() - originStartedAt }); } async function localGit(root, args) { const result = await runCommandWithTimeout([ "git", "-c", `core.hooksPath=${os.devNull}`, "-c", "core.fsmonitor=false", ...GIT_NONINTERACTIVE_ARGS, "-C", root, ...args ], { timeoutMs: GIT_TIMEOUT_MS, maxOutputBytes: 262144, maxCombinedOutputBytes: 524288, outputCapture: "head", baseEnv: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "", SSH_ASKPASS: "" } }); if (result.termination !== "exit" || result.code !== 0) throw new Error("local Git inspection failed"); return result.stdout.trim(); } function credentialFreeHttpOrigin(raw) { let parsed; try { parsed = new URL(raw); } catch { return; } return (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.username === "" && parsed.password === "" && parsed.search === "" && parsed.hash === "" ? parsed.href : void 0; } async function requiresWorkspaceTransfer(root) { for (const marker of [".worktreeinclude", ".gitmodules"]) try { await fs.lstat(path.join(root, marker)); return true; } catch (error) { if (error.code !== "ENOENT") throw error; } try { return /\bfilter\s*=\s*lfs\b/u.test(await fs.readFile(path.join(root, ".gitattributes"), "utf8")); } catch (error) { if (error.code !== "ENOENT") throw error; return false; } } async function inspectEligibleOrigin(localPath) { try { const canonicalPath = await fs.realpath(localPath); let root; try { root = await fs.realpath(await localGit(canonicalPath, ["rev-parse", "--show-toplevel"])); } catch { return { kind: "fallback", reason: "not-git-workspace" }; } if (root !== canonicalPath) return { kind: "fallback", reason: "not-repository-root" }; if (await requiresWorkspaceTransfer(root)) return { kind: "fallback", reason: "workspace-transfer-required" }; const [status, commit, rawOrigin] = await Promise.all([ localGit(root, [ "status", "--porcelain=v1", "--untracked-files=all" ]), localGit(root, ["rev-parse", "HEAD"]), localGit(root, [ "remote", "get-url", "origin" ]).catch(() => "") ]); if (status) return { kind: "fallback", reason: "workspace-dirty" }; const origin = credentialFreeHttpOrigin(rawOrigin); if (!COMMIT_PATTERN.test(commit) || !origin) return { kind: "fallback", reason: "origin-unavailable" }; return { kind: "eligible", identity: { commit, origin, root } }; } catch { return { kind: "fallback", reason: "inspection-failed" }; } } function succeeded(result) { return result.termination === "exit" && result.code === 0; } /** Optional published-origin fast path; HTTPS transfer remains the canonical fallback. */ function createNodeWorkerWorkspaceFallback(exec) { let supportsSeeds = true; let seedStoreFailureLogged = false; const capture = async (dir, base, reference) => await exec({ argv: [ "node", "-e", REMOTE_WORKSPACE_MANIFEST_JS, dir, ...base ? [base, "eligible"] : ["", "all"], ...reference ? [reference.slice(7)] : [] ], timeoutMs: GIT_TIMEOUT_MS, transportRetry: "idempotent" }); const checkoutAndCapture = async (identity, workspaceDir, expectedManifestRef, seeded) => { const checkedOut = await exec({ argv: [ "git", ...GIT_NONINTERACTIVE_ARGS, "checkout", "--detach", "--force", identity.commit ], timeoutMs: GIT_TIMEOUT_MS, transportRetry: "never" }); if (!succeeded(checkedOut) || checkedOut.workspaceDir !== workspaceDir) return { kind: "fallback", reason: "checkout-failed" }; const captured = await capture(checkedOut.workspaceDir, identity.commit); const manifestRef = captured.stdout.trim(); if (!succeeded(captured) || !MANIFEST_REF_PATTERN.test(manifestRef)) return { kind: "fallback", reason: "manifest-capture-failed" }; if (manifestRef !== expectedManifestRef) return { kind: "fallback", reason: "manifest-mismatch" }; return { kind: "synced", seeded, result: { mode: "git", remoteWorkspaceDir: checkedOut.workspaceDir, manifestRef } }; }; return { async captureManifest(dir, base, reference) { const captured = await capture(dir, base, reference); const manifestRef = captured.stdout.trim(); if (!succeeded(captured) || !MANIFEST_REF_PATTERN.test(manifestRef)) { const detail = boundedWorkerError(captured.stderr.trim() || (!succeeded(captured) ? `${captured.termination} (exit code ${captured.code}, signal ${captured.signal})` : "invalid manifest reference")); throw new Error(`Node workspace manifest capture failed: ${detail}`); } return manifestRef; }, async trySyncWorkspace(request, expectedManifestRef) { validateWorkspaceSyncRequest(request); const inspection = await inspectEligibleOrigin(request.localPath); if (inspection.kind === "fallback") return inspection; const { identity } = inspection; const seedKey = createHash("sha256").update(identity.origin).digest("hex"); let outcome; if (supportsSeeds) try { const applied = await exec({ argv: ["openclaw-internal-workspace-seed"], seed: { action: "apply", key: seedKey }, timeoutMs: GIT_TIMEOUT_MS, transportRetry: "never" }); if (succeeded(applied) && applied.stdout.trim() === "applied") { const remote = await exec({ argv: [ "git", ...GIT_NONINTERACTIVE_ARGS, "remote", "get-url", "origin" ], timeoutMs: GIT_TIMEOUT_MS, transportRetry: "never" }); if (!succeeded(remote) || remote.stdout.trim() !== identity.origin) throw new Error("Node workspace seed origin mismatch"); const fetched = await exec({ argv: [ "git", ...GIT_NONINTERACTIVE_ARGS, "fetch", "--prune", "origin", "+refs/heads/*:refs/remotes/origin/*" ], timeoutMs: GIT_TIMEOUT_MS, transportRetry: "never" }); if (!succeeded(fetched)) throw workspaceSyncError(fetched); outcome = await checkoutAndCapture(identity, applied.workspaceDir, expectedManifestRef, true); } } catch (error) { if (error instanceof Error && error.message.includes("INVALID_REQUEST")) supportsSeeds = false; else workspaceSyncLog.info("node worker workspace seeded sync failed; cloning", { error: boundedWorkerError(error) }); } if (outcome?.kind !== "synced") { const cloned = await exec({ argv: [ "git", ...GIT_NONINTERACTIVE_ARGS, "-c", "init.templateDir=", "clone", "--filter=blob:none", "--no-checkout", "--", identity.origin, "." ], resetWorkspace: true, timeoutMs: GIT_TIMEOUT_MS, transportRetry: "never" }); if (!succeeded(cloned)) return { kind: "fallback", reason: "clone-failed" }; outcome = await checkoutAndCapture(identity, cloned.workspaceDir, expectedManifestRef, false); } if (outcome.kind === "synced" && supportsSeeds) try { const stored = await exec({ argv: ["openclaw-internal-workspace-seed"], seed: { action: "store", key: seedKey, maxAgeMs: 216e5 }, timeoutMs: 18e4, transportRetry: "never" }); if (!succeeded(stored)) throw workspaceSyncError(stored); } catch (error) { if (error instanceof Error && error.message.includes("INVALID_REQUEST")) supportsSeeds = false; if (!seedStoreFailureLogged) { seedStoreFailureLogged = true; workspaceSyncLog.warn("node worker workspace seed store failed", { error: boundedWorkerError(error) }); } } return outcome; }, async finalizeSync(request, result) { if (result.mode === "plain") return result; const author = await resolveWorkerWorkspaceGitAuthor(request, async (argv) => runCommandWithTimeout(argv, { timeoutMs: GIT_TIMEOUT_MS, maxOutputBytes: 1024 })); const git = [ "git", "-C", result.remoteWorkspaceDir, "config", "--local" ]; for (const [key, value] of Object.entries(author)) { if (!value) continue; const configured = await exec({ argv: [ ...git, `user.${key}`, value ], transportRetry: "never" }); if (!succeeded(configured)) throw workspaceSyncError(configured); } return result; } }; } //#endregion //#region src/gateway/worker-environments/node-worker-workspace-actions.ts function createNodeWorkerWorkspaceActions(params) { const { restoredWorkspace } = params; let workspaceReady = restoredWorkspace !== void 0; const exec = async (command) => { if (!workspaceReady) throw new Error("node worker workspace is unavailable before sync"); return await params.runWorkspaceCommand(command); }; const workspace = createNodeWorkerWorkspaceFallback(exec); const quiesceWorkspace = createWorkerWorkspaceQuiescence({ ownerSignal: params.ownerSignal, sharedHost: true, runWorkspaceCommand: exec }); const validateRestoredWorkspace = async () => { if (!restoredWorkspace) return; const prepared = await params.workspaceTransfer.prepareSync({ environmentId: params.environmentId, ownerEpoch: params.ownerEpoch, sessionId: params.sessionId, generation: params.ownerEpoch, localPath: restoredWorkspace.localPath, isAuthorized: params.isOwnerCurrent, signal: params.ownerSignal }); params.workspaceTransfer.revoke(params.environmentId, prepared.token); }; const placementHashMemo = /* @__PURE__ */ new Map(); const reconcileWorkspace = (request) => runInstrumentedWorkspaceReconcile((metrics) => reconcileWorkspaceRun(request, metrics)); const reconcileWorkspaceRun = async (request, metrics) => { pruneWorkspaceHashMemo(placementHashMemo); const runLocalReconciliation = (operation) => measureLocalWorkspaceReconciliation(metrics, () => withWorkspaceHashMemo(placementHashMemo, operation, metrics.gateway)); const pending = request.journal.load(); if (pending) { await recoverWorkerWorkspaceReconciliation({ root: request.localPath, journal: pending }); request.journal.abort(); } const uploadToken = params.workspaceTransfer.prepareUpload(params.environmentId, request.baseManifestRef); let uploadedResult; try { uploadedResult = await exec({ argv: ["openclaw-internal-workspace-transfer"], transfer: { direction: "upload", token: uploadToken, baseManifestRef: request.baseManifestRef }, timeoutMs: 6e5, transportRetry: "never" }); } finally { params.workspaceTransfer.revoke(params.environmentId, uploadToken); } if (uploadedResult.termination !== "exit" || uploadedResult.code !== 0) throw new Error("Node workspace reconcile upload failed"); const uploaded = params.workspaceTransfer.takeUpload(params.environmentId, request.baseManifestRef); try { const changed = uploaded.currentManifestRef !== request.baseManifestRef; let expectedRemoteRef = uploaded.currentManifestRef; const verifyStable = async () => { if (await workspace.captureManifest(request.remoteWorkspaceDir, uploaded.base.baseCommit, expectedRemoteRef) !== expectedRemoteRef) throw new Error("Cloud workspace changed during final reconciliation"); }; await verifyStable(); const publishAcceptedManifest = async (accepted) => { if (accepted.manifestRef === expectedRemoteRef) return; const token = params.workspaceTransfer.publishSnapshot(params.environmentId, { manifest: accepted.manifest, manifestRef: accepted.manifestRef, rawManifest: serializeWorkerWorkspaceManifest(accepted.manifest), root: await fs.realpath(request.localPath) }); try { const published = await exec({ argv: ["openclaw-internal-workspace-transfer"], transfer: { direction: "download", token, manifestRef: accepted.manifestRef }, timeoutMs: 6e5, transportRetry: "never" }); if (published.termination !== "exit" || published.code !== 0 || published.stdout.trim() !== accepted.manifestRef) throw new Error("Node workspace accepted manifest publication failed"); expectedRemoteRef = accepted.manifestRef; } finally { params.workspaceTransfer.revoke(params.environmentId, token); } }; const preparedStagedResult = request.stagedResult ? await runLocalReconciliation(async () => await workerWorkspaceResultStaging.prepareRequestedWorkerWorkspaceResult({ request, stagingRoot: uploaded.stagingRoot, currentManifestRef: uploaded.currentManifestRef, baseManifestRaw: uploaded.baseRaw, currentManifestRaw: uploaded.currentRaw, publishAcceptedManifest })) : void 0; let appliedWorkspaceResult; if (!preparedStagedResult) appliedWorkspaceResult = await runLocalReconciliation(async () => await applyStagedWorkerWorkspace({ root: request.localPath, stagingRoot: uploaded.stagingRoot, baseManifestRef: request.baseManifestRef, currentManifestRef: uploaded.currentManifestRef, base: uploaded.base, current: uploaded.current, journal: request.journal, publishAcceptedManifest })); return { get manifestRef() { return expectedRemoteRef; }, changed, verifyStable, verifyLocalStable: async () => await runLocalReconciliation(async () => await (appliedWorkspaceResult?.verifyLocalStable() ?? assertWorkspaceResultStable({ root: request.localPath, base: uploaded.base, current: uploaded.current }))), getAppliedWorkspaceResult: () => appliedWorkspaceResult, ...preparedStagedResult ? { ...preparedStagedResult, applyPreparedStagedResult: async () => { await runLocalReconciliation(async () => await preparedStagedResult.applyPreparedStagedResult()); appliedWorkspaceResult = preparedStagedResult.getAppliedWorkspaceResult(); } } : {} }; } finally { await fs.rm(uploaded.stagingRoot, { recursive: true, force: true }); } }; return { validateRestoredWorkspace, runWorkspaceCommand: exec, stageAttachments: async (request) => { const prepared = await params.workspaceTransfer.prepareAttachments({ ...request, environmentId: params.environmentId }); try { const result = await exec({ argv: ["openclaw-internal-workspace-transfer"], transfer: { direction: "download", token: prepared.token, manifestRef: prepared.snapshot.manifestRef, attachments: true }, transportRetry: "never", assertCurrent: () => { if (!request.isAuthorized()) throw new Error("Worker attachment transfer authority closed"); }, signal: request.signal }); if (result.termination !== "exit" || result.code !== 0 || result.stdout.trim() !== prepared.snapshot.manifestRef) throw new Error("Worker attachment transfer failed"); } finally { params.workspaceTransfer.revoke(params.environmentId, prepared.token); } }, syncWorkspace: async (request) => { workspaceReady = true; try { const prepared = await params.workspaceTransfer.prepareSync({ environmentId: params.environmentId, ownerEpoch: params.ownerEpoch, sessionId: params.sessionId, generation: params.ownerEpoch, localPath: request.localPath, isAuthorized: params.isOwnerCurrent, signal: params.ownerSignal }); try { if (!request.projectKey) { const originStartedAt = performance.now(); const origin = await workspace.trySyncWorkspace(request, prepared.snapshot.manifestRef); recordNodeSyncPath(params.environmentId, params.sessionId, origin, originStartedAt); if (origin.kind === "synced") return await workspace.finalizeSync(request, origin.result); } const transferred = await exec({ argv: ["openclaw-internal-workspace-transfer"], transfer: { direction: "download", token: prepared.token, manifestRef: prepared.snapshot.manifestRef, ...request.projectKey && prepared.snapshot.manifest.baseCommit ? { seedKey: workerProjectSeedKey({ key: request.projectKey, baseCommit: prepared.snapshot.manifest.baseCommit }) } : {} }, timeoutMs: 6e5, transportRetry: "never" }); if (transferred.termination !== "exit" || transferred.code !== 0 || transferred.stdout.trim() !== prepared.snapshot.manifestRef) throw new Error("Node workspace transfer failed"); return await workspace.finalizeSync(request, { mode: prepared.snapshot.manifest.baseCommit ? "git" : "plain", remoteWorkspaceDir: transferred.workspaceDir, manifestRef: prepared.snapshot.manifestRef }); } finally { params.workspaceTransfer.revoke(params.environmentId, prepared.token); } } catch (error) { workspaceReady = restoredWorkspace !== void 0; throw error; } }, quiesceWorkspace, reconcileWorkspace }; } //#endregion //#region src/gateway/worker-environments/node-worker-tunnel.ts const DEFAULT_COMMAND_TIMEOUT_MS = 6e4; const COMMAND_RESULT_GRACE_MS = 5e3; const RETRY_DELAY_MS = 100; const tunnelLog = createSubsystemLogger("gateway/worker-tunnel"); const RETRYABLE_TRANSPORT_CODES = /* @__PURE__ */ new Set([ "DISCONNECTED", "NOT_CONNECTED", "PAIRING_CHANGED", "PRIVATE_DIALECT_UNAVAILABLE", "ROUTE_CHANGED", "TIMEOUT", "UNAVAILABLE" ]); function spawnResultFromReceipt(receipt) { if (receipt.state === "completed") return { stdout: receipt.resultJson, stderr: "", code: 0, signal: null, killed: false, termination: "exit" }; if (receipt.state === "failed" || receipt.state === "interrupted" || receipt.state === "cancelled") return { stdout: "", stderr: receipt.errorText, code: 1, signal: null, killed: receipt.state === "cancelled" || receipt.state === "interrupted", termination: "exit" }; throw new Error("node worker launch returned without a terminal receipt"); } function payloadJson(value) { if (!value) throw new Error("node workspace command omitted its result"); try { return JSON.parse(value); } catch { throw new Error("node workspace command returned malformed JSON"); } } /** Owns node-channel handles without treating the persistent machine as a disposable lease. */ function createNodeWorkerTunnelManager(options) { const entries = /* @__PURE__ */ new Map(); const retiredEntries = /* @__PURE__ */ new Set(); let resolveWorkspaceBinding; const gatewayNamespace = nodeWorkerGatewayNamespace(options.gatewayDeviceId); const hasDurableBinding = (entry) => { const current = options.getEnvironment(entry.environmentId); return Boolean(current && current.ownerEpoch === entry.ownerEpoch && current.bootstrapReceipt?.installKind === "bundle" && sameWorkerBuild(current.bootstrapReceipt, entry.expectedBuild) && current.attachedSessionIds.length <= 1 && (current.attachedSessionIds.length === 0 || current.attachedSessionIds[0] === entry.sessionId)); }; const isLiveEntry = (entry) => entries.get(entry.environmentId) === entry && !entry.abortController.signal.aborted; const isEnvironmentOwner = (entry) => hasDurableBinding(entry) && isLiveEntry(entry); const findNode = async (entry, signal) => { const transport = options.getTransport(); if (!transport) throw new Error("device worker node transport is unavailable"); const node = (await raceNodeWorkerOperation(transport.listCurrentNodes(), signal)).find((candidate) => candidate.nodeId === entry.deviceId); if (!node) throw new WorkerTunnelOwnerDisconnectedError("device worker node is not connected with the supervisor dialect"); return { transport, node }; }; const runWorkspaceCommand = async (entry, command) => { const assertCurrent = () => { if (!isEnvironmentOwner(entry)) throw new Error("node worker workspace authority closed"); command.assertCurrent?.(); }; const commandTimeoutMs = command.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS; const transportTimeoutMs = addTimerTimeoutGraceMs(commandTimeoutMs, COMMAND_RESULT_GRACE_MS) ?? commandTimeoutMs; const deadline = Date.now() + transportTimeoutMs; const signals = [entry.abortController.signal, AbortSignal.timeout(transportTimeoutMs)]; if (command.signal) signals.push(command.signal); const signal = AbortSignal.any(signals); const input = { gatewayNamespace, environmentId: entry.environmentId, sessionId: entry.sessionId, generation: entry.ownerEpoch, argv: [...command.argv], ...command.input === void 0 ? {} : { input: command.input }, timeoutMs: commandTimeoutMs, ...command.resetWorkspace === void 0 ? {} : { resetWorkspace: command.resetWorkspace }, ...command.transfer === void 0 ? {} : { transfer: command.transfer }, ...command.seed === void 0 ? {} : { seed: command.seed } }; while (true) { assertCurrent(); const remainingMs = deadline - Date.now(); if (remainingMs <= 0 || signal.aborted) throw signal.reason ?? /* @__PURE__ */ new Error("node worker workspace command timed out"); let result; try { const { node, transport } = await findNode(entry, signal); assertCurrent(); result = await transport.invoke({ node, command: NODE_WORKER_WORKSPACE_EXEC_COMMAND, params: input, timeoutMs: remainingMs, signal, isDispatchAuthorized: () => { assertCurrent(); return true; } }); } catch (error) { assertCurrent(); if (command.transportRetry !== "idempotent" || signal.aborted || !isEnvironmentOwner(entry)) throw error; await sleepWithAbort(Math.min(RETRY_DELAY_MS, Math.max(1, deadline - Date.now())), signal); continue; } if (!result.ok) { const code = result.error?.code ?? "UNAVAILABLE"; if (code === "WORKSPACE_TRANSFER_FAILED") throw new NodeWorkerWorkspaceTransferError(result.error?.message ?? "workspace-transfer-failed: transfer did not complete"); if (command.transportRetry === "idempotent" && RETRYABLE_TRANSPORT_CODES.has(code)) { await sleepWithAbort(Math.min(RETRY_DELAY_MS, remainingMs), signal); continue; } throw new Error(result.error?.message && code === "INVALID_REQUEST" ? `node workspace command failed (${code}): ${result.error.message}` : `node workspace command failed (${code})`); } const parsed = parseNodeWorkerWorkspaceExecResult(payloadJson(result.payloadJSON)); if (!parsed) throw new Error("node workspace command violated its private result contract"); return parsed; } }; const createHandle = (entry, restoredWorkspace) => { const buildLaunchInput = (plan, claim) => ({ environmentSession: 1, launchId: plan.assignment.turnId, gatewayNamespace, expectedBundleHash: entry.expectedBuild.bundleHash, placementGeneration: claim.placementGeneration, descriptor: plan }); const { validateRestoredWorkspace, ...workspaceActions } = createNodeWorkerWorkspaceActions({ environmentId: entry.environmentId, ownerEpoch: entry.ownerEpoch, sessionId: entry.sessionId, ownerSignal: entry.abortController.signal, isOwnerCurrent: () => isLiveEntry(entry), restoredWorkspace, workspaceTransfer: options.workspaceTransfer, runWorkspaceCommand: (command) => runWorkspaceCommand(entry, command) }); return { handle: { ...workspaceActions, environmentId: entry.environmentId, ownerEpoch: entry.ownerEpoch, measureLaunchTurn: (plan, claim) => measureNodeWorkerLaunchBytes(entry.deviceId, buildLaunchInput(plan, claim)), launchTurn: async (request) => { if (entry.executionMode !== "worker-turn") throw new Error("remote-exec environments do not launch embedded worker turns"); const plan = request.plan; const claim = request.turnClaim; const isDispatchAuthorized = () => isEnvironmentOwner(entry) && claim.owner.kind === "worker" && claim.owner.environmentId === entry.environmentId && claim.owner.ownerEpoch === entry.ownerEpoch && claim.sessionId === plan.admission.sessionId && claim.runId === plan.assignment.runId && options.validateWorkerTurn(claim); const operation = options.launchNodeWorker({ deviceId: entry.deviceId, input: buildLaunchInput(plan, claim), isDispatchAuthorized, isCancellationAuthorized: () => hasDurableBinding(entry), timeoutMs: request.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS, ...request.credentialExpiresAtMs === void 0 ? {} : { credentialExpiresAtMs: request.credentialExpiresAtMs }, onDispatchReady: request.onDispatchReady, signal: request.signal ? AbortSignal.any([entry.abortController.signal, request.signal]) : entry.abortController.signal }); entry.launchTasks.add(operation); try { return spawnResultFromReceipt(await operation); } finally { entry.launchTasks.delete(operation); } }, stop: async () => { await stopEntry(entry); } }, validateRestoredWorkspace }; }; function stopEntry(entry, reason) { if (entries.get(entry.environmentId) === entry) entries.delete(entry.environmentId); entry.abortController.abort(/* @__PURE__ */ new Error("node worker tunnel owner stopped")); entry.readiness.reject(/* @__PURE__ */ new Error("node worker tunnel stopped before connecting")); return stopEnvironmentOwner(entry, reason, async () => { await entry.initialization?.catch(() => void 0); await Promise.allSettled(entry.launchTasks); }); } function stopEnvironmentOwner(entry, reason, drain) { if (entry.stopPromise) { if (entry.stopReason === reason || !retiredEntries.has(entry)) return entry.stopPromise; return entry.stopPromise.catch((error) => { if (!reason) throw error; }).then(() => retiredEntries.has(entry) ? stopEnvironmentOwner(entry, reason) : void 0); } retiredEntries.add(entry); entry.stopReason = reason; entry.stopPromise = (async () => { await drain?.(); let stopping = true; try { if (entry.executionMode === "worker-turn" && reason === void 0) { const signal = AbortSignal.timeout(DEFAULT_COMMAND_TIMEOUT_MS); const { transport, node } = await findNode(entry, signal); if (node.workerHost.environmentSession !== 1) throw new Error(formatNodeRunnerUpdateRequired(node.nodeId, NODE_RUNNER_UPDATE_REQUIRED_ISSUE)); const operation = transport.invoke({ node, command: NODE_WORKER_ENVIRONMENT_STOP_COMMAND, params: { gatewayNamespace, environmentId: entry.environmentId, sessionId: entry.sessionId, ownerEpoch: entry.ownerEpoch }, timeoutMs: DEFAULT_COMMAND_TIMEOUT_MS, signal, isDispatchAuthorized: () => stopping && retiredEntries.has(entry) }); const result = await raceNodeWorkerOperation(operation, signal); if (!result.ok) { const code = result.error?.code ?? "UNAVAILABLE"; const message = `node worker environment stop failed (${code})`; throw RETRYABLE_TRANSPORT_CODES.has(code) ? new WorkerTunnelOwnerDisconnectedError(message) : new Error(message); } } } finally { stopping = false; await options.workspaceTransfer.close(entry.environmentId); } if (reason !== "provider-destroying") retiredEntries.delete(entry); })().finally(() => { if (retiredEntries.has(entry)) entry.stopPromise = void 0; }); return entry.stopPromise; } async function stop(environmentId, ownerEpoch, reason) { const matches = (entry) => entry.environmentId === environmentId && (ownerEpoch === void 0 || ownerEpoch === entry.ownerEpoch); const live = [...entries.values()].filter(matches); const retired = [...retiredEntries].filter(matches); const operations = [...live.map((entry) => stopEntry(entry, reason)), ...retired.map((entry) => stopEnvironmentOwner(entry, reason))]; if (operations.length === 0) { const record = options.getEnvironment(environmentId); if (record?.nodeDeviceId && (ownerEpoch === void 0 || record.ownerEpoch === ownerEpoch)) { if (reason === "provider-destroying" || reason === "provider-destroyed") operations.push(options.workspaceTransfer.close(environmentId)); else { if (record.attachedSessionIds.length > 1) throw new Error("node worker environment teardown has an ambiguous session owner"); const sessionId = record.attachedSessionIds[0]; if (sessionId) operations.push(stopEnvironmentOwner({ deviceId: record.nodeDeviceId, environmentId, ownerEpoch: record.ownerEpoch, sessionId, executionMode: record.profileSnapshot.executionMode === "remote-exec" ? "remote-exec" : "worker-turn" }, reason)); } } } await joinWorkerTunnelStops(operations); } return { bindWorkspaceBindingResolver(resolver) { resolveWorkspaceBinding = resolver; }, async start(request) { const current = entries.get(request.environmentId); const retiring = [...retiredEntries].filter((entry) => entry.environmentId === request.environmentId); if (retiring.some((entry) => entry.ownerEpoch > request.ownerEpoch)) throw new Error("node worker tunnel owner epoch is stale"); if (current) { if (request.ownerEpoch < current.ownerEpoch) throw new Error("node worker tunnel owner epoch is stale"); if (request.ownerEpoch === current.ownerEpoch) { if (current.abortController.signal.aborted || current.executionMode !== request.executionMode || current.deviceId !== request.deviceId || current.sessionId !== request.sessionId || !sameWorkerBuild(current.expectedBuild, request.expectedBuild)) throw new Error("node worker tunnel owner binding changed within one epoch"); return current.readiness.promise; } } const readiness = createDeferredCore(); readiness.promise.catch(() => void 0); const entry = { ...request, abortController: new AbortController(), launchTasks: /* @__PURE__ */ new Set(), readiness }; entries.set(entry.environmentId, entry); entry.initialization = (async () => { if (current) await stopEntry(current); await Promise.all(retiring.map((owner) => stopEnvironmentOwner(owner))); if (!isLiveEntry(entry)) return; const restoredWorkspace = resolveWorkspaceBinding ? await raceNodeWorkerOperation(resolveWorkspaceBinding({ environmentId: request.environmentId, ownerEpoch: request.ownerEpoch, sessionId: request.sessionId }), entry.abortController.signal) : void 0; if (!isLiveEntry(entry)) return; const created = createHandle(entry, restoredWorkspace); await created.validateRestoredWorkspace(); if (!isLiveEntry(entry)) return; entry.handle = created.handle; readiness.resolve(created.handle); })(); entry.initialization.catch((error) => { readiness.reject(error); stopEntry(entry).catch((cleanupError) => { tunnelLog.warn("node worker tunnel cleanup failed after initialization error", { environmentId: entry.environmentId, ownerEpoch: entry.ownerEpoch, error: boundedWorkerError(cleanupError) }); }); }); return await readiness.promise; }, stop, async stopAll() { const environmentIds = /* @__PURE__ */ new Set([ ...entries.keys(), ...[...retiredEntries].map((entry) => entry.environmentId), ...options.listEnvironments().filter((record) => record.nodeDeviceId).map((record) => record.environmentId) ]); const stopped = await Promise.allSettled([...environmentIds].map((environmentId) => stop(environmentId))); stopped.push(...await Promise.allSettled([options.workspaceTransfer.closeAll()])); const failure = stopped.find((result) => result.status === "rejected"); if (failure) throw failure.reason; }, status(environmentId) { const entry = entries.get(environmentId); return entry && !entry.abortController.signal.aborted ? entry.handle ? "connected" : "connecting" : "stopped"; } }; } //#endregion export { createNodeWorkerTunnelManager };