Files
orca/src/main/ssh/ssh-relay-opencode-pinned-node.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

67 lines
2.8 KiB
TypeScript

import { join } from 'node:path'
import { getAppEnvironment } from '../../shared/app-environment'
import { waitForPromiseWithSignal } from '../../shared/abort-signal-reason'
import { pinnedNodeRuntimeAsset, type NodeRuntimeTarget } from '../../shared/node-runtime-pin'
import type { SshConnection } from './ssh-connection'
import { resolveOrcadDeploymentTargetFacts } from './orcad-deployment-target'
import { ensureRemoteOrcadNodeRuntime, type RemoteRuntimeStep } from './orcad-remote-node-runtime'
import { materializeNodeRuntimeArchive } from './pinned-runtime-materializer'
import type { RemoteHostPlatform } from './ssh-remote-platform'
import { pinnedRuntimeTargetForHost } from './ssh-relay-runtime-ladder'
const DOWNLOAD_TIMEOUT_MS = 180_000
const downloads = new Map<string, Promise<string>>()
/**
* The pinned Node for hosts whose own Node cannot read OpenCode's database (design D4a). Every
* host, Windows included, installs it into the shared runtimes/ store as the official archive
* with a `.verified` marker; nothing here touches vault-sqlite/, which old relays' references
* still name. `runtimeSha256` is the ref the relay dir must carry so store GC keeps it.
*/
export async function preparePinnedNodeForVault(options: {
conn: SshConnection
host: RemoteHostPlatform
relayDir: string
cacheRoot?: string
signal: AbortSignal
exec: (command: string) => Promise<string>
remote: RemoteRuntimeStep
}): Promise<{ executable: string; runtimeSha256: string }> {
const { conn, host, signal, exec } = options
const facts = await resolveOrcadDeploymentTargetFacts({ conn, host, signal, exec })
// Why before any upload: below every runtime's glibc floor the self-test could only fail.
const target = pinnedRuntimeTargetForHost(facts)
if (!target) {
const glibc = facts.glibc ? `${facts.glibc.major}.${facts.glibc.minor}` : 'unknown'
throw new Error(`No Orca-managed Node runs on this host's glibc ${glibc}`)
}
const cacheRoot =
options.cacheRoot ?? join(getAppEnvironment().getPath('userData'), 'orcad-artifacts')
const { executable } = await ensureRemoteOrcadNodeRuntime({
conn,
host,
slotDir: options.relayDir,
target,
archivePath: () => cachedArchive(target, cacheRoot, signal),
signal,
remoteStep: options.remote
})
return { executable, runtimeSha256: pinnedNodeRuntimeAsset(target).executableSha256 }
}
function cachedArchive(
target: NodeRuntimeTarget,
cacheRoot: string,
signal: AbortSignal
): Promise<string> {
const key = `${cacheRoot}\0${target}`
let pending = downloads.get(key)
if (!pending) {
pending = materializeNodeRuntimeArchive(target, cacheRoot, {
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
}).finally(() => downloads.delete(key))
downloads.set(key, pending)
}
return waitForPromiseWithSignal(pending, signal)
}