From a7fd19de5f2d0b8b7243259fa0a2ea669b5323d7 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 07:07:51 -0400 Subject: [PATCH] feat: compose SSH relay host evidence Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 25 +- ...github-release-implementation-checklist.md | 123 +++++++++- .../ssh-relay-host-evidence-detection.test.ts | 229 ++++++++++++++++++ .../ssh/ssh-relay-host-evidence-detection.ts | 92 +++++++ 6 files changed, 458 insertions(+), 14 deletions(-) create mode 100644 src/main/ssh/ssh-relay-host-evidence-detection.test.ts create mode 100644 src/main/ssh/ssh-relay-host-evidence-detection.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 4541f8301c2..3cf63d7a535 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -264,6 +264,7 @@ jobs: src/main/ssh/ssh-relay-artifact-selector.test.ts \ src/main/ssh/ssh-relay-libc-detection.test.ts \ src/main/ssh/ssh-relay-linux-kernel-detection.test.ts \ + src/main/ssh/ssh-relay-host-evidence-detection.test.ts \ src/main/ssh/ssh-relay-darwin-version-detection.test.ts \ src/main/ssh/ssh-relay-darwin-translation-detection.test.ts \ src/main/ssh/ssh-relay-linux-libstdcxx-detection.test.ts \ @@ -815,6 +816,7 @@ jobs: src/main/ssh/ssh-relay-artifact-selector.test.ts ` src/main/ssh/ssh-relay-libc-detection.test.ts ` src/main/ssh/ssh-relay-linux-kernel-detection.test.ts ` + src/main/ssh/ssh-relay-host-evidence-detection.test.ts ` src/main/ssh/ssh-relay-darwin-version-detection.test.ts ` src/main/ssh/ssh-relay-darwin-translation-detection.test.ts ` src/main/ssh/ssh-relay-linux-libstdcxx-detection.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index e5055c9dd7a..81348800f38 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -122,6 +122,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-artifact-selector.test.ts', 'ssh-relay-libc-detection.test.ts', 'ssh-relay-linux-kernel-detection.test.ts', + 'ssh-relay-host-evidence-detection.test.ts', 'ssh-relay-darwin-version-detection.test.ts', 'ssh-relay-darwin-translation-detection.test.ts', 'ssh-relay-linux-libstdcxx-detection.test.ts', diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md index 6e8f935c9d6..08c622088dc 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — audit the smallest -disconnected full host-evidence composition boundary after native acceptance of every individual -compatibility detector, 2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — disconnected host- +evidence composition is locally green; exact-head all-six native CI is next, 2026-07-15, Codex +implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -269,6 +269,25 @@ format, max-lines, diff, and protected-resolver gates pass; replacement native C clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. Both Windows architectures pass all 32 detector cases under native Windows PowerShell. Windows arm64 completes full runtime proof and retains only the declared hosted build-26200 rejection against 26100. No live SSH, composer, product caller, setting/fallback, tuple, publication, or default exists. +`E-M5-HOST-EVIDENCE-COMPOSITION-AUDIT-001` fixes the next disconnected boundary: consume an explicit +canonical detected platform, SSH connection, and signal; run only that OS family's already-proven +detectors with at most three concurrent bounded channels; and return frozen selector-shaped evidence. +Darwin translation unknown returns no evidence rather than inventing native/translated state. +Inconsistent platform identity, product/startup callers, selector changes, live SSH, settings, +fallback, transfer/install, tuple enablement, publication, defaults, and SignPath stay absent. +RED is recorded before implementation: the purpose suite exits 1 because its module is absent +(0 tests, 316 ms, 132,235,264-byte max RSS), and the workflow oracle exits 1 because the suite has +only its POSIX native-family occurrence (6 passed/1 failed, 409 ms, 131,743,744-byte max RSS). +Pre-RED diff and protected-resolver checks pass. +`E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001` is green: 20/20 purpose and 193/193 focused assertions, +512/512 broader relay assertions, 280/280 release-script assertions, typecheck, lint, format, +max-lines, diff, no-product-import, and protected-resolver gates pass. The disconnected composer +validates canonical platform identity before probes; invokes only Linux's three, Darwin's two, or +Windows' one accepted bounded detector; runs multi-probe families concurrently; preserves +conservative unknowns; refuses unknown Darwin translation; propagates cancellation/errors; and +deeply freezes evidence. It is required in both native workflow families. Exact-head all-six Node 24 +client proof remains mandatory; no live SSH, product caller, settings/fallback, transfer/install, +tuple, publication, default, or SignPath work is included. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md index c01b7e9fb07..7d61e908426 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md @@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
Last updated: 2026-07-15
-Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the smallest disconnected full host-evidence composition boundary after native acceptance of every individual detector**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
-Session checkpoint: **WINDOWS COMPATIBILITY CI CLOSED / HOST COMPOSITION AUDIT NEXT — 2026-07-15, Codex implementation owner** — `E-M5-WINDOWS-COMPATIBILITY-DETECTION-CI-001` proves both native Windows architectures pass the strict detector, all six primary clients and both Linux supplements pass, Windows x64 baseline passes, and Windows arm64 retains only the declared build-26200/26100 runner mismatch after full runtime proof. Audit only the next disconnected composition boundary; do not add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
+Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: disconnected host-evidence composition is locally green; exact-head native CI is the next gate**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
+Session checkpoint: **HOST COMPOSITION LOCAL GREEN / NATIVE CI NEXT — 2026-07-15, Codex implementation owner** — `E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001` proves canonical six-tuple mapping, conservative unknowns, Darwin translation policy, bounded concurrency, cancellation/error propagation, deep freezing, workflow-family inclusion, and all local static/broad/release gates. Commit and push this isolated package, then require exact-head all-six native client proof before accepting it or opening another package. Do not add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -147,11 +147,10 @@ same change as the work it records. local gates, and all-six native Node 24 proof are green under E-M5-ARTIFACT-CACHE-ROOT-LOCAL-RED-001, E-M5-ARTIFACT-CACHE-ROOT-LOCAL-001, and E-M5-ARTIFACT-CACHE-ROOT-CI-001. No Electron or product caller exists. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit the smallest - disconnected full host-evidence composition boundary after - E-M5-WINDOWS-COMPATIBILITY-DETECTION-CI-001 closes every individual detector on native clients. - Keep live SSH, transfer/install, settings, fallback wiring, tuple enablement, publication, - defaults, and SignPath separately gated. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — disconnected full host- + evidence composition is locally green under E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001. Exact-head + all-six native client proof is the next acceptance gate. Keep live SSH, transfer/install, + settings, fallback wiring, tuple enablement, publication, defaults, and SignPath separately gated. - Completed Work Package 2 gate: target-native Windows source-signature reports from exact-head artifact jobs 87267322867 and 87267322870 were independently downloaded and matched to their identities and signing-stage reports under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001. PR Checks @@ -15926,6 +15925,108 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, two files, 38 pass tuple enablement, release publication, defaults, and SignPath remain absent. Legacy remains the sole production/default path and every tuple stays disabled. +### E-M5-HOST-EVIDENCE-COMPOSITION-AUDIT-001 — disconnected platform-specific evidence composition + +- Date/owner: 2026-07-15, Codex implementation owner. +- Audit: complete reads of the canonical remote-platform type/detector and tests, all six accepted + platform-specific compatibility detectors and purpose suites, selector host-evidence union and + conservative legacy classifications, native workflow commands/oracle, and the current production- + caller graph. The detector functions remain imported only by purpose tests and no host composer + exists. The existing platform detector has no cancellation input and must remain unchanged here. +- Finding: add one purpose-named `ssh-relay-host-evidence-detection` module consuming an explicit + already-detected `RemoteHostPlatform`, SSH connection, and optional cancellation signal. First + require the platform object's relay-platform identity to agree with its canonical OS/architecture; + inconsistent input returns undefined without a probe. Then invoke only the accepted detector family: + Linux kernel/libc/libstdc++ concurrently (at most three channels), Darwin version/translation + concurrently (at most two), or the single Windows compatibility detector. Return a deeply frozen + `SshRelayHostEvidence` object with canonical OS/architecture. +- Conservative mapping: Linux and Windows are not subject to the reviewed Darwin/Rosetta process- + translation policy, so their composed `processTranslated` value is false. Linux preserves + `{ family: 'unknown' }` and missing optional fields for later selector classification. A missing + complete Windows segment yields Windows evidence with all optional compatibility fields absent so + selection remains legacy. Darwin preserves explicit true/false translation and optional version; + unknown translation returns no host evidence because inventing either boolean could select a native + artifact or misdiagnose a translated process. No selector/type change is needed. +- Failure/concurrency contract: pass the exact signal to every invoked detector. Pre-abort and + `AbortError` propagate; unexpected detector rejection propagates unchanged rather than becoming + fallback eligibility. Ordinary probe failures retain each accepted detector's existing unknown + return. All OS-specific calls start before composition awaits their results, bounding the composed + probe to the slowest existing 15-second detector instead of their sum. No unsupported detector, + platform re-probe, retry, network/download, Node, or filesystem operation is introduced. +- Required RED/green proof: canonical Linux/Darwin/Windows x64 and arm64 mapping; exact detector + family/call count; complete and missing/unknown fields; native/translated/unknown Darwin behavior; + inconsistent platform rejection with zero probes; deep freezing; all relevant calls starting + concurrently; exact signal forwarding; cancellation/unexpected rejection propagation; absence of + production imports; and both native workflow families requiring the purpose suite. +- Deliberately absent: platform-detection modification, full startup/official-manifest/cache + orchestration, live SSH/remote cells, semantic transfer capability, transfer/install, settings, + fallback classification/wiring, Electron, `ORCA_RELAY_PATH`, tuple enablement, publication, + defaults, and SignPath. Legacy remains the sole production/default path and every tuple stays + disabled. +- RED evidence, 2026-07-15: + - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-host-evidence-detection.test.ts` — EXPECTED FAIL, exit 1: purpose suite + could not import absent `ssh-relay-host-evidence-detection`; zero tests collected in 316 ms, + 1.76 s wall time, 132,235,264-byte maximum RSS, 96,490,992-byte peak memory footprint. + - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +config/scripts/ssh-relay-runtime-workflow.test.mjs` — EXPECTED FAIL, exit 1: 6 passed and the + native-family inclusion oracle failed because the purpose suite had only the POSIX workflow + occurrence (`source.split(...)` length 2 instead of 3); 409 ms suite duration, 1.84 s wall time, + 131,743,744-byte maximum RSS, 95,851,800-byte peak memory footprint. + - Scope check before RED: `git diff --check` passed; protected + `ssh-remote-node-resolution.ts` and `.test.ts` diff remained empty. No implementation or product + caller existed. + +### E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001 — bounded immutable host evidence is locally green + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0, + pnpm 10.24.0, atop pushed head `cfe56d0fd` plus this isolated uncommitted package. The expected + Node-24 engine warning means exact Node 24 remains a native CI gate. +- Implementation: the new 92-line `ssh-relay-host-evidence-detection.ts` accepts only an explicit + already-detected platform, connection, and optional signal. It rejects canonical OS/architecture + inconsistency before any probe; starts only the selected OS family's accepted bounded detectors; + runs Linux's three and Darwin's two probes concurrently; preserves Linux/Windows unknowns for + conservative legacy selection; refuses to invent Darwin translation; propagates cancellation and + unexpected rejection; and returns deeply frozen selector-shaped evidence. It has no production + import or caller. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-host-evidence-detection.test.ts` — PASS, one file / 20 tests in + 337 ms Vitest, 1.69 seconds wall, 132,431,872-byte maximum RSS, zero swaps. It covers all six + canonical tuples and exact family call counts, full/unknown/missing mapping, native/translated/ + unknown Darwin behavior, inconsistent input with zero probes, deep freezing, concurrency, exact + signal forwarding, cancellation, and unexpected rejection. +- Workflow-oracle command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, one file / seven tests in + 384 ms Vitest, 1.73 seconds wall, 132,972,544-byte maximum RSS, zero swaps. The purpose suite now + occurs exactly once in both POSIX and PowerShell native contract commands. +- Post-format focused command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1` plus the composer, Windows compatibility, libc, kernel, + Darwin version/translation, libstdc++, selector, platform detection, and workflow-oracle suites — + PASS, 10 files / 193 passed / one declared native-Windows skip in 4.66 seconds Vitest, 7.18 seconds + wall, 138,493,952-byte maximum RSS, zero swaps. +- Broader relay command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS, 39 files passed / three + declared skipped files, 512 tests passed / four declared platform skips in 25.68 seconds Vitest, + 27.42 seconds wall, 281,608,192-byte maximum RSS, zero swaps. Only the established stale-lock + diagnostic and local Node 26 `module.register()` deprecation warning appear. +- Release-script command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 --silent=true config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests + in 24.51 seconds Vitest, 27.61 seconds wall, 188,825,600-byte maximum RSS, zero swaps. +- Static gates: `/usr/bin/time -l pnpm typecheck` passes in 5.81 seconds wall with + 1,262,075,904-byte maximum RSS. `/usr/bin/time -l pnpm lint` passes in 27.03 seconds wall with + 2,034,597,888-byte maximum RSS; all switch-exhaustiveness, 41 reliability, 355-entry max-lines, + bundled-skill, 9,837 localization-reference, locale-parity, and zero-candidate localization gates + pass, with the existing 26 warnings only in untouched files. Explicit max-lines, targeted + `oxlint`, targeted `oxfmt --check`, `git diff --check`, no-production-import, and protected- + resolver empty-diff checks pass. The 92-line implementation and 229-line purpose suite add no + max-lines suppression or vague module. +- Boundary/residual gaps: this is local mock/composition proof, not live SSH or native detector + execution. Exact-head all-six Node 24 client proof is mandatory next. Platform detection, + manifest/cache startup composition, live SSH/remote cells, semantic transfer capability, + transfer/install, Electron/settings, fallback classification/wiring, `ORCA_RELAY_PATH`, tuple + enablement, publication, defaults, and SignPath remain absent. Legacy remains the sole production/ + default path and every tuple stays disabled. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -15984,10 +16085,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Audit only the smallest disconnected Windows compatibility-evidence boundary for the already-reviewed -OS build, OpenSSH for Windows, Windows PowerShell, and .NET Framework selector fields. Record the -exact acquisition/parsing/failure contract before adding purpose tests or implementation. Do not -compose full host evidence, provision production secrets or environments, add an Electron/startup +Commit and push only the locally green disconnected host-evidence composition package under +E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001, then require exact-head all-six Node 24 native client proof +and adjacent PR checks before accepting it or auditing the next package. Do not modify platform +detection or selectors, provision production secrets or environments, add an Electron/startup consumer, embed unreviewed keys or manifest bytes, add SSH transfer/install, mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets diff --git a/src/main/ssh/ssh-relay-host-evidence-detection.test.ts b/src/main/ssh/ssh-relay-host-evidence-detection.test.ts new file mode 100644 index 00000000000..130f62c54ad --- /dev/null +++ b/src/main/ssh/ssh-relay-host-evidence-detection.test.ts @@ -0,0 +1,229 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import type { SshConnection } from './ssh-connection' +import { detectSshRelayHostEvidence } from './ssh-relay-host-evidence-detection' +import { getRemoteHostPlatform, type RemoteHostPlatform } from './ssh-remote-platform' + +const detectorMocks = vi.hoisted(() => ({ + kernel: vi.fn(), + libc: vi.fn(), + libstdcxx: vi.fn(), + darwinVersion: vi.fn(), + darwinTranslation: vi.fn(), + windowsCompatibility: vi.fn() +})) + +vi.mock('./ssh-relay-linux-kernel-detection', () => ({ + detectSshRelayLinuxKernelRelease: detectorMocks.kernel +})) +vi.mock('./ssh-relay-libc-detection', () => ({ + detectSshRelayLinuxLibc: detectorMocks.libc +})) +vi.mock('./ssh-relay-linux-libstdcxx-detection', () => ({ + detectSshRelayLinuxLibstdcxx: detectorMocks.libstdcxx +})) +vi.mock('./ssh-relay-darwin-version-detection', () => ({ + detectSshRelayDarwinVersion: detectorMocks.darwinVersion +})) +vi.mock('./ssh-relay-darwin-translation-detection', () => ({ + detectSshRelayDarwinProcessTranslation: detectorMocks.darwinTranslation +})) +vi.mock('./ssh-relay-windows-compatibility-detection', () => ({ + detectSshRelayWindowsCompatibility: detectorMocks.windowsCompatibility +})) + +const connection = {} as SshConnection + +beforeEach(() => { + vi.resetAllMocks() + detectorMocks.kernel.mockResolvedValue('6.8.0-63-generic') + detectorMocks.libc.mockResolvedValue({ family: 'glibc', version: '2.39' }) + detectorMocks.libstdcxx.mockResolvedValue({ + libstdcxxVersion: '6.0.33', + glibcxxVersion: '3.4.33' + }) + detectorMocks.darwinVersion.mockResolvedValue('15.5') + detectorMocks.darwinTranslation.mockResolvedValue(false) + detectorMocks.windowsCompatibility.mockResolvedValue({ + build: 26100, + openSshVersion: '9.8p1', + powerShellVersion: '5.1', + dotNetFrameworkRelease: 533325 + }) +}) + +function allDetectorCallCounts(): number[] { + return Object.values(detectorMocks).map((mock) => mock.mock.calls.length) +} + +describe('SSH relay host evidence detection', () => { + it.each([ + ['linux-x64', 'linux', 'x64', [1, 1, 1, 0, 0, 0]], + ['linux-arm64', 'linux', 'arm64', [1, 1, 1, 0, 0, 0]], + ['darwin-x64', 'darwin', 'x64', [0, 0, 0, 1, 1, 0]], + ['darwin-arm64', 'darwin', 'arm64', [0, 0, 0, 1, 1, 0]], + ['win32-x64', 'win32', 'x64', [0, 0, 0, 0, 0, 1]], + ['win32-arm64', 'win32', 'arm64', [0, 0, 0, 0, 0, 1]] + ] as const)( + 'maps canonical %s and invokes only its detector family', + async (relayPlatform, os, architecture, expectedCalls) => { + const result = await detectSshRelayHostEvidence( + getRemoteHostPlatform(relayPlatform), + connection + ) + + expect(result).toMatchObject({ os, architecture, processTranslated: false }) + expect(allDetectorCallCounts()).toEqual(expectedCalls) + } + ) + + it('composes complete Linux evidence and deeply freezes owned values', async () => { + const result = await detectSshRelayHostEvidence(getRemoteHostPlatform('linux-x64'), connection) + + expect(result).toEqual({ + os: 'linux', + architecture: 'x64', + processTranslated: false, + kernelVersion: '6.8.0-63-generic', + libc: { family: 'glibc', version: '2.39' }, + libstdcxxVersion: '6.0.33', + glibcxxVersion: '3.4.33' + }) + expect(Object.isFrozen(result)).toBe(true) + expect(result?.os === 'linux' && Object.isFrozen(result.libc)).toBe(true) + }) + + it('preserves unknown Linux libc and missing optional detector evidence', async () => { + detectorMocks.kernel.mockResolvedValue(undefined) + detectorMocks.libc.mockResolvedValue({ family: 'unknown' }) + detectorMocks.libstdcxx.mockResolvedValue(undefined) + + await expect( + detectSshRelayHostEvidence(getRemoteHostPlatform('linux-arm64'), connection) + ).resolves.toEqual({ + os: 'linux', + architecture: 'arm64', + processTranslated: false, + libc: { family: 'unknown' } + }) + }) + + it.each([false, true])('preserves Darwin process translation %s', async (translated) => { + detectorMocks.darwinTranslation.mockResolvedValue(translated) + + await expect( + detectSshRelayHostEvidence(getRemoteHostPlatform('darwin-arm64'), connection) + ).resolves.toEqual({ + os: 'darwin', + architecture: 'arm64', + processTranslated: translated, + version: '15.5' + }) + }) + + it('keeps a known Darwin translation result when the version is unavailable', async () => { + detectorMocks.darwinVersion.mockResolvedValue(undefined) + + await expect( + detectSshRelayHostEvidence(getRemoteHostPlatform('darwin-x64'), connection) + ).resolves.toEqual({ + os: 'darwin', + architecture: 'x64', + processTranslated: false + }) + }) + + it('returns no Darwin evidence when process translation is unknown', async () => { + detectorMocks.darwinTranslation.mockResolvedValue(undefined) + + await expect( + detectSshRelayHostEvidence(getRemoteHostPlatform('darwin-arm64'), connection) + ).resolves.toBeUndefined() + }) + + it('preserves complete Windows compatibility evidence', async () => { + await expect( + detectSshRelayHostEvidence(getRemoteHostPlatform('win32-arm64'), connection) + ).resolves.toEqual({ + os: 'win32', + architecture: 'arm64', + processTranslated: false, + build: 26100, + openSshVersion: '9.8p1', + powerShellVersion: '5.1', + dotNetFrameworkRelease: 533325 + }) + }) + + it('returns conservative Windows evidence when compatibility detection is unavailable', async () => { + detectorMocks.windowsCompatibility.mockResolvedValue(undefined) + + await expect( + detectSshRelayHostEvidence(getRemoteHostPlatform('win32-x64'), connection) + ).resolves.toEqual({ + os: 'win32', + architecture: 'x64', + processTranslated: false + }) + }) + + it('rejects inconsistent platform identity without starting any detector', async () => { + const inconsistent = { + ...getRemoteHostPlatform('linux-x64'), + os: 'win32' + } as RemoteHostPlatform + + await expect(detectSshRelayHostEvidence(inconsistent, connection)).resolves.toBeUndefined() + expect(allDetectorCallCounts()).toEqual([0, 0, 0, 0, 0, 0]) + }) + + it.each([ + ['linux-x64', ['kernel', 'libc', 'libstdcxx']], + ['darwin-x64', ['darwinVersion', 'darwinTranslation']] + ] as const)( + 'starts all %s family probes before awaiting a result', + async (relayPlatform, names) => { + const settlements = names.map(() => Promise.withResolvers()) + names.forEach((name, index) => + detectorMocks[name].mockReturnValue(settlements[index].promise) + ) + + const pending = detectSshRelayHostEvidence(getRemoteHostPlatform(relayPlatform), connection) + + expect(names.map((name) => detectorMocks[name].mock.calls.length)).toEqual(names.map(() => 1)) + settlements.forEach((settlement) => settlement.resolve(undefined)) + await pending + } + ) + + it('forwards the exact signal to every selected detector', async () => { + const controller = new AbortController() + + await detectSshRelayHostEvidence(getRemoteHostPlatform('linux-x64'), connection, { + signal: controller.signal + }) + + for (const mock of [detectorMocks.kernel, detectorMocks.libc, detectorMocks.libstdcxx]) { + expect(mock).toHaveBeenCalledWith(connection, { signal: controller.signal }) + } + }) + + it('propagates cancellation without converting it to host evidence', async () => { + const abortError = new Error('cancel host evidence') + abortError.name = 'AbortError' + detectorMocks.windowsCompatibility.mockRejectedValue(abortError) + + await expect( + detectSshRelayHostEvidence(getRemoteHostPlatform('win32-x64'), connection) + ).rejects.toBe(abortError) + }) + + it('propagates unexpected detector rejection unchanged', async () => { + const failure = new Error('unexpected kernel detector failure') + detectorMocks.kernel.mockRejectedValue(failure) + + await expect( + detectSshRelayHostEvidence(getRemoteHostPlatform('linux-x64'), connection) + ).rejects.toBe(failure) + }) +}) diff --git a/src/main/ssh/ssh-relay-host-evidence-detection.ts b/src/main/ssh/ssh-relay-host-evidence-detection.ts new file mode 100644 index 00000000000..deb984e31a1 --- /dev/null +++ b/src/main/ssh/ssh-relay-host-evidence-detection.ts @@ -0,0 +1,92 @@ +import type { SshConnection } from './ssh-connection' +import { detectSshRelayDarwinProcessTranslation } from './ssh-relay-darwin-translation-detection' +import { detectSshRelayDarwinVersion } from './ssh-relay-darwin-version-detection' +import { detectSshRelayLinuxLibc } from './ssh-relay-libc-detection' +import { detectSshRelayLinuxKernelRelease } from './ssh-relay-linux-kernel-detection' +import { detectSshRelayLinuxLibstdcxx } from './ssh-relay-linux-libstdcxx-detection' +import { detectSshRelayWindowsCompatibility } from './ssh-relay-windows-compatibility-detection' +import type { SshRelayHostEvidence } from './ssh-relay-artifact-selector' +import { getRemoteHostPlatform, type RemoteHostPlatform } from './ssh-remote-platform' + +type DetectionOptions = { signal?: AbortSignal } + +function isCanonicalPlatform(platform: RemoteHostPlatform): boolean { + const canonical = getRemoteHostPlatform(platform.relayPlatform) + return canonical.os === platform.os && canonical.arch === platform.arch +} + +async function detectLinuxHostEvidence( + platform: RemoteHostPlatform, + connection: SshConnection, + options: DetectionOptions +): Promise { + // Why: independent bounded probes run together so composition adds no serial bootstrap latency. + const [kernelVersion, libc, libstdcxx] = await Promise.all([ + detectSshRelayLinuxKernelRelease(connection, options), + detectSshRelayLinuxLibc(connection, options), + detectSshRelayLinuxLibstdcxx(connection, options) + ]) + const frozenLibc = Object.freeze({ ...libc }) + return Object.freeze({ + os: 'linux', + architecture: platform.arch, + processTranslated: false, + ...(kernelVersion === undefined ? {} : { kernelVersion }), + libc: frozenLibc, + ...(libstdcxx === undefined ? {} : libstdcxx) + }) +} + +async function detectDarwinHostEvidence( + platform: RemoteHostPlatform, + connection: SshConnection, + options: DetectionOptions +): Promise { + const [version, processTranslated] = await Promise.all([ + detectSshRelayDarwinVersion(connection, options), + detectSshRelayDarwinProcessTranslation(connection, options) + ]) + // Why: guessing native or translated could select incompatible executable bytes. + if (processTranslated === undefined) { + return undefined + } + return Object.freeze({ + os: 'darwin', + architecture: platform.arch, + processTranslated, + ...(version === undefined ? {} : { version }) + }) +} + +async function detectWindowsHostEvidence( + platform: RemoteHostPlatform, + connection: SshConnection, + options: DetectionOptions +): Promise { + const compatibility = await detectSshRelayWindowsCompatibility(connection, options) + return Object.freeze({ + os: 'win32', + architecture: platform.arch, + processTranslated: false, + ...compatibility + }) +} + +export async function detectSshRelayHostEvidence( + platform: RemoteHostPlatform, + connection: SshConnection, + options: DetectionOptions = {} +): Promise { + // Why: inconsistent caller evidence must not start probes or select a runtime for another tuple. + if (!isCanonicalPlatform(platform)) { + return undefined + } + + if (platform.os === 'linux') { + return detectLinuxHostEvidence(platform, connection, options) + } + if (platform.os === 'darwin') { + return detectDarwinHostEvidence(platform, connection, options) + } + return detectWindowsHostEvidence(platform, connection, options) +}