Files
orca/src/main/ssh/orcad-remote-node-runtime-windows.ts
T
8afa1db50c feat(ssh): rung B glibc 2.17 compat runtime; gate remote vault on host node:sqlite (#24148)
* feat(ssh): wire rung B to the glibc 2.17 compat runtime; gate rung C vault on full node:sqlite

- COMPAT_RELAY_RUNTIMES lists linux-x64-glibc217; rung B plans the compat slot and compat
  pinned Node when glibc is below 2.28 or rung A refused with libc_floor/missing_lib.
- The relay version folds the compat runtime's executable hash; refusals are cached per runtime.
- The orcad template stages an optional linux-x64-glibc217 target (base package + compat
  node-pty slot + compat runtime marker); the verifier and materializer accept it.
- node-pty slot loader falls back to the compat slot when the default slot is missing or
  needs a newer glibc.
- Runtime store GC keeps the compat pin beside the default one on every relay connect.
- hasNodeSqliteReaderApi (DatabaseSync + backup) gates relay session search and the relay
  OpenCode reader, which now names the host Node version in its unavailable reason; the SSH
  vault reader installs the compat Node on old-glibc hosts and uploads nothing when no
  pinned Node can run.
- Rung D: a remembered noexec reports home_noexec and never advises installing Node.

* fix(ssh): re-prove a replayed noexec after rung D so allowing exec recovers the host

* fix(ssh): keep the rung B compat runtime pinned in the relay-connect store GC

* test(ssh): mock deployment-target facts in the Windows OpenCode runtime tests

* ci(ssh): build the glibc 2.17 compat slot for the hostile-host matrix; CentOS 7 lands on rung B

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-01 05:32:05 -07:00

170 lines
9.5 KiB
TypeScript

/**
* The Windows half of the pinned-Node runtime store: `runtimes\node-<sha>\node.exe` with a
* `.verified` marker, beside the relay version directories (design D2, D5 "Windows").
*
* Each phase is ONE PowerShell invocation through `powerShellCommand`, which adds no new
* `-EncodedCommand` site, spells no `-ExecutionPolicy` and compiles nothing (`Add-Type`); see
* docs/reference/windows-edr-posture.md. node.exe keeps upstream's name and layout.
*/
import {
pinnedNodeRuntimeAsset,
NODE_RUNTIME_PIN,
nodeRuntimeExecutablePath
} from '../../shared/node-runtime-pin'
import type { NodeRuntimeTarget } from '../../shared/node-runtime-pin'
import { ORCAD_NODE_RUNTIME_WINDOWS_EXECUTABLE } from '../../shared/orcad-artifacts'
import {
REMOTE_NODE_RUNTIME_EXIT_PREFIX,
REMOTE_NODE_RUNTIME_MISSING,
REMOTE_NODE_RUNTIME_READY,
REMOTE_NODE_RUNTIME_SECURITY_MODIFIED,
REMOTE_NODE_RUNTIME_SELFTEST_FAILED,
REMOTE_NODE_RUNTIME_VERIFIED_MARKER
} from './orcad-remote-node-runtime-report'
import { powerShellCommand, powerShellLiteral } from './ssh-remote-powershell'
/** Promotion may fall back to Expand-Archive, which unpacks the whole ~30 MiB zip. */
export const WINDOWS_NODE_RUNTIME_PROMOTE_TIMEOUT_MS = 300_000
function windowsPath(...segments: string[]): string {
return segments
.map((segment, index) => (index === 0 ? segment.replace(/\/+$/, '') : segment))
.join('/')
}
// Why no `$ErrorActionPreference = 'Stop'`: under Windows PowerShell 5.1 it turns any stderr
// line from a native command (tar.exe, node.exe) into a terminating error.
function prelude(): string[] {
return [
// Why: PS 5.1 progress records slow Expand-Archive badly and leak CLIXML into stdout.
"$ProgressPreference = 'SilentlyContinue'",
'function Get-OrcaSha256([string]$p) { if (Test-Path -LiteralPath $p -PathType Leaf) { (Get-FileHash -LiteralPath $p -Algorithm SHA256).Hash.ToLowerInvariant() } else { "" } }'
]
}
function runtimeVariables(runtimeDir: string): string[] {
return [
`$runtimeDir = ${powerShellLiteral(runtimeDir)}`,
`$exe = ${powerShellLiteral(windowsPath(runtimeDir, ORCAD_NODE_RUNTIME_WINDOWS_EXECUTABLE))}`,
`$verified = ${powerShellLiteral(windowsPath(runtimeDir, REMOTE_NODE_RUNTIME_VERIFIED_MARKER))}`
]
}
/**
* Ready only when node.exe hashes to the pin under its marker. When it is not, and a stage is
* named, the same invocation creates the upload stage so the install costs no extra spawn.
*/
export function windowsNodeRuntimeProbeCommand(
runtimeDir: string,
target: NodeRuntimeTarget,
stageDir?: string
): string {
return powerShellCommand(
[
...prelude(),
...runtimeVariables(runtimeDir),
`if ((Get-OrcaSha256 $exe) -eq ${powerShellLiteral(pinnedNodeRuntimeAsset(target).executableSha256)}) {`,
`if (Test-Path -LiteralPath $verified -PathType Leaf) { Write-Output ${powerShellLiteral(REMOTE_NODE_RUNTIME_READY)}; exit 0 }`,
// Why adopt: earlier Windows vault readers left the pinned node.exe here with no marker.
// Running it is the same check promotion makes before it writes one.
`try { $adoptOut = ((& $exe --version 2>&1) | ForEach-Object { "$_" }) -join ''; if (($LASTEXITCODE -eq 0) -and ($adoptOut.Trim() -eq ${powerShellLiteral(`v${NODE_RUNTIME_PIN.version}`)})) { [IO.File]::WriteAllText($verified, ''); Write-Output ${powerShellLiteral(REMOTE_NODE_RUNTIME_READY)}; exit 0 } } catch { }`,
'}',
...(stageDir
? [
`$null = New-Item -ItemType Directory -Force -Path ${powerShellLiteral(stageDir)} -ErrorAction Stop`
]
: []),
`Write-Output ${powerShellLiteral(REMOTE_NODE_RUNTIME_MISSING)}`
].join('\n')
)
}
/** Cheap warm-path check: a published runtime has its marker and node.exe; no re-hash. */
export function windowsNodeRuntimePresentCommand(runtimeDir: string): string {
return powerShellCommand(
[
...runtimeVariables(runtimeDir),
`if ((Test-Path -LiteralPath $verified -PathType Leaf) -and (Test-Path -LiteralPath $exe -PathType Leaf)) { Write-Output ${powerShellLiteral(REMOTE_NODE_RUNTIME_READY)} } else { Write-Output ${powerShellLiteral(REMOTE_NODE_RUNTIME_MISSING)} }`
].join('\n')
)
}
/**
* Verify the uploaded zip, extract node.exe (tar.exe, else Expand-Archive), verify it, run it,
* then publish the runtime directory with one directory rename. Bytes that were verified and
* then change or vanish are reported as security software, never retried as a transfer fault.
*/
export function windowsNodeRuntimePromoteCommand(args: {
stageDir: string
archive: string
runtimeDir: string
target: NodeRuntimeTarget
}): string {
const asset = pinnedNodeRuntimeAsset(args.target)
const member = nodeRuntimeExecutablePath(args.target, asset.archive)
const modified = (what: string): string =>
`Write-Output ${powerShellLiteral(`${REMOTE_NODE_RUNTIME_SECURITY_MODIFIED} ${what}`)}`
return powerShellCommand(
[
...prelude(),
...runtimeVariables(args.runtimeDir),
`$stage = ${powerShellLiteral(args.stageDir)}`,
`$archive = ${powerShellLiteral(windowsPath(args.stageDir, args.archive))}`,
`$extracted = ${powerShellLiteral(windowsPath(args.stageDir, ...member.split('/')))}`,
`$candidate = ${powerShellLiteral(windowsPath(args.stageDir, 'promote'))}`,
`$pin = ${powerShellLiteral(asset.executableSha256)}`,
// Why a function: `return` leaves it and the stage cleanup below still runs, which `exit` would skip.
'$script:code = 0',
'function Invoke-OrcaPromote {',
`$archiveHash = Get-OrcaSha256 $archive`,
`if ($archiveHash -eq '') { Write-Output 'ORCA_NODE_RUNTIME_ARCHIVE_MISSING'; $script:code = 1; return }`,
`if ($archiveHash -ne ${powerShellLiteral(asset.archiveSha256)}) { ${modified('the uploaded archive changed after it was written')}; return }`,
// Why the System32 path: a Git or MSYS tar.exe earlier on PATH cannot read a zip.
"$tar = Join-Path $env:SystemRoot 'System32\\tar.exe'",
'$extractedBy = $null',
'if (Test-Path -LiteralPath $tar -PathType Leaf) {',
`$null = & $tar -xf $archive -C $stage ${powerShellLiteral(member)} 2>&1`,
"if ($LASTEXITCODE -eq 0) { $extractedBy = 'tar' }",
'}',
'if ($null -eq $extractedBy) {',
"try { Expand-Archive -LiteralPath $archive -DestinationPath $stage -Force -ErrorAction Stop; $extractedBy = 'Expand-Archive' } catch { Write-Output ('ORCA_NODE_RUNTIME_EXTRACT_FAILED ' + $_.Exception.Message); $script:code = 1; return }",
'}',
`if (-not (Test-Path -LiteralPath $extracted -PathType Leaf)) { ${modified('node.exe vanished after extraction')}; return }`,
`if ((Get-OrcaSha256 $extracted) -ne $pin) { ${modified('node.exe changed after extraction')}; return }`,
// Why run it: executing is the only reliable check for application control and AV blocks.
"$runOut = ''; $runStatus = $null",
'try { $runOut = ((& $extracted --version 2>&1) | ForEach-Object { "$_" }) -join "`n"; $runStatus = $LASTEXITCODE } catch { $runStatus = -1; $runOut = $_.Exception.Message }',
`if (($runStatus -ne 0) -or ($runOut.Trim() -ne ${powerShellLiteral(`v${NODE_RUNTIME_PIN.version}`)})) {`,
`Write-Output ${powerShellLiteral(REMOTE_NODE_RUNTIME_SELFTEST_FAILED)}`,
`Write-Output (${powerShellLiteral(REMOTE_NODE_RUNTIME_EXIT_PREFIX)} + $runStatus)`,
'Write-Output ($runOut.Substring(0, [Math]::Min(4000, $runOut.Length)))',
'return',
'}',
`if ((Get-OrcaSha256 $extracted) -ne $pin) { ${modified('node.exe changed after it ran')}; return }`,
'$null = New-Item -ItemType Directory -Force -Path $candidate -ErrorAction Stop',
`Move-Item -LiteralPath $extracted -Destination (Join-Path $candidate ${powerShellLiteral(ORCAD_NODE_RUNTIME_WINDOWS_EXECUTABLE)}) -ErrorAction Stop`,
`[IO.File]::WriteAllText((Join-Path $candidate ${powerShellLiteral(REMOTE_NODE_RUNTIME_VERIFIED_MARKER)}), '')`,
'$null = New-Item -ItemType Directory -Force -Path (Split-Path -Parent $runtimeDir) -ErrorAction Stop',
'if ((Test-Path -LiteralPath $runtimeDir) -and ((Get-OrcaSha256 $exe) -ne $pin)) { Remove-Item -LiteralPath $runtimeDir -Recurse -Force -ErrorAction Stop }',
// Why Directory.Move: it refuses an existing target, where Move-Item would nest inside it.
// Why retry: an on-access scan of the new node.exe can briefly hold the directory.
'for ($attempt = 1; ($attempt -le 5) -and -not (Test-Path -LiteralPath $runtimeDir); $attempt++) { try { [IO.Directory]::Move($candidate, $runtimeDir) } catch { Start-Sleep -Milliseconds 400 } }',
"if (-not (Test-Path -LiteralPath $exe -PathType Leaf)) { throw 'The pinned Node runtime directory could not be published' }",
"if (-not (Test-Path -LiteralPath $verified -PathType Leaf)) { [IO.File]::WriteAllText($verified, '') }",
`if ((Get-OrcaSha256 $exe) -ne $pin) { Remove-Item -LiteralPath $verified -Force -ErrorAction SilentlyContinue; ${modified('node.exe changed after it was published')}; return }`,
`Write-Output ('extracted-by ' + $extractedBy)`,
`Write-Output ${powerShellLiteral(REMOTE_NODE_RUNTIME_READY)}`,
'}',
'try { Invoke-OrcaPromote } catch { [Console]::Error.WriteLine($_.Exception.Message); $script:code = 1 }',
'Remove-Item -LiteralPath $stage -Recurse -Force -ErrorAction SilentlyContinue',
'exit $script:code'
].join('\n')
)
}
export function windowsNodeRuntimeStageCleanupCommand(stageDir: string): string {
return powerShellCommand(
`Remove-Item -LiteralPath ${powerShellLiteral(stageDir)} -Recurse -Force -ErrorAction SilentlyContinue`
)
}