Files
orca/src/main/ssh/orcad-remote-node-runtime-report.ts
T
OrcaWinandm4air 67014c8c60 feat(ssh): pinned-Node relay on Windows SSH hosts (#24135)
* feat(ssh): pinned-Node relay on Windows SSH hosts (D5 Windows, D2)

Windows hosts opted into remoteRuntime 'pinned-node' now get the same rung A
relay POSIX hosts do, instead of an early host-Node fallback.

- Runtime store: the official node-v24.21.0-win-<arch>.zip is uploaded to a
  stage under %USERPROFILE%\.orca-remote\runtimes, verified against the pinned
  archive hash, node.exe extracted with System32 tar.exe (Expand-Archive
  fallback), hashed with Get-FileHash, run once, and published with
  node.exe + .verified by one Directory.Move. One powershell.exe per phase via
  the existing powerShellCommand helper; the probe also creates the stage. No
  new -EncodedCommand site, no -ExecutionPolicy, no Add-Type. node.exe keeps
  its real name at runtimes\node-<sha>\node.exe.
- Bytes that change or vanish after Orca wrote and verified them are reported
  as ORCA_NODE_RUNTIME_SECURITY_MODIFIED and become a remembered
  'security_software' refusal (fallback to the host-Node relay); application
  control blocks classify as 'noexec'.
- Addons: the win32 slot's conpty.node, conpty_console_list.node,
  conpty\conpty.dll + OpenConsole.exe, watcher and windows-process-tree.node
  ride with the relay; the orcad template now carries the win32 targets.
- Self-test on Windows is one powershell.exe running relay.js on node.exe; the
  report must name the pinned Node. The relay self-test loads conpty.node and
  opens a PTY with useConptyDll, and reports a missing bundled ConPTY file as a
  load failure. A pinned relay's terminals use the bundled ConPTY too; host-Node
  relays are unchanged.
- describeRelayRuntime recognizes the Windows store layout.

* fix(ssh): skip the redundant stage-cleanup powershell.exe after a Windows runtime promote

The promote script already removes its stage on every path, so the client-side
cleanup only runs when promote never returned (upload failure, abort, timeout).

* test(ssh): expect the ladder's remembered flag and pin check on Windows pinned relays

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-10-01 02:34:58 -07:00

71 lines
2.9 KiB
TypeScript

/** What the host-side runtime-store scripts print, shared by the POSIX and Windows installers. */
export const REMOTE_NODE_RUNTIME_READY = 'ORCA_NODE_RUNTIME_READY'
export const REMOTE_NODE_RUNTIME_MISSING = 'ORCA_NODE_RUNTIME_MISSING'
export const REMOTE_NODE_RUNTIME_SELFTEST_FAILED = 'ORCA_NODE_RUNTIME_SELFTEST_FAILED'
/** A file Orca wrote and verified changed or vanished afterwards; the rest of the line says which. */
export const REMOTE_NODE_RUNTIME_SECURITY_MODIFIED = 'ORCA_NODE_RUNTIME_SECURITY_MODIFIED'
export const REMOTE_NODE_RUNTIME_EXIT_PREFIX = 'ORCA_RUNTIME_EXIT='
export const REMOTE_NODE_RUNTIME_VERIFIED_MARKER = '.verified'
/** The pinned runtime ran on the host and did not report its version: a host verdict, with evidence. */
export class RemoteNodeRuntimeSelfTestError extends Error {
constructor(
readonly exitStatus: number | null,
readonly output: string
) {
super(
`The pinned Node runtime did not run on the host (exit ${exitStatus ?? 'unknown'}): ${output}`
)
this.name = 'RemoteNodeRuntimeSelfTestError'
}
}
/** The host answered, and what it answered is that something rewrote or removed our verified bytes. */
export class RemoteNodeRuntimeSecurityModifiedError extends Error {
constructor(readonly detail: string) {
super(`Security software on the host removed or modified the pinned Node runtime: ${detail}`)
this.name = 'RemoteNodeRuntimeSecurityModifiedError'
}
}
/** Splits `ORCA_RUNTIME_EXIT=<n>` from the output that follows it. */
export function parseRemoteRuntimeExitReport(text: string): {
exitStatus: number | null
output: string
} {
const lines = text.split(/\r?\n/)
const index = lines.findIndex((line) => line.startsWith(REMOTE_NODE_RUNTIME_EXIT_PREFIX))
if (index === -1) {
return { exitStatus: null, output: text.trim() }
}
const status = Number.parseInt(lines[index].slice(REMOTE_NODE_RUNTIME_EXIT_PREFIX.length), 10)
return {
exitStatus: Number.isNaN(status) ? null : status,
output: lines
.slice(index + 1)
.join('\n')
.trim()
}
}
/** Throws the host's verdict when a promote script reported one instead of READY. */
export function assertRemoteNodeRuntimePromoted(promoted: string): void {
const selfTestFailure = promoted.indexOf(REMOTE_NODE_RUNTIME_SELFTEST_FAILED)
if (selfTestFailure !== -1) {
const report = parseRemoteRuntimeExitReport(promoted.slice(selfTestFailure))
throw new RemoteNodeRuntimeSelfTestError(report.exitStatus, report.output)
}
const modified = promoted
.split(/\r?\n/)
.find((line) => line.startsWith(REMOTE_NODE_RUNTIME_SECURITY_MODIFIED))
if (modified !== undefined) {
throw new RemoteNodeRuntimeSecurityModifiedError(
modified.slice(REMOTE_NODE_RUNTIME_SECURITY_MODIFIED.length).trim()
)
}
if (promoted.trim().split(/\r?\n/).at(-1)?.trim() !== REMOTE_NODE_RUNTIME_READY) {
throw new Error(`The host did not verify the pinned Node runtime: ${promoted.trim()}`)
}
}