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)
+}