From a6feb8c3be0e15ae45fc6b431c1665e5fd84d2c9 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 17 Jul 2026 00:25:12 -0400 Subject: [PATCH] test(ssh): pin Windows fixture host trust Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 26 ++++- ...github-release-implementation-checklist.md | 108 ++++++++++++++---- ...6-07-14-ssh-relay-github-release-plan.html | 26 +++-- ...sh-system-no-input-windows-openssh.test.ts | 73 ++++++++++-- 4 files changed, 190 insertions(+), 43 deletions(-) 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 18d7efcc0f4..763f4f3c4d8 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,13 @@ 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 6 / Work Package 5 bounded runtime transfer — bounded verbose -Win32-OpenSSH lifecycle capture locally green; exact-head native proof required, 2026-07-16, Codex -implementation owner.** +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — explicit fixture +host-key trust correction in progress, 2026-07-16, Codex implementation owner.** Exact head +`b24dda744892c2ee43a3fb345d889ba97adb4239`, run `29554073553`, and Windows x64/ARM64 jobs +`87802525440`/`87802525442` prove the bounded verbose diagnostic and show both clients stop before +authentication while looking for the runner account's missing default `known_hosts`. The next +single-variable correction passes the fixture's exact pinned file with strict checking; production +SSH arguments and every product/default path remain unchanged. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain @@ -225,6 +229,22 @@ output replay; the live suite uses 6 seconds, `-vvv`, outer 8/10-second safety c trace tail. Focused 119/119, all 285 release contracts, typecheck, full lint/reliability/max-lines, formatting, diff, and protected-resolver isolation pass with honest native skips. Commit/push only this artifact and require x64/ARM64 phase classification before selecting ConPTY or another client. +Exact head `b24dda744892c2ee43a3fb345d889ba97adb4239`, run `29554073553`, and Windows x64/ARM64 +jobs `87802525440`/`87802525442` prove 101 files/863 cases with 19 skips plus the internal timeout, +replay, and job-termination contracts. Both 6111.6731/6395.98-millisecond traces stop before +authentication at `hostkeys_find_by_key_hostfile`: Win32-OpenSSH looks under the runner account +instead of the isolated fixture client home. `E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-CI-RED-001` +therefore replaces the prior session-close hypothesis with one narrower correction: pass only the +fixture's exact pinned `known_hosts` with strict checking. Both profile teardowns still catch locked +`UsrClass.dat`; all four non-Windows jobs and both Linux supplements pass. +`E-M6-WINDOWS-DIAGNOSTIC-HOST-TRUST-CORRECTION-LOCAL-001` records that correction locally green: +the purpose contract proves `-vvv`, `StrictHostKeyChecking=yes`, and the exact fixture +`UserKnownHostsFile` occur before `--`, rejects `StrictHostKeyChecking=no`, and compares against an +unchanged production argument vector. Focused 120/120, all 285 release contracts, typecheck, full +lint/reliability/max-lines, formatting, diff, and protected-resolver isolation pass with honest +native/live skips. Commit/push only this runner diagnostic and require fresh x64/ARM64 strict-trust, +authentication, natural settlement, full-size transfer, and exact teardown proof. Production, +Beta, fallback, tuple, publication, default, and signing paths remain untouched. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown 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 3c2a337c901..a9066c68d8f 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-16
-Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **Locally green / bounded verbose client-lifecycle native proof required — 2026-07-16, Codex implementation owner**. `E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-LOCAL-001` records the runner-temp internal deadline, job settlement, bounded replay, and verbose trace-tail contracts locally green. Commit/push only this diagnostic and require both native architectures to identify the last completed client phase before selecting ConPTY or another client. Every production/default SSH path remains disconnected. Real Apple/SignPath rehearsals remain deferred.
-Session checkpoint: **PRIVATE CONSOLE EOF PROVEN / PRIVATE OUTPUT PIPES FALSIFIED / BOUNDED REGULAR OUTPUT ACTIVE — 2026-07-16, Codex implementation owner** — exact head `5841b12bc94ecd1027e507e4b741e09fcb0a093d`, run `29552101635`, and x64/ARM64 jobs `87796673326`/`87796673353` both pass 101 files/860 cases with 19 skips, legacy compilation, real-console qualification/EOF, argv/binary/exit/handle-exclusion/cancellation, unchanged full-runtime reproducibility, and full-size cache gates. Live diagnostics still time out after 8025.1553/8034.6943 milliseconds with ended stdout, closed channel, no sentinel/process exit, and zero stderr; both exact teardowns catch locked `UsrClass.dat`. All four non-Windows primary jobs and both Linux supplements pass. No product caller, full-size Windows transfer claim, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
+Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — explicit fixture host-key trust correction, 2026-07-16, Codex implementation owner**. Exact head `b24dda744892c2ee43a3fb345d889ba97adb4239`, run `29554073553`, and x64/ARM64 jobs `87802525440`/`87802525442` prove the internal deadline, job settlement, bounded replay, and verbose trace capture. Both clients stop before authentication while resolving the runner account's missing default `known_hosts`; Win32-OpenSSH ignored the workflow's overridden profile variables. Pass only the fixture's exact pinned `known_hosts` path with strict checking, keep production arguments unchanged, and require fresh native proof before selecting ConPTY or another client. Every production/default SSH path remains disconnected. Real Apple/SignPath rehearsals remain deferred.
+Session checkpoint: **VERBOSE TRACE CLASSIFIED / FIXTURE TRUST PATH ACTIVE — 2026-07-16, Codex implementation owner** — x64/ARM64 pass 101 files/863 cases with 19 skips, legacy compilation, launcher internal-timeout/replay/job-termination, unchanged runtime reproducibility, and full-size cache gates. Their 6111.6731/6395.98-millisecond traces both show `C:\\Users\\runneradmin\\.ssh\\known_hosts` lookup and stop at `hostkeys_find_by_key_hostfile` before authentication; lifecycle is `sentinel=false`, `stdoutEnded=true`, `processExit=1`, `channelClosed=true`, `closeCode=1`, and `stderrBytes=7923`. Both exact teardowns still catch locked `UsrClass.dat`. All four non-Windows primary jobs and both Linux supplements pass. No product caller, full-size Windows transfer claim, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
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) @@ -19867,15 +19867,15 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases wi - Syntax/focused evidence: `node --check config/scripts/build-windows-ssh-no-input-launcher.mjs` and `node --check config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs` pass. `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 - src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts - src/main/ssh/ssh-system-no-input-windows-openssh.test.ts - src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts - config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs - config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases with nine declared +src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases with nine declared native/live skips in 2.59 seconds real; maximum RSS is 171,491,328 bytes and peak footprint is 97,064,696 bytes. - Release contracts: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 - --reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 51 files and 285 runnable cases +--reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 51 files and 285 runnable cases with seven declared Windows skips in 13.29 seconds. - Static gates: `/usr/bin/time -l pnpm typecheck` passes in 2.86 seconds real with 1,247,576,064-byte maximum RSS and 97,343,320-byte peak footprint. `/usr/bin/time -l pnpm lint` @@ -19883,7 +19883,7 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases wi 355-entry max-lines ratchet; maximum RSS is 2,044,805,120 bytes and peak footprint is 97,163,000 bytes. Purpose-file formatting and `git diff --check` pass; `git diff --exit-code HEAD -- src/main/ssh/ssh-remote-node-resolution.ts - src/main/ssh/ssh-remote-node-resolution.test.ts` is empty. +src/main/ssh/ssh-remote-node-resolution.test.ts` is empty. - Residual boundary: local source contracts do not prove legacy .NET Framework compilation, delete-on-close behavior, overflow settlement, binary replay, job cancellation, live OpenSSH, or exact fixture teardown on Windows. Commit and push only this disconnected package, then require @@ -19942,11 +19942,11 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases wi and external-cancellation cases remain. The new native case is honestly skipped locally. - Syntax/focused evidence: both launcher scripts pass `node --check`. `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 - src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts - src/main/ssh/ssh-system-no-input-windows-openssh.test.ts - src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts - config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs - config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases with ten declared +src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases with ten declared native/live skips in 2.69 seconds real; maximum RSS is 171,212,800 bytes and peak footprint is 97,179,456 bytes. - Release/static evidence: the unquoted release-contract command passes 51 files and 285 runnable @@ -19960,6 +19960,74 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases wi package and require exact-head x64/ARM64 evidence. Do not select ConPTY, another client, a product caller, Beta, fallback, tuple, publication, default, signing, or SignPath behavior from local proof. +### E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-CI-RED-001 — native trace exposes missing fixture host-key path + +- Date/owner/source/run: 2026-07-16, Codex implementation owner; exact head + `b24dda744892c2ee43a3fb345d889ba97adb4239`, artifact run + [29554073553](https://github.com/stablyai/orca/actions/runs/29554073553), Windows x64 job + [87802525440](https://github.com/stablyai/orca/actions/runs/29554073553/job/87802525440) on + `windows-2022`, and native ARM64 job + [87802525442](https://github.com/stablyai/orca/actions/runs/29554073553/job/87802525442) on + `windows-11-arm64`; both use Node 24.18.0. +- Preserved gates: both Windows jobs pass legacy .NET launcher compilation and 101 files/863 + runnable cases with 19 declared skips, including internal diagnostic timeout, exact replay, + job-owned termination, console EOF, argv/binary/exit, output overflow/cleanup, unrelated-handle + exclusion, external cancellation, clean-build identity, runtime smoke, and both full-size cache + gates. Linux x64/ARM64 jobs `87802525448`/`87802525441`, Darwin x64/ARM64 jobs + `87802525450`/`87802525446`, and Linux oldest-userland supplements + `87803014758`/`87803014770` pass at the same exact head. +- Native lifecycle evidence: x64 exits after 6111.6731 ms and ARM64 after 6395.98 ms. Both report + `timedOut=false`, `sentinel=false`, `stdoutEnded=true`, `processExit=1`, `channelClosed=true`, + `closeCode=1`, and `stderrBytes=7923`. The false outer-timeout field is expected: the new internal + 6,000 ms launcher deadline terminates and settles the owned job, replays the bounded trace, and + returns exit 1 before the 8,000/10,000 ms external safety ceilings. +- Trace classification: both stock Win32-OpenSSH 9.5 clients establish TCP, complete key exchange, + receive the fixture's Ed25519 host key, and then try + `C:\Users\runneradmin\.ssh\known_hosts`. They stop at `hostkeys_find_by_key_hostfile` because that + default file is absent. The workflow-created pinned file is instead under the isolated + `ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_CLIENT_HOME`. Win32-OpenSSH therefore ignored the step's + overridden `HOME`/`USERPROFILE` for host-key discovery. Earlier server authentication/session + log lines came from the fixture readiness probe and do not prove this diagnostic authenticated. +- Teardown/residual: both jobs still fail visibly on the owned fixture profile's locked + `UsrClass.dat`; the full-size Windows transfer correctly remains unexecuted after the diagnostic. + This result does not justify ConPTY or an alternate client. The next single-variable correction + must pass only the fixture's exact pinned `known_hosts` path with `StrictHostKeyChecking=yes`, + never weaken trust, and keep production SSH argument construction unchanged. + +### E-M6-WINDOWS-DIAGNOSTIC-HOST-TRUST-CORRECTION-LOCAL-001 — exact pinned trust path is locally green + +- Date/owner/base/runner: 2026-07-16, Codex implementation owner; uncommitted runner-only test + correction atop exact verbose-trace head `b24dda744892c2ee43a3fb345d889ba97adb4239` on Apple arm64, + macOS 26.2 build 25C56, Node 26.5.0, and pnpm 10.24.0. +- Boundary: the live diagnostic now requires + `ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_CLIENT_HOME`, builds its exact fixture file with + `path.join(clientHome, '.ssh', 'known_hosts')`, and inserts `-vvv`, + `StrictHostKeyChecking=yes`, and `UserKnownHostsFile=` before OpenSSH's `--` + separator. The purpose contract compares the complete diagnostic vector with an independently + built production vector, rejects `StrictHostKeyChecking=no`, and proves connection reuse and all + production arguments are unchanged. +- Syntax/focused evidence: both launcher scripts pass `node --check`. + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 120 runnable cases with ten declared + native/live skips in 3.13 seconds real; maximum RSS is 174,571,520 bytes and peak footprint is + 97,113,848 bytes. +- Release/static evidence: the unquoted release-contract command passes 51 files and 285 runnable + cases with eight declared Windows skips in 16.23 seconds; maximum RSS is 196,100,096 bytes and + peak footprint is 97,179,360 bytes. `/usr/bin/time -l pnpm typecheck` passes in 3.38 seconds real + with 1,208,680,448-byte maximum RSS and 97,031,952-byte peak footprint. `/usr/bin/time -l pnpm +lint` passes in 18.59 seconds real with the 26 existing warnings, all 41 reliability gates, and + the 355-entry max-lines ratchet; maximum RSS is 2,027,831,296 bytes and peak footprint is + 97,392,520 bytes. +- Residual boundary: local execution does not prove the fixture path is accepted by Win32-OpenSSH, + authentication completes, the launcher exits naturally, full-size transfer runs, or the fixture + profile tears down. Commit/push only this correction and require exact-head x64/ARM64 evidence. + No production caller, Beta, fallback, tuple, publication, default, signing, or SignPath behavior + is authorized by this local proof. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -20018,12 +20086,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the bounded internal-timeout and verbose-output capture recorded by -`E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-LOCAL-001`, then require exact-head Windows x64 and -ARM64 legacy-compiler, launcher-timeout settlement, verbose-client phase classification, and exact -profile/directory teardown evidence. Do not -change the production Windows handle or check the broad Milestone 6 Windows system-SSH item until -both architectures select the same console-independent strategy. Do not add an +Commit and push only the explicit diagnostic fixture-trust correction recorded by +`E-M6-WINDOWS-DIAGNOSTIC-HOST-TRUST-CORRECTION-LOCAL-001`, then require exact-head Windows x64 and +ARM64 legacy-compiler, launcher, strict host-key authentication, natural client settlement, +full-size transfer, and exact profile/directory teardown evidence. Do not change the production +Windows handle or check the broad Milestone 6 Windows system-SSH item until both architectures pass +the same console-independent strategy. Do not add an Electron/startup/product importer, per-target 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/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 44b59a7bcec..a0d81e387e1 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1669,19 +1669,23 @@ queued console EOF before child creation, and shared that console with the SSH child while retaining private output pipes, exact handle inheritance, job ownership, and bounded cancellation. Native x64/arm64 prove the console handle - and zero-byte cooked EOF but the live client still hangs, falsifying stdin alone. - The next one-variable diagnostic retained that console stdin and replaced only - child stdout/stderr pipes with unique delete-on-close regular files under an - explicit per-stream byte ceiling, bounded polling/cancellation, incremental + and zero-byte cooked EOF but the live client still hangs, falsifying stdin + alone. The next one-variable diagnostic retained that console stdin and replaced + only child stdout/stderr pipes with unique delete-on-close regular files under + an explicit per-stream byte ceiling, bounded polling/cancellation, incremental post-exit replay, and exact cleanup. Native x64/arm64 prove those contracts, but the live client still hangs with the same lifecycle tuple, falsifying redirected - output pipes as the remaining cause. Before selecting ConPTY or another SSH - client, the runner-only launcher must boundedly terminate its own diagnostic - child and replay an exact verbose OpenSSH trace to classify the stuck client - phase. Do not package, sign, or add a product caller. POSIX retains ignored - stdin plus -n; fixture-owned Windows session processes settle - before profile hives are boundedly unloaded and native profile deletion begins; - MaxSessions=1; interrupted upload; stale lock + output pipes as the remaining cause. The bounded verbose trace on native + x64/arm64 then shows that Win32-OpenSSH ignores the workflow's overridden + profile variables for host-key discovery and stops before authentication while + looking for the runner account's missing default known_hosts. + Before selecting ConPTY or another SSH client, pass only the fixture's exact + pinned known_hosts path with strict host-key checking and require + both architectures to pass. Do not weaken trust, package, sign, or add a product + caller. POSIX retains ignored stdin plus -n; fixture-owned Windows + session processes settle before profile hives are boundedly unloaded and native + profile deletion begins; MaxSessions=1; interrupted upload; stale + lock Bundled Node hashes the full staged tree before sentinel write; diff --git a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts index f90acfe38f5..2b3f476ccb8 100644 --- a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +++ b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts @@ -1,4 +1,5 @@ import { spawn } from 'node:child_process' +import { join } from 'node:path' import { performance } from 'node:perf_hooks' import { describe, expect, it } from 'vitest' @@ -26,9 +27,12 @@ type DiagnosticResult = { const host = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_HOST const user = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_USER const identityFile = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_IDENTITY +const clientHome = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_CLIENT_HOME const launcherPath = process.env.ORCA_SSH_WINDOWS_NO_INPUT_LAUNCHER const port = Number.parseInt(process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_PORT ?? '', 10) -const hasLiveInput = Boolean(host && user && identityFile && launcherPath && Number.isInteger(port)) +const hasLiveInput = Boolean( + host && user && identityFile && clientHome && launcherPath && Number.isInteger(port) +) function createTarget(): SshTarget { return { @@ -44,6 +48,25 @@ function createTarget(): SshTarget { } } +function buildDiagnosticSshArgs(target: SshTarget, knownHostsPath: string): string[] { + const sshArgs = buildSshArgs(target) + const destinationSeparator = sshArgs.indexOf('--') + if (destinationSeparator < 0) { + throw new Error('Windows OpenSSH diagnostic arguments lost the destination separator') + } + // Why: Win32-OpenSSH ignores overridden profile variables; use only the fixture's pinned trust. + sshArgs.splice( + destinationSeparator, + 0, + '-vvv', + '-o', + 'StrictHostKeyChecking=yes', + '-o', + `UserKnownHostsFile=${knownHostsPath}` + ) + return sshArgs +} + async function runDiagnostic(): Promise { const sshPath = findSystemSsh() if (!sshPath) { @@ -51,14 +74,10 @@ async function runDiagnostic(): Promise { } const mode: DiagnosticMode = 'private-console-regular-output-verbose-timeout' const startedAt = performance.now() - // Why: Win32-OpenSSH still hangs with proven console EOF plus piped output, so this diagnostic - // isolates stdout/stderr as bounded regular files without changing production transport. - const sshArgs = buildSshArgs(createTarget()) - const destinationSeparator = sshArgs.indexOf('--') - if (destinationSeparator < 0) { - throw new Error('Windows OpenSSH diagnostic arguments lost the destination separator') - } - sshArgs.splice(destinationSeparator, 0, '-vvv') + const sshArgs = buildDiagnosticSshArgs( + createTarget(), + join(clientHome as string, '.ssh', 'known_hosts') + ) const child = spawn( launcherPath as string, ['--diagnostic-timeout-ms', '6000', sshPath, ...sshArgs, 'echo ORCA-SYSTEM-SSH-OK'], @@ -147,6 +166,42 @@ async function runDiagnostic(): Promise { }) } +describe('Windows OpenSSH no-input diagnostic trust arguments', () => { + it('adds only strict fixture trust and verbosity before the destination separator', () => { + const target: SshTarget = { + id: 'windows-no-input-contract', + label: 'windows-no-input-contract', + host: '127.0.0.1', + port: 22224, + username: 'fixture-user', + identityFile: 'C:\\fixture\\client-key', + identitiesOnly: true, + systemSshConnectionReuse: true, + source: 'manual' + } + const knownHostsPath = 'C:\\fixture\\client-home\\.ssh\\known_hosts' + const productionArgs = buildSshArgs(target) + const diagnosticArgs = buildDiagnosticSshArgs(target, knownHostsPath) + const destinationSeparator = productionArgs.indexOf('--') + const diagnosticOnlyArgs = [ + '-vvv', + '-o', + 'StrictHostKeyChecking=yes', + '-o', + `UserKnownHostsFile=${knownHostsPath}` + ] + + expect(diagnosticArgs).toEqual([ + ...productionArgs.slice(0, destinationSeparator), + ...diagnosticOnlyArgs, + ...productionArgs.slice(destinationSeparator) + ]) + expect(diagnosticArgs.indexOf('-vvv')).toBeLessThan(diagnosticArgs.indexOf('--')) + expect(diagnosticArgs).not.toContain('StrictHostKeyChecking=no') + expect(buildSshArgs(target)).toEqual(productionArgs) + }) +}) + describe.skipIf(!hasLiveInput)('Windows OpenSSH no-input child-handle diagnostic', () => { it( 'captures the verbose client phase after a bounded private-console diagnostic timeout',