From daffd545d75570f198db093c3ba054ae1fa033ba Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 21:27:18 -0400 Subject: [PATCH] test(ssh): isolate Windows no-input pipe EOF Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 17 ++-- ...github-release-implementation-checklist.md | 77 +++++++++++++++++-- ...6-07-14-ssh-relay-github-release-plan.html | 16 ++-- ...sh-system-no-input-windows-openssh.test.ts | 42 ++++------ 4 files changed, 105 insertions(+), 47 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 6bde62110ff..8023221f18c 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 @@ -123,12 +123,17 @@ independently returns the same lifecycle RED. The corrected POSIX contract uses `Writable` facade so ending it cannot half-close the command channel. Exact correction head `4f57b885d24ad1755a5c863eaf37369208cecedb`, artifact run `29545688003`, and Linux x64/ARM64 jobs `87777401256`/`87777401218` are green through live SFTP and restricted no-tar POSIX system -SSH. Windows x64/ARM64 jobs `87777401200`/`87777401195` independently falsify the standard -anonymous-pipe EOF hypothesis with the unchanged four-field timeout and profile RED. Next compare a -console-independent overlapped pipe against inherited stdin as a diagnostic control; do not change -the production Windows handle until native evidence selects it. The disconnected bounded diagnostic -and workflow ordering are locally green under -`E-M6-WINDOWS-NOINPUT-HANDLE-DIAGNOSTIC-LOCAL-001`; native x64/ARM64 execution is next. +SSH. Windows x64/ARM64 jobs `87777401200`/`87777401195` reproduce the unchanged four-field timeout +with a standard pipe plus mandatory `-n`. Exact diagnostic head +`da938a7405d0a6c760979358a6c0a38030d9acaf`, artifact run `29546702916`, and Windows x64/ARM64 jobs +`87780486286`/`87780486259` prove that inherited and overlapped handles also time out when every +candidate retains `-n`; the invalid inherited control and shared `-n` mean these runs do not isolate +the child handle. `E-M6-WINDOWS-NOINPUT-HANDLE-DIAGNOSTIC-CI-RED-001` therefore requires the next +single-variable diagnostic to use the upstream-recommended standard stdin pipe with immediate EOF +and omit `-n` only on Windows. POSIX retains ignored stdin plus `-n`; production stays unchanged +until x64 and ARM64 select the same console-independent strategy. The replacement disconnected +diagnostic is locally green under `E-M6-WINDOWS-NOINPUT-PIPE-NO-N-LOCAL-001`; commit and run that +exact head next. `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 a0c6408218d..d29ff215318 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
Last updated: 2026-07-15
Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: native Windows no-input handle qualification**. 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: **POSIX EOF CORRECTION NATIVELY GREEN / STANDARD WINDOWS PIPE NATIVELY RED / HANDLE DIAGNOSTIC LOCALLY GREEN — 2026-07-15, Codex implementation owner** — exact correction head `4f57b885d24ad1755a5c863eaf37369208cecedb`, artifact run `29545688003`, and Linux x64/ARM64 jobs `87777401256`/`87777401218` prove the separate no-input facade through live SFTP and restricted no-tar POSIX system SSH with bounded full-size transfer, four-file concurrency, cancellation, and cleanup. Windows x64/ARM64 jobs `87777401200`/`87777401195` independently falsify the standard anonymous-pipe EOF hypothesis: official OpenSSH authenticates and the remote command closes, but clients time out after 31.36/31.82 seconds with `sentinel=false`, `stdoutEnded=false`, `processExit=not-observed`, and `channelClosed=false`; exact profile teardown remains RED. `E-M6-WINDOWS-NOINPUT-HANDLE-DIAGNOSTIC-LOCAL-001` adds a bounded, non-sensitive native comparison of a console-independent overlapped pipe and inherited-stdin upstream control before the unchanged full-size measurement; focused 117+2, 283-contract, typecheck, full lint/reliability/max-lines, all three PowerShell parse, formatting, diff, and protected-resolver gates pass locally. Commit/push and require native x64/ARM64 diagnostic output before selecting another production handle. Do not advance Milestone 6. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
+Session checkpoint: **POSIX EOF CORRECTION NATIVELY GREEN / WINDOWS `-n` HANDLE DIAGNOSTIC NATIVELY RED / NO-`-n` PIPE DIAGNOSTIC LOCALLY GREEN — 2026-07-15, Codex implementation owner** — exact correction head `4f57b885d24ad1755a5c863eaf37369208cecedb`, artifact run `29545688003`, and Linux x64/ARM64 jobs `87777401256`/`87777401218` prove the separate no-input facade through live SFTP and restricted no-tar POSIX system SSH with bounded full-size transfer, four-file concurrency, cancellation, and cleanup. Windows x64/ARM64 jobs `87777401200`/`87777401195` reproduce the no-input hang with a standard pipe plus mandatory `-n`. Exact diagnostic head `da938a7405d0a6c760979358a6c0a38030d9acaf`, artifact run `29546702916`, and Windows x64/ARM64 jobs `87780486286`/`87780486259` prove that inherited and overlapped handles also time out when every candidate retains `-n`; both inherited controls are invalid on redirected hosted-runner stdin, so the runs do not isolate the child handle. Each x64 attempt reports 8.03/8.02 seconds and each ARM64 attempt 8.04/8.02 seconds with `sentinel=false`, `stdoutEnded=true`, termination-only null exit/close codes, zero stderr bytes, and exact profile teardown RED. `E-M6-WINDOWS-NOINPUT-PIPE-NO-N-LOCAL-001` replaces only the disconnected diagnostic with the upstream-recommended standard stdin pipe, immediate parent EOF, ordinary stdout/stderr, and no `-n`; focused 117+2, 283-contract, format, typecheck, full lint/reliability/max-lines, diff, and protected-resolver gates pass locally. Commit/push and require a fresh exact-head x64/ARM64 result next. Do not change production or advance Milestone 6 without both native results. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. 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) @@ -19433,6 +19433,69 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two de - Residual boundary: local macOS intentionally skips the live case. Require exact-head native x64 and ARM64 output before selecting any Windows handle; keep profile teardown, full-size transfer, default behavior, fallback, Beta, tuple publication, and SignPath separately gated. +- Native proof result: exact diagnostic head `da938a7405d0a6c760979358a6c0a38030d9acaf`, artifact run + `29546702916`, Windows x64 job `87780486286`, and Windows ARM64 job `87780486259` are RED as + recorded by `E-M6-WINDOWS-NOINPUT-HANDLE-DIAGNOSTIC-CI-RED-001`; the workflow correctly stops + before the following unchanged product full-size result. + +### E-M6-WINDOWS-NOINPUT-HANDLE-DIAGNOSTIC-CI-RED-001 — mandatory `-n` invalidates every hosted handle control + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact pushed head + `da938a7405d0a6c760979358a6c0a38030d9acaf`, artifact run `29546702916`, Windows x64 job + `87780486286` on `windows-2022`, and Windows ARM64 job `87780486259` on `windows-11-arm`. +- Exact log command: `gh api repos/stablyai/orca/actions/jobs//logs | rg -n -C 10 +'ssh_windows_no_input_handle_diagnostic|Windows no-input handle diagnostic failed|profile +teardown:|UsrClass.dat'` for each job. +- X64 result: inherited and overlapped attempts both return `success=false`, `timedOut=true`, + `sentinel=false`, `stdoutEnded=true`, `processExit=null`, `channelClosed=true`, `closeCode=null`, + and `stderrBytes=0` after 8029.88/8021.57 milliseconds. ARM64 independently returns the same + fields after 8044.18/8015.22 milliseconds. These exit/close fields are termination settlement, + not remote-command success. +- Teardown result: both exact jobs fail `_Classes` hive unload, retain locked `UsrClass.dat`, and + retain the fixture profile directory. The workflow correctly stops before the unchanged product + full-size test; no Windows transport cell is green and no tuple is enabled. +- Interpretation: the inherited control is invalid because hosted-runner stdin is itself redirected. + More importantly, inherited, standard-pipe, and overlapped candidates all retained OpenSSH `-n`, + which routes Windows no-input handling through the same null-input boundary implicated by + Win32-OpenSSH #856/#1330. These runs cannot reject the child pipe independently of `-n`. +- Course correction: before production changes, run one bounded single-variable diagnostic using + Node's standard stdin pipe, destroy the parent writer immediately, keep stdout/stderr as ordinary + pipes, and omit `-n` only for that Windows attempt. This is the upstream-recommended pipe EOF + boundary without the confounding null-input option. Test an overlapped stdin pipe without `-n` + only if the standard pipe remains RED. POSIX keeps ignored stdin plus `-n`. +- Residual boundary: require exact-head x64 and ARM64 lifecycle and exact teardown proof. Keep the + product handle, default, legacy fallback, Beta, tuple publication, and SignPath unchanged. + +### E-M6-WINDOWS-NOINPUT-PIPE-NO-N-LOCAL-001 — unconfounded standard-pipe EOF diagnostic is locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted replacement atop exact + RED head `da938a7405d0a6c760979358a6c0a38030d9acaf` on Apple arm64, macOS 26.2 build 25C56, + Node 26.5.0, pnpm 10.24.0. +- Diagnostic boundary: the disconnected Windows live test now spawns `ssh.exe` with standard stdin, + stdout, and stderr pipes, destroys the parent stdin writer immediately, and deliberately calls + `buildSshArgs` without `noInput` so OpenSSH receives no `-n`. The 8-second timeout and 10-second + hard settlement bound, non-sensitive lifecycle fields, workflow ordering, and fail-closed step + remain unchanged. Production args, handles, no-input facade, and product full-size test are not + changed. +- Focused command: `/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-workflow.test.mjs` passes 117 cases with two expected native + skips in 4.07 seconds real; maximum RSS is 168,067,072 bytes and peak footprint 97,589,080 bytes. +- Release contracts: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 --reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 50 files and + 283/283 cases in 25.92 seconds real; maximum RSS is 195,493,888 bytes and peak footprint + 97,687,288 bytes. +- Static gates: `/usr/bin/time -l pnpm typecheck` passes in 5.73 seconds real with maximum RSS + 1,225,736,192 bytes and peak footprint 96,687,936 bytes. `/usr/bin/time -l pnpm lint` passes in + 24.85 seconds real with the same 26 pre-existing warnings, all 41 reliability gates, and the + 355-entry max-lines ratchet; maximum RSS is 2,055,323,648 bytes and peak footprint 98,260,872 + bytes. `pnpm exec oxfmt --check `, `git diff --check`, and the protected + resolver zero-diff gate pass after formatting. +- Residual boundary: local macOS skips both native live cases. Commit the diagnostic and evidence, + then require a fresh exact-head artifact run with the same successful lifecycle on Windows x64 + and ARM64 plus exact profile/directory teardown before any production handle change. ## Accepted Gaps @@ -19492,12 +19555,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Implement the purpose-named disconnected Windows no-input handle diagnostic required by -`E-M6-WINDOWS-NOINPUT-STANDARD-PIPE-CI-RED-001`. Compare an overlapped pipe with immediate parent -destruction against inherited stdin, keep each attempt bounded, and record sentinel/stdout-end/ -process-exit/channel-close on native x64 and ARM64. Do not change the production Windows handle or -check the broad Milestone 6 Windows system-SSH item until the diagnostic selects a console-independent -strategy. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple +Commit and push the disconnected no-`-n` standard-pipe diagnostic recorded by +`E-M6-WINDOWS-NOINPUT-PIPE-NO-N-LOCAL-001`, then run the exact head on Windows x64 and ARM64 and +record sentinel/stdout-end/process-exit/channel-close plus exact profile/directory teardown. 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 +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 behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, 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 9a1e9a8c39a..9d12a8dc7cb 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 @@ -1646,13 +1646,15 @@ stdout-end, sentinel, and channel-close settlement; POSIX no-input probes use an OS-level ignored stdin handle plus OpenSSH -n; Windows must not map stdin to NUL because Win32-OpenSSH issues #856/#1330 document a - redirected-handle hang; a standard anonymous pipe with its parent write end - destroyed is also rejected by native x64/arm64 evidence; qualify a - console-independent overlapped-pipe EOF strategy against an inherited-stdin - control before selecting the Windows handle, always with -n; - fixture-owned Windows session processes settle before profile hives are - boundedly unloaded and native profile deletion begins; - MaxSessions=1; interrupted upload; stale lock + redirected-handle hang; native x64/arm64 evidence also proves that inherited, + standard-pipe, and overlapped-pipe handles all hang when the Windows client is + simultaneously forced through -n, so those runs do not isolate the + child handle. Qualify the upstream-recommended standard-pipe EOF with Windows + -n omitted as the next single-variable diagnostic; only test an + overlapped stdin pipe, also without -n, if the standard pipe fails. + 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 94aa511423a..ed3bf45f24b 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 @@ -7,7 +7,7 @@ import type { SshTarget } from '../../shared/ssh-types' import { buildSshArgs } from './system-ssh-args' import { findSystemSsh } from './system-ssh-binary' -type DiagnosticMode = 'inherited' | 'overlapped' +type DiagnosticMode = 'pipe-eof-no-n' type DiagnosticResult = { mode: DiagnosticMode @@ -42,28 +42,20 @@ function createTarget(): SshTarget { } } -async function runDiagnostic(mode: DiagnosticMode): Promise { +async function runDiagnostic(): Promise { const sshPath = findSystemSsh() if (!sshPath) { throw new Error('Native Windows OpenSSH client is unavailable') } + const mode: DiagnosticMode = 'pipe-eof-no-n' const startedAt = performance.now() - const child = spawn( - sshPath, - [...buildSshArgs(createTarget(), { noInput: true }), 'echo ORCA-SYSTEM-SSH-OK'], - { - // Why: qualify a console-independent overlapped pipe against the - // upstream inherited-handle control before changing production. - stdio: - mode === 'overlapped' - ? ['overlapped', 'overlapped', 'overlapped'] - : ['inherit', 'pipe', 'pipe'], - windowsHide: true - } - ) - if (mode === 'overlapped') { - child.stdin?.destroy() - } + const child = spawn(sshPath, [...buildSshArgs(createTarget()), 'echo ORCA-SYSTEM-SSH-OK'], { + // Why: isolate the upstream-recommended pipe EOF from Win32-OpenSSH's + // null-input path before changing the production no-input command. + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true + }) + child.stdin?.destroy() return new Promise((resolve, reject) => { let stdout = '' @@ -141,18 +133,14 @@ async function runDiagnostic(mode: DiagnosticMode): Promise { describe.skipIf(!hasLiveInput)('Windows OpenSSH no-input child-handle diagnostic', () => { it( - 'compares inherited stdin with a console-independent overlapped EOF pipe', - { timeout: 30_000 }, + 'qualifies a standard EOF pipe without the Windows null-input option', + { timeout: 15_000 }, async () => { expect(process.platform).toBe('win32') - const inherited = await runDiagnostic('inherited') - const overlapped = await runDiagnostic('overlapped') - console.log( - `ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ inherited, overlapped })}` - ) + const pipeEofNoN = await runDiagnostic() + console.log(`ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ pipeEofNoN })}`) - // The inherited control must settle or this fixture cannot distinguish handle strategies. - expect(inherited.success).toBe(true) + expect(pipeEofNoN.success).toBe(true) } ) })