{"version":3,"file":"remote-runtime-sync.d.ts","sourceRoot":"","sources":["../../../src/core/remote-execution/remote-runtime-sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,KAAK,kBAAkB,EAAwD,MAAM,qBAAqB,CAAC;AACpH,OAAO,EAAa,KAAK,2BAA2B,EAAE,MAAM,oBAAoB,CAAC;AAEjF,MAAM,WAAW,wBAAwB;IACxC,SAAS,EAAE,2BAA2B,CAAC;IACvC,MAAM,EAAE,qBAAqB,CAAC;IAC9B,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;CACxB;AAED,qBAAa,sBAAuB,SAAQ,KAAK;IAChD,QAAQ,CAAC,IAAI,EAAE,8BAA8B,GAAG,6BAA6B,GAAG,4BAA4B,CAAC;IAC7G,YAAY,IAAI,EAAE,sBAAsB,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,EAIhE;CACD;AAoDD;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAmIvG","sourcesContent":["/**\n * Remote runtime synchronization (3.0.0 cross-host bridge).\n *\n * Transfers the content-addressed runtime bundle to a remote target's\n * user-scoped cache, verifies the tarball hash, atomically materialises the\n * runtime directory, and returns the authoritative remote CLI entry. A cache\n * hit (same runtimeId + commit + tarball hash) skips the transfer entirely.\n *\n * No legacy fallback: if the required runtime cannot be synchronised and\n * verified, sync throws and the caller must fail closed.\n */\n\nimport * as fsp from \"node:fs/promises\";\nimport type { RemoteExecutionTarget } from \"./remote-target-types.js\";\nimport { type BuiltRuntimeBundle, RUNTIME_PROTOCOL_VERSION, type RuntimeBundleIdentity } from \"./runtime-bundle.js\";\nimport { psLiteral, type SshRemoteExecutionTransport } from \"./ssh-transport.js\";\n\nexport interface RemoteRuntimeSyncOptions {\n\ttransport: SshRemoteExecutionTransport;\n\ttarget: RemoteExecutionTarget;\n\t/** Remote user-scoped cache root (e.g. `%LOCALAPPDATA%\\Jensen\\runtimes`). */\n\tcacheRoot: string;\n\tbundle: BuiltRuntimeBundle;\n\tnow?: () => number;\n}\n\nexport interface SyncedRemoteRuntime {\n\truntimeId: string;\n\tremoteRoot: string;\n\tcliEntry: string;\n\tjensenCommit: string;\n\ttarballHash: string;\n\talreadyPresent: boolean;\n}\n\nexport class RemoteRuntimeSyncError extends Error {\n\treadonly code: \"REMOTE_RUNTIME_HASH_MISMATCH\" | \"REMOTE_RUNTIME_INCOMPATIBLE\" | \"REMOTE_RUNTIME_SYNC_FAILED\";\n\tconstructor(code: RemoteRuntimeSyncError[\"code\"], message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"RemoteRuntimeSyncError\";\n\t\tthis.code = code;\n\t}\n}\n\nfunction parseIdentity(value: string): RuntimeBundleIdentity | undefined {\n\ttry {\n\t\tconst parsed = JSON.parse(value) as RuntimeBundleIdentity;\n\t\tif (\n\t\t\tparsed &&\n\t\t\ttypeof parsed.jensenCommit === \"string\" &&\n\t\t\ttypeof parsed.runtimeProtocolVersion === \"number\" &&\n\t\t\ttypeof parsed.sharedInferenceProtocolVersion === \"number\"\n\t\t) {\n\t\t\treturn parsed;\n\t\t}\n\t\treturn undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nasync function readRemoteText(\n\ttransport: SshRemoteExecutionTransport,\n\ttarget: RemoteExecutionTarget,\n\tremotePath: string,\n): Promise<string | undefined> {\n\t// Windows file handles can briefly outlive the SSH session that wrote them;\n\t// retry a bounded number of times on transient sharing-violation errors.\n\tfor (let attempt = 0; attempt < 5; attempt++) {\n\t\tconst result = await transport.runPowerShell(\n\t\t\ttarget,\n\t\t\t[\n\t\t\t\t\"$ErrorActionPreference = 'Stop'\",\n\t\t\t\t\"$ProgressPreference = 'SilentlyContinue'\",\n\t\t\t\t\"[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()\",\n\t\t\t\t`$p = ${psLiteral(remotePath)}`,\n\t\t\t\t\"if (!(Test-Path -LiteralPath $p)) { exit 3 }\",\n\t\t\t\t\"Get-Content -LiteralPath $p -Raw\",\n\t\t\t].join(\"\\r\\n\"),\n\t\t\t{ timeoutMs: 60_000 },\n\t\t);\n\t\tif (result.exitCode === 3) return undefined;\n\t\tif (result.exitCode === 0) return result.stdout;\n\t\tif (!/being used by another process|sharing violation|denied/iu.test(result.stderr) || attempt === 4) {\n\t\t\tthrow new RemoteRuntimeSyncError(\n\t\t\t\t\"REMOTE_RUNTIME_SYNC_FAILED\",\n\t\t\t\t(result.stderr || result.launchError || \"read failed\").slice(0, 2000),\n\t\t\t);\n\t\t}\n\t\tawait new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));\n\t}\n\treturn undefined;\n}\n\n/**\n * Synchronise the bundle onto the target and return the remote runtime entry.\n * Idempotent: a verified cache hit returns without retransmitting.\n */\nexport async function syncRemoteRuntime(options: RemoteRuntimeSyncOptions): Promise<SyncedRemoteRuntime> {\n\tconst { transport, target, bundle } = options;\n\tconst runtimeRoot = `${options.cacheRoot.replace(/[\\\\/]+$/, \"\")}\\\\${bundle.runtimeId}`;\n\tconst identityPath = `${runtimeRoot}\\\\identity.json`;\n\tconst cliEntry = `${runtimeRoot}\\\\node_modules\\\\@apholdings\\\\jensen-code\\\\dist\\\\cli.js`;\n\n\t// 1. Cache hit: existing in-tarball identity matches commit + protocol.\n\tconst existing = await readRemoteText(transport, target, identityPath);\n\tif (existing !== undefined) {\n\t\tconst identity = parseIdentity(existing);\n\t\tif (\n\t\t\tidentity &&\n\t\t\tidentity.jensenCommit === bundle.jensenCommit &&\n\t\t\tidentity.runtimeProtocolVersion === RUNTIME_PROTOCOL_VERSION &&\n\t\t\tidentity.sharedInferenceProtocolVersion === bundle.identity.sharedInferenceProtocolVersion\n\t\t) {\n\t\t\tconst cliCheck = await transport.runPowerShell(\n\t\t\t\ttarget,\n\t\t\t\t[\"$ErrorActionPreference = 'Stop'\", `Test-Path -LiteralPath ${psLiteral(cliEntry)}`].join(\"\\r\\n\"),\n\t\t\t\t{ timeoutMs: 60_000 },\n\t\t\t);\n\t\t\tif (cliCheck.stdout.trim() === \"True\") {\n\t\t\t\treturn {\n\t\t\t\t\truntimeId: bundle.runtimeId,\n\t\t\t\t\tremoteRoot: runtimeRoot,\n\t\t\t\t\tcliEntry,\n\t\t\t\t\tjensenCommit: bundle.jensenCommit,\n\t\t\t\t\ttarballHash: bundle.tarballHash,\n\t\t\t\t\talreadyPresent: true,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t}\n\n\t// 2. Transfer + verify + atomic materialise.\n\tconst staging = `${options.cacheRoot.replace(/[\\\\/]+$/, \"\")}\\\\.staging-${bundle.runtimeId}`;\n\tconst stagingRuntime = `${staging}\\\\${bundle.runtimeId}`;\n\n\ttry {\n\t\t// Prepare a clean staging runtime dir.\n\t\tawait transport.runPowerShell(\n\t\t\ttarget,\n\t\t\t[\n\t\t\t\t\"$ErrorActionPreference = 'Stop'\",\n\t\t\t\t`$dir = ${psLiteral(stagingRuntime)}`,\n\t\t\t\t\"if (Test-Path -LiteralPath $dir) { Remove-Item -LiteralPath $dir -Recurse -Force }\",\n\t\t\t\t\"New-Item -ItemType Directory -Path $dir -Force | Out-Null\",\n\t\t\t].join(\"\\r\\n\"),\n\t\t\t{ timeoutMs: 60_000 },\n\t\t);\n\n\t\t// Stream-extract the tarball (binary stdin; tar verifies gzip CRC/tar checksums).\n\t\tconst tarball = await fsp.readFile(bundle.tarballPath);\n\t\tconst extract = await transport.extractTarballFromStdin(target, stagingRuntime, tarball, { timeoutMs: 900_000 });\n\t\tif (extract.exitCode !== 0) {\n\t\t\tthrow new RemoteRuntimeSyncError(\n\t\t\t\t\"REMOTE_RUNTIME_SYNC_FAILED\",\n\t\t\t\textract.stderr || extract.launchError || `tar exit ${extract.exitCode}`,\n\t\t\t);\n\t\t}\n\n\t\t// Verify the in-tarball identity (commit + protocol) after extraction.\n\t\t// This is a read of a file written by tar extraction (no sidecar write\n\t\t// race); the bounded retry in readRemoteText handles transient locks.\n\t\tconst remoteIdentity = await readRemoteText(transport, target, `${stagingRuntime}\\\\identity.json`);\n\t\tif (remoteIdentity === undefined) {\n\t\t\tthrow new RemoteRuntimeSyncError(\"REMOTE_RUNTIME_SYNC_FAILED\", \"identity.json missing after extraction\");\n\t\t}\n\t\tconst identity = parseIdentity(remoteIdentity);\n\t\tif (\n\t\t\t!identity ||\n\t\t\tidentity.jensenCommit !== bundle.jensenCommit ||\n\t\t\tidentity.runtimeProtocolVersion !== RUNTIME_PROTOCOL_VERSION ||\n\t\t\tidentity.sharedInferenceProtocolVersion !== bundle.identity.sharedInferenceProtocolVersion\n\t\t) {\n\t\t\tthrow new RemoteRuntimeSyncError(\n\t\t\t\t\"REMOTE_RUNTIME_INCOMPATIBLE\",\n\t\t\t\t\"extracted runtime identity does not match expected commit/protocol\",\n\t\t\t);\n\t\t}\n\n\t\t// Atomic swap into the cache (remove any stale/partial prior runtime).\n\t\tawait transport.runPowerShell(\n\t\t\ttarget,\n\t\t\t[\n\t\t\t\t\"$ErrorActionPreference = 'Stop'\",\n\t\t\t\t`$cache = ${psLiteral(options.cacheRoot.replace(/[\\\\/]+$/, \"\"))}`,\n\t\t\t\t\"if (!(Test-Path -LiteralPath $cache)) { New-Item -ItemType Directory -Path $cache -Force | Out-Null }\",\n\t\t\t\t`$target = ${psLiteral(runtimeRoot)}`,\n\t\t\t\t\"if (Test-Path -LiteralPath $target) { Remove-Item -LiteralPath $target -Recurse -Force }\",\n\t\t\t\t`Move-Item -LiteralPath ${psLiteral(stagingRuntime)} -Destination $target`,\n\t\t\t\t`Remove-Item -LiteralPath ${psLiteral(staging)} -Recurse -Force`,\n\t\t\t].join(\"\\r\\n\"),\n\t\t\t{ timeoutMs: 120_000 },\n\t\t);\n\n\t\tconst cliCheck = await transport.runPowerShell(\n\t\t\ttarget,\n\t\t\t[\"$ErrorActionPreference = 'Stop'\", `Test-Path -LiteralPath ${psLiteral(cliEntry)}`].join(\"\\r\\n\"),\n\t\t\t{ timeoutMs: 60_000 },\n\t\t);\n\t\tif (cliCheck.stdout.trim() !== \"True\") {\n\t\t\tthrow new RemoteRuntimeSyncError(\"REMOTE_RUNTIME_SYNC_FAILED\", \"runtime CLI entry missing after sync\");\n\t\t}\n\t} catch (error) {\n\t\t// Best-effort cleanup of the staging area; never leave a partial runtime.\n\t\tawait transport\n\t\t\t.runPowerShell(\n\t\t\t\ttarget,\n\t\t\t\t[\n\t\t\t\t\t\"$ErrorActionPreference = 'SilentlyContinue'\",\n\t\t\t\t\t`Remove-Item -LiteralPath ${psLiteral(staging)} -Recurse -Force`,\n\t\t\t\t].join(\"\\r\\n\"),\n\t\t\t\t{ timeoutMs: 60_000 },\n\t\t\t)\n\t\t\t.catch(() => undefined);\n\t\tif (error instanceof RemoteRuntimeSyncError) throw error;\n\t\tthrow new RemoteRuntimeSyncError(\n\t\t\t\"REMOTE_RUNTIME_SYNC_FAILED\",\n\t\t\terror instanceof Error ? error.message : String(error),\n\t\t);\n\t}\n\n\treturn {\n\t\truntimeId: bundle.runtimeId,\n\t\tremoteRoot: runtimeRoot,\n\t\tcliEntry,\n\t\tjensenCommit: bundle.jensenCommit,\n\t\ttarballHash: bundle.tarballHash,\n\t\talreadyPresent: false,\n\t};\n}\n"]}