From 8ae6dcf6450b5f081788357dd8fe776293fb0cfe Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 19:54:23 -0400 Subject: [PATCH] fix(ssh): detach Windows no-input probe Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 31 +++++++ ...time-windows-openssh-workflow-contract.mjs | 93 +++++++++++++++++++ .../ssh-relay-runtime-workflow.test.mjs | 81 +--------------- ...elease-implementation-checklist-summary.md | 24 ++++- ...github-release-implementation-checklist.md | 81 +++++++++++++++- ...6-07-14-ssh-relay-github-release-plan.html | 5 +- src/main/ssh/ssh-connection.test.ts | 7 +- src/main/ssh/ssh-connection.ts | 8 +- src/main/ssh/ssh-system-fallback.test.ts | 17 ++++ src/main/ssh/system-ssh-args.ts | 9 ++ 10 files changed, 269 insertions(+), 87 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 15aec733174..462649ee579 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -1697,9 +1697,40 @@ jobs: if (Test-Path -LiteralPath $log) { Get-Content -LiteralPath $log -Tail 200 } try { if ($ownsAccount -and $userSid) { + function Dismount-FixtureRegistryHive([string]$hiveName) { + $registryPath = "Registry::HKEY_USERS\$hiveName" + if (-not (Test-Path -LiteralPath $registryPath)) { return } + $regPath = Join-Path $env:SystemRoot 'System32/reg.exe' + $process = Start-Process -FilePath $regPath ` + -ArgumentList @('unload', "HKU\$hiveName") -PassThru -WindowStyle Hidden + try { + if (-not $process.WaitForExit(2000)) { + $process.Kill() + if (-not $process.WaitForExit(2000)) { + throw "Fixture registry hive unload did not stop: $hiveName" + } + throw "Fixture registry hive unload timed out: $hiveName" + } + if ($process.ExitCode -ne 0) { + throw "Fixture registry hive unload failed ($($process.ExitCode)): $hiveName" + } + } finally { + $process.Dispose() + } + } $escapedSid = $userSid.Replace("'", "''") $profileFailure = $null for ($attempt = 1; $attempt -le 20; $attempt += 1) { + try { + # Why: the fixture's UsrClass hive can outlive sshd; unload only + # this owned SID before asking Windows to delete its profile. + Dismount-FixtureRegistryHive "${userSid}_Classes" + Dismount-FixtureRegistryHive $userSid + } catch { + $profileFailure = $_.Exception.Message + Start-Sleep -Milliseconds 500 + continue + } $profile = Get-CimInstance Win32_UserProfile -Filter "SID='$escapedSid'" ` -ErrorAction SilentlyContinue if (-not $profile) { break } diff --git a/config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs b/config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs new file mode 100644 index 00000000000..fd0569834dd --- /dev/null +++ b/config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs @@ -0,0 +1,93 @@ +export function assertWindowsOpenSshWorkflow(workflow, expect) { + const steps = workflow.jobs['build-windows-runtime'].steps + const start = steps.find( + (step) => step.name === 'Start loopback Windows OpenSSH system-SSH fixture' + ) + const measure = steps.find( + (step) => step.name === 'Measure exact full-size runtime over Windows system SSH' + ) + const stop = steps.find( + (step) => step.name === 'Stop loopback Windows OpenSSH system-SSH fixture' + ) + + expect(start).toBeDefined() + expect(measure).toBeDefined() + expect(stop).toBeDefined() + expect(start.shell).toBe('pwsh') + expect(start.run).toContain('10.0.0.0p2-Preview') + expect(start.run).toContain('23f50f3458c4c5d0b12217c6a5ddfde0137210a30fa870e98b29827f7b43aba5') + expect(start.run).toContain('698c6aec31c1dd0fb996206e8741f4531a97355686b5431ef347d531b07fcd42') + expect(start.run).toContain('4652e861c0335ee80a51306ceab75aa35c8865b235f97ce7dd5a0fd9dab44b5d') + expect(start.run).toContain('7ad2b7721893c54ad6e4fec1a3477701fb48975323c2c4ac6cd0b8c972ab242a') + expect(start.run).toContain('--connect-timeout 20 --max-time 300 --retry 2') + expect(start.run).toContain('Get-AuthenticodeSignature') + expect(start.run).toContain('$actualNativeNames -join "`n"') + expect(start.run).toContain('$expectedNativeNames -join "`n"') + expect(start.run).toContain("'ssh-sk-helper.exe'") + expect(start.run).toContain("'sshd-session.exe'") + expect(start.run).toContain('$libcryptoFiles.Count -ne 1') + expect(start.run).toContain("$libcryptoSignature.Status -ne 'Valid'") + expect(start.run).toContain('587116075365AA15BCD8E4FA9CB31BE372B5DE51') + expect(start.run).toContain('CN=Microsoft 3rd Party Application Component') + expect(start.run).toContain('F5877012FBD62FABCBDC8D8CEE9C9585BA30DF79') + expect(start.run).toContain('3F56A45111684D454E231CFDC4DA5C8D370F9816') + expect(start.run).toContain('$expectedExecutableSignerThumbprints -notcontains $thumbprint') + expect(start.run).not.toContain("'NotSigned'") + expect(start.run).toContain("if ($nativeFile.Name -eq 'libcrypto.dll') { continue }") + expect(start.run).toContain("$signature.Status -ne 'Valid'") + expect(start.run).toContain('CN=Microsoft Corporation') + expect(start.run).toContain('[Security.AccessControl.DirectorySecurity]::new()') + expect(start.run).toContain('[Security.AccessControl.FileSecurity]::new()') + expect(start.run).toContain('$security.SetAccessRuleProtection($true, $false)') + expect(start.run).toContain('[void]$security.AddAccessRule($accessRule)') + expect(start.run).toContain('$acl.AreAccessRulesProtected') + expect(start.run).toContain('Fixture ACL trustee closure changed') + expect(start.run).toContain('@($systemSid, $administratorsSid)') + expect(start.run).not.toContain('Add-WindowsCapability') + expect(start.run).toContain('New-LocalUser') + expect(start.run.indexOf("Join-Path $fixture 'account-owned'")).toBeLessThan( + start.run.indexOf('New-LocalUser') + ) + expect(start.run).toContain('ListenAddress 127.0.0.1') + expect(start.run).toContain('PasswordAuthentication no') + expect(start.run).toContain('StrictModes yes') + expect(start.run).toContain('StrictHostKeyChecking=yes') + expect(start.run).toContain('powershell.exe') + expect(start.run).toContain('([Version]$remotePowerShellVersion).Major -ne 5') + expect(start.run).toContain('/setowner') + expect(start.run).toContain("Set-FixtureOwner $hostKey 'S-1-5-18'") + expect(start.run).toContain('Set-FixtureOwner $authorizedKeys $userSid') + expect(start.run).not.toContain('administrators_authorized_keys') + expect(start.run).toContain('New-Service') + expect(start.run.indexOf("Join-Path $fixture 'service-owned'")).toBeLessThan( + start.run.indexOf('New-Service') + ) + expect(measure.shell).toBe('pwsh') + expect(measure.run).toContain("$env:ORCA_SSH_FORCE_SYSTEM_TRANSPORT = '1'") + expect(measure.run).toContain('ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts') + expect(measure.run).toContain("Join-Path $firstOutput 'runtime'") + expect(stop.if).toBe("always() && runner.os == 'Windows'") + expect(stop.shell).toBe('pwsh') + expect(stop.run).toContain('fixture-owned') + expect(stop.run).toContain('service-owned') + expect(stop.run).toContain('account-owned') + expect(stop.run).not.toContain('service-backup.json') + expect(stop.run).not.toContain('sc.exe config') + expect(stop.run).toContain('sc.exe delete') + expect(stop.run).toContain('Dismount-FixtureRegistryHive "${userSid}_Classes"') + expect(stop.run).toContain('Dismount-FixtureRegistryHive $userSid') + expect(stop.run).toContain("Join-Path $env:SystemRoot 'System32/reg.exe'") + expect(stop.run).toContain('@(\'unload\', "HKU\\$hiveName")') + expect(stop.run).toContain('$process.WaitForExit(2000)') + expect(stop.run).toContain('$process.Kill()') + expect(stop.run.indexOf('Dismount-FixtureRegistryHive "${userSid}_Classes"')).toBeLessThan( + stop.run.indexOf('Dismount-FixtureRegistryHive $userSid') + ) + expect(stop.run.indexOf('Dismount-FixtureRegistryHive $userSid')).toBeLessThan( + stop.run.indexOf('Remove-CimInstance -InputObject $profile') + ) + expect(stop.run).toContain('Remove-CimInstance -InputObject $profile') + expect(stop.run).toContain('Fixture profile did not settle within 10 seconds') + expect(stop.run).toContain('fixture profile directory remains') + expect(stop.run).toContain('Remove-LocalUser') +} diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 273c61b10d0..d7f964b688a 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises' import { parse } from 'yaml' import { describe, expect, it } from 'vitest' +import { assertWindowsOpenSshWorkflow } from './ssh-relay-runtime-windows-openssh-workflow-contract.mjs' const workflowUrl = new URL( '../../.github/workflows/ssh-relay-runtime-artifacts.yml', @@ -142,85 +143,7 @@ describe('SSH relay runtime artifact workflow', () => { it('runs full-size Windows system SSH against loopback native OpenSSH', async () => { const workflow = parse(await readFile(workflowUrl, 'utf8')) - const steps = workflow.jobs['build-windows-runtime'].steps - const start = steps.find( - (step) => step.name === 'Start loopback Windows OpenSSH system-SSH fixture' - ) - const measure = steps.find( - (step) => step.name === 'Measure exact full-size runtime over Windows system SSH' - ) - const stop = steps.find( - (step) => step.name === 'Stop loopback Windows OpenSSH system-SSH fixture' - ) - - expect(start).toBeDefined() - expect(measure).toBeDefined() - expect(stop).toBeDefined() - expect(start.shell).toBe('pwsh') - expect(start.run).toContain('10.0.0.0p2-Preview') - expect(start.run).toContain('23f50f3458c4c5d0b12217c6a5ddfde0137210a30fa870e98b29827f7b43aba5') - expect(start.run).toContain('698c6aec31c1dd0fb996206e8741f4531a97355686b5431ef347d531b07fcd42') - expect(start.run).toContain('4652e861c0335ee80a51306ceab75aa35c8865b235f97ce7dd5a0fd9dab44b5d') - expect(start.run).toContain('7ad2b7721893c54ad6e4fec1a3477701fb48975323c2c4ac6cd0b8c972ab242a') - expect(start.run).toContain('--connect-timeout 20 --max-time 300 --retry 2') - expect(start.run).toContain('Get-AuthenticodeSignature') - expect(start.run).toContain('$actualNativeNames -join "`n"') - expect(start.run).toContain('$expectedNativeNames -join "`n"') - expect(start.run).toContain("'ssh-sk-helper.exe'") - expect(start.run).toContain("'sshd-session.exe'") - expect(start.run).toContain('$libcryptoFiles.Count -ne 1') - expect(start.run).toContain("$libcryptoSignature.Status -ne 'Valid'") - expect(start.run).toContain('587116075365AA15BCD8E4FA9CB31BE372B5DE51') - expect(start.run).toContain('CN=Microsoft 3rd Party Application Component') - expect(start.run).toContain('F5877012FBD62FABCBDC8D8CEE9C9585BA30DF79') - expect(start.run).toContain('3F56A45111684D454E231CFDC4DA5C8D370F9816') - expect(start.run).toContain('$expectedExecutableSignerThumbprints -notcontains $thumbprint') - expect(start.run).not.toContain("'NotSigned'") - expect(start.run).toContain("if ($nativeFile.Name -eq 'libcrypto.dll') { continue }") - expect(start.run).toContain("$signature.Status -ne 'Valid'") - expect(start.run).toContain('CN=Microsoft Corporation') - expect(start.run).toContain('[Security.AccessControl.DirectorySecurity]::new()') - expect(start.run).toContain('[Security.AccessControl.FileSecurity]::new()') - expect(start.run).toContain('$security.SetAccessRuleProtection($true, $false)') - expect(start.run).toContain('[void]$security.AddAccessRule($accessRule)') - expect(start.run).toContain('$acl.AreAccessRulesProtected') - expect(start.run).toContain('Fixture ACL trustee closure changed') - expect(start.run).toContain('@($systemSid, $administratorsSid)') - expect(start.run).not.toContain('Add-WindowsCapability') - expect(start.run).toContain('New-LocalUser') - expect(start.run.indexOf("Join-Path $fixture 'account-owned'")).toBeLessThan( - start.run.indexOf('New-LocalUser') - ) - expect(start.run).toContain('ListenAddress 127.0.0.1') - expect(start.run).toContain('PasswordAuthentication no') - expect(start.run).toContain('StrictModes yes') - expect(start.run).toContain('StrictHostKeyChecking=yes') - expect(start.run).toContain('powershell.exe') - expect(start.run).toContain('([Version]$remotePowerShellVersion).Major -ne 5') - expect(start.run).toContain('/setowner') - expect(start.run).toContain("Set-FixtureOwner $hostKey 'S-1-5-18'") - expect(start.run).toContain('Set-FixtureOwner $authorizedKeys $userSid') - expect(start.run).not.toContain('administrators_authorized_keys') - expect(start.run).toContain('New-Service') - expect(start.run.indexOf("Join-Path $fixture 'service-owned'")).toBeLessThan( - start.run.indexOf('New-Service') - ) - expect(measure.shell).toBe('pwsh') - expect(measure.run).toContain("$env:ORCA_SSH_FORCE_SYSTEM_TRANSPORT = '1'") - expect(measure.run).toContain('ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts') - expect(measure.run).toContain("Join-Path $firstOutput 'runtime'") - expect(stop.if).toBe("always() && runner.os == 'Windows'") - expect(stop.shell).toBe('pwsh') - expect(stop.run).toContain('fixture-owned') - expect(stop.run).toContain('service-owned') - expect(stop.run).toContain('account-owned') - expect(stop.run).not.toContain('service-backup.json') - expect(stop.run).not.toContain('sc.exe config') - expect(stop.run).toContain('sc.exe delete') - expect(stop.run).toContain('Remove-CimInstance -InputObject $profile') - expect(stop.run).toContain('Fixture profile did not settle within 10 seconds') - expect(stop.run).toContain('fixture profile directory remains') - expect(stop.run).toContain('Remove-LocalUser') + assertWindowsOpenSshWorkflow(workflow, expect) }) it('uploads only the first output after two clean builds verify and compare', async () => { 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 3cb25f2833a..a6ab90cec81 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 @@ -66,8 +66,28 @@ are atomically replaced with exact protected allow-only rules independent of dri owned `Win32_UserProfile` is deleted through a bounded native lifecycle before account/filesystem teardown. Focused tests pass 71 cases with one declared live skip, all three PowerShell blocks parse, all 283 artifact contracts pass, typecheck and full lint/reliability/max-lines pass, formatting/diff -are clean, and the protected resolver files remain unchanged. Commit/push this isolated package and -require fresh x64/arm64 live proof before advancing. +are clean, and the protected resolver files remain unchanged. Exact head +`b652c1ebcce3803443d84c89811146366cdbdd5b` is pushed. Artifact run `29542292712` preserves all +non-Windows gates and independently reaches the Windows fixture on x64, but job `87766845800` is +RED: exact ACL replacement, official OpenSSH setup, authentication, and PowerShell 5.1 pass before +the production probe times out after 32.064 seconds. Closing the Node stdin pipe is insufficient; +the no-input probe needs native OpenSSH `-n` without disabling connection reuse. Teardown also +finds the owned fixture SID's `UsrClass.dat` hive still loaded after the bounded profile-removal +loop. Add probe-only `-n`, boundedly unload only that SID's `_Classes` and primary hives before +retrying `Win32_UserProfile` removal, and require fresh x64/arm64 live proof before advancing. +ARM64 job `87766845741` independently reaches the same boundary: official OpenSSH setup passes, +the production probe times out after 34.01 seconds, and teardown catches the same owned +`UsrClass.dat` lock after the bounded profile loop. This is a shared native OpenSSH/profile +lifecycle correction, not an x64-only exception. +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-NOINPUT-PROFILE-CORRECTION-LOCAL-001` records the narrow +correction locally green: only the no-input connection probe emits OpenSSH `-n` before `--`, reuse +flags remain enabled, ordinary commands exclude `-n`, and teardown boundedly unloads only the +fixture SID's `_Classes` and primary hives before retrying native profile deletion. The Windows +workflow oracle is split by concrete domain responsibility without a max-lines bypass. Focused +tests pass 115 cases with one declared native skip, all three PowerShell blocks parse, 283/283 +artifact contracts pass, typecheck and full lint/reliability/max-lines pass, formatting/diff are +clean, and the protected resolver files remain unchanged. Commit/push this isolated package and +require fresh native x64/arm64 proof before advancing. `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 16d1c3936a4..e02b21f3f04 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: exact-head Windows OpenSSH live-proof diagnosis and fixture correction**. 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 ACL/PROBE/PROFILE CORRECTION LOCALLY GREEN / EXACT-HEAD PROOF NEXT — 2026-07-15, Codex implementation owner** — exact pushed head `d28552f0d953e40d96a4c24cdcdb5ba7d85b7c9f`, artifact run `29541119819`, and x64/arm64 jobs `87763404467`/`87763404492` independently pass checkout/toolchain, 283 contracts, clean-build equality/smoke, full-size cache, both pinned archive/library hashes, exact 15-file closure, and every audited signer check before exposing three narrower gaps. ARM64's C: fixture root retains explicit Authenticated Users and Users ACEs; x64's no-input production `ssh.exe` probe leaves piped stdin open and waits 30 seconds after the remote command closes; x64 teardown finds the owned profile's loaded `UsrClass.dat`. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-EXACT-HEAD-CORRECTION-LOCAL-001` atomically replaces each fixture DACL with exact protected allow-only rules, closes stdin for the no-input production probe without disabling connection reuse, and deletes the owned `Win32_UserProfile` through a bounded native lifecycle before account/filesystem cleanup. Focused, PowerShell syntax, 283-contract, typecheck, full lint/reliability/max-lines, formatting, diff, and protected-resolver isolation gates pass locally. Commit/push and require fresh x64/arm64 live proof. 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: **WINDOWS NATIVE NO-INPUT/PROFILE-HIVE CORRECTION LOCALLY GREEN / EXACT-HEAD PROOF NEXT — 2026-07-15, Codex implementation owner** — exact head `b652c1ebcce3803443d84c89811146366cdbdd5b`, artifact run `29542292712`, x64 job `87766845800`, and ARM64 job `87766845741` preserve all prior artifact/build/cache/hash/closure/signer gates and independently expose the same native probe/profile gaps. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-NOINPUT-PROFILE-CORRECTION-LOCAL-001` adds probe-only OpenSSH `-n` before `--` while retaining connection reuse and excluding `-n` from ordinary commands; boundedly unloads only the fixture-owned SID's `_Classes` and primary hives before retrying native profile deletion; retains exact teardown assertions; and splits the Windows workflow oracle by concrete domain responsibility without a max-lines bypass. Focused tests, all three PowerShell parses, 283 artifact contracts, typecheck, full lint/reliability/max-lines, formatting, diff, and protected-resolver isolation pass locally. Commit/push this isolated package and require fresh x64/arm64 proof. 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) @@ -19092,6 +19092,85 @@ config/scripts/ssh-relay-runtime-*.test.mjs` passes 50 files and 283/283 cases i exact-head x64/arm64 proof before closing the live Windows gate. Legacy remains the sole production/default path; product/Beta/settings/fallback/tuple/publication/signing remain absent. +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-NOINPUT-PROFILE-RED-001 — exact head proves ACL and exposes native no-input/profile-hive gaps + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact pushed head + `b652c1ebcce3803443d84c89811146366cdbdd5b`, artifact run `29542292712`, native x64 job + `87766845800` (`windows-2022`, Windows Server 2022) and native ARM64 job `87766845741` + (`windows-11-arm`). +- Preserved evidence: all four non-Windows primary jobs and both Linux supplement jobs pass. The + x64 job passes exact checkout/toolchain, 283 runtime artifact contracts, clean-build identity and + smoke, full-size cache, both pinned archive/library hashes, exact 15-file closure, all audited + signer checks, exact fixture DACL replacement, official OpenSSH service/authentication, and + PowerShell 5.1 before the live transfer suite. +- X64 RED: the production no-input connection probe times out after 32.064 seconds with + `System SSH connection timed out`. Closing the spawned process's Node stdin pipe does not make + native Windows OpenSSH detach its stdin worker. Preserve `systemSshConnectionReuse`; add + OpenSSH `-n` only to the no-input probe argv before `--`, and prove argv order plus connection + behavior. +- Teardown RED: the bounded ten-second `Remove-CimInstance Win32_UserProfile` loop cannot settle + while the owned fixture profile's `UsrClass.dat` remains loaded; profile and fixture directory + deletion then fail and exact absence assertions catch both. For the fixture-owned SID only, + boundedly unload `HKU\\<SID>_Classes` and then `HKU\\<SID>` with `reg.exe unload`, retry + native profile deletion, and retain exact account/profile/directory absence assertions. +- ARM64 independent RED: official OpenSSH fixture startup passes, then the same production probe + fails with `System SSH connection timed out` after 34.01 seconds. The teardown reports + `Fixture profile did not settle within 10 seconds`, then exact filesystem deletion identifies + `C:\Users\orca_ssh_fixture\AppData\Local\Microsoft\Windows\UsrClass.dat` as used by another + process and reports that the fixture profile directory remains. The same correction is therefore + required on both native Windows architectures. +- Promotion condition: update the workflow oracle, parse every PowerShell block, pass the focused, + 283-contract, typecheck, full-lint/reliability/max-lines, formatting, diff, and protected-resolver + isolation gates, then require fresh exact-head x64/arm64 live proof. Legacy remains the sole + production/default path and SignPath remains deferred. + +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-NOINPUT-PROFILE-CORRECTION-LOCAL-001 — probe-only native no-input and owned-hive teardown are locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted correction atop exact + RED head `b652c1ebcce3803443d84c89811146366cdbdd5b` on Apple arm64, macOS 26.2 build 25C56, + Node 26.5.0, pnpm 10.24.0. Native Windows execution remains required. +- Probe correction: `SystemSshBuildArgsOptions.noInput` emits OpenSSH `-n` before the `--` + destination terminator. Only the raw cross-shell connection probe requests it; ordinary + system-SSH commands are asserted to omit it. The probe still requests Orca ControlMaster, + `ControlPersist=300`, and keepalives, so this does not evade the required connection-reuse path. + The existing explicit stdin close remains as deterministic local pipe ownership. +- Teardown correction: for the fixture-created account SID only, the workflow checks and unloads + `HKU\\<SID>_Classes` before `HKU\\<SID>` through `%SystemRoot%\\System32\\reg.exe`. + Every process wait is bounded to two seconds, a stuck process is killed and boundedly awaited, + and failure is retried inside the existing ten-second profile lifecycle. Native + `Remove-CimInstance Win32_UserProfile`, account deletion, directory deletion, and all exact + absence assertions remain mandatory. +- Structure/oracle correction: Windows OpenSSH workflow assertions moved into the concrete + `config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs` responsibility while + `ssh-relay-runtime-workflow.test.mjs` remains the caller used by every historical command. The + files are 93 and 551 lines respectively; no max-lines disable or budget bump was added. +- 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-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 3 files plus one declared native live + skip, 115 passed tests plus one skip, in 4.24 seconds real; maximum RSS is 158,269,440 bytes and + peak footprint is 97,392,376 bytes. Independent YAML extraction plus PowerShell + `[scriptblock]::Create()` parses all three start/measure/stop blocks. +- Release-contract command: `/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 21.65 seconds + real; maximum RSS is 195,526,656 bytes and peak footprint is 98,375,440 bytes. +- Static commands: `/usr/bin/time -l pnpm typecheck` passes in 6.75 seconds real with maximum RSS + 1,826,357,248 bytes and peak footprint 98,015,040 bytes. `/usr/bin/time -l pnpm lint` passes in + 22.61 seconds real with only the 26 pre-existing warnings, all 41 reliability gates, the + 355-entry max-lines ratchet, bundled guides, and localization checks; maximum RSS is + 2,036,318,208 bytes and peak footprint is 97,425,240 bytes. +- Isolation evidence: targeted `oxfmt --check` passes all ten changed code/workflow/plan files; + `git diff --check`, an independent no-index whitespace check for the new contract module, and + exact working-tree equality to `HEAD` for `ssh-remote-node-resolution.ts` and its test pass. Only + the artifact workflow, concrete workflow oracle split, no-input argv/probe implementation and + tests, and three planning artifacts are touched. +- Residual boundary: local macOS proof cannot execute native Windows `ssh.exe -n`, registry hive + unload, profile removal, service/auth, full-size transfer, cancellation, collision, or teardown. + Commit/push this isolated correction and require fresh exact-head x64/arm64 proof before closing + the live Windows gate. Legacy remains the sole production/default path; SignPath and product + caller/Beta/settings/fallback/tuple/publication work remain deferred. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit 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 39e6db19cf1..9140560c1eb 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 @@ -1642,7 +1642,10 @@ Full-size runtime over SFTP and POSIX/Windows system SSH; tar/no-tar; no remote hash tools; corrupted upload; bundled-Node corruption/failure; bounded memory - and concurrency; no-input system-SSH probe stdin/child settlement; + and concurrency; no-input system-SSH probe uses native OpenSSH -n + before the destination terminator and proves child settlement without disabling + connection reuse; fixture-owned Windows profile hives are boundedly unloaded + before native profile deletion; MaxSessions=1; interrupted upload; stale lock diff --git a/src/main/ssh/ssh-connection.test.ts b/src/main/ssh/ssh-connection.test.ts index b0c9194d992..035520fcc49 100644 --- a/src/main/ssh/ssh-connection.test.ts +++ b/src/main/ssh/ssh-connection.test.ts @@ -949,6 +949,7 @@ describe('SshConnection', () => { 'echo ORCA-SYSTEM-SSH-OK', { wrapCommand: false, + noInput: true, resolvedConfig: expect.objectContaining({ proxyUseFdpass: true }) } ) @@ -1002,6 +1003,7 @@ describe('SshConnection', () => { 'echo ORCA-SYSTEM-SSH-OK', expect.objectContaining({ wrapCommand: false, + noInput: true, resolvedConfig: expect.objectContaining({ proxyUseFdpass: true }) }) ) @@ -1012,6 +1014,7 @@ describe('SshConnection', () => { expect.objectContaining({ disableControlMaster: true, wrapCommand: false, + noInput: true, resolvedConfig: expect.objectContaining({ proxyUseFdpass: true }) }) ) @@ -1050,7 +1053,7 @@ describe('SshConnection', () => { expect(spawnSystemSshCommandMock).toHaveBeenCalledWith( expect.objectContaining({ proxyCommand: 'ssh -W %h:%p bastion.example.com' }), 'echo ORCA-SYSTEM-SSH-OK', - { wrapCommand: false } + { wrapCommand: false, noInput: true } ) }) @@ -1071,7 +1074,7 @@ describe('SshConnection', () => { expect(spawnSystemSshCommandMock).toHaveBeenCalledWith( expect.objectContaining({ host: '192.168.0.210' }), 'echo ORCA-SYSTEM-SSH-OK', - { wrapCommand: false } + { wrapCommand: false, noInput: true } ) }) diff --git a/src/main/ssh/ssh-connection.ts b/src/main/ssh/ssh-connection.ts index a8f11275c0b..dacc2da40c7 100644 --- a/src/main/ssh/ssh-connection.ts +++ b/src/main/ssh/ssh-connection.ts @@ -694,7 +694,8 @@ export class SshConnection { // Why: this probe runs before remote platform detection. A raw echo works // under POSIX shells, cmd.exe, and PowerShell; `/bin/sh` wrapping does not. const channel = this.spawnTrackedSystemSshCommand('echo ORCA-SYSTEM-SSH-OK', { - wrapCommand: false + wrapCommand: false, + noInput: true }) try { await new Promise((resolve, reject) => { @@ -908,7 +909,10 @@ export class SshConnection { return new Error('SSH connection attempt was cancelled') } - private spawnTrackedSystemSshCommand(command: string, options?: SshExecOptions): ClientChannel { + private spawnTrackedSystemSshCommand( + command: string, + options?: SshExecOptions & Pick + ): ClientChannel { if (options?.signal?.aborted) { throw createSshOperationAbortError() } diff --git a/src/main/ssh/ssh-system-fallback.test.ts b/src/main/ssh/ssh-system-fallback.test.ts index 324a0ed6e48..b9b6c0940d6 100644 --- a/src/main/ssh/ssh-system-fallback.test.ts +++ b/src/main/ssh/ssh-system-fallback.test.ts @@ -391,9 +391,26 @@ describe('spawnSystemSsh', () => { expect(args).toContain('ServerAliveCountMax=3') }) + it('places no-input mode before the destination without disabling connection reuse', () => { + const args = buildSshArgs(createTarget(), { + noInput: true, + resolvedConfig: createResolvedConfig() + }) + const noInputIdx = args.indexOf('-n') + const terminatorIdx = args.indexOf('--') + + expect(noInputIdx).toBeGreaterThan(-1) + expect(noInputIdx).toBeLessThan(terminatorIdx) + expect(args).toContain('ControlMaster=auto') + expect(args).toContain('ControlPersist=300') + expect(args).not.toContain('-S') + }) + it('spawns a remote command through the system ssh target', () => { spawnSystemSshCommand(createTarget({ configHost: 'fdpass-host' }), 'echo hello') + const args = spawnMock.mock.calls[0][1] as string[] + expect(args).not.toContain('-n') expect(spawnMock).toHaveBeenCalledWith( SYSTEM_SSH_PATH, expect.arrayContaining(['--', 'deploy@fdpass-host', "exec /bin/sh -c 'echo hello'"]), diff --git a/src/main/ssh/system-ssh-args.ts b/src/main/ssh/system-ssh-args.ts index 8e812a0b68b..ab9391e3629 100644 --- a/src/main/ssh/system-ssh-args.ts +++ b/src/main/ssh/system-ssh-args.ts @@ -5,6 +5,7 @@ export type SystemSshBuildArgsOptions = { resolvedConfig?: SystemSshResolvedConfig | null disableControlMaster?: boolean suppressOrcaControlMaster?: boolean + noInput?: boolean } export function buildSshArgs(target: SshTarget, options?: SystemSshBuildArgsOptions): string[] { @@ -13,6 +14,11 @@ export function buildSshArgs(target: SshTarget, options?: SystemSshBuildArgsOpti args.push('-o', 'BatchMode=no') // Forward stdin/stdout for relay communication args.push('-T') + if (options?.noInput === true) { + // Why: closing a parent pipe does not reliably detach native Windows + // OpenSSH's input worker for commands that can never consume stdin. + args.push('-n') + } // Why: ControlMaster multiplexes all SSH exec commands over a single connection, // eliminating the ~9s handshake overhead per command. Without this, each @@ -91,6 +97,9 @@ export function getSystemSshBuildArgsFromOperationOptions( if (options?.suppressOrcaControlMaster === true) { buildArgsOptions.suppressOrcaControlMaster = true } + if (options?.noInput === true) { + buildArgsOptions.noInput = true + } return Object.keys(buildArgsOptions).length === 0 ? undefined : buildArgsOptions }