From fac052b7714e2c93c5c84b41b784869d4b3fe125 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 12:42:49 -0400 Subject: [PATCH] feat(ssh): add POSIX relay file destination Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 24 +- ...github-release-implementation-checklist.md | 188 +++++++++- ...lay-runtime-posix-file-destination.test.ts | 325 ++++++++++++++++++ ...sh-relay-runtime-posix-file-destination.ts | 318 +++++++++++++++++ 6 files changed, 832 insertions(+), 26 deletions(-) create mode 100644 src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts create mode 100644 src/main/ssh/ssh-relay-runtime-posix-file-destination.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 4e4aecb241a..83d675d4ac0 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -286,6 +286,7 @@ jobs: src/main/ssh/ssh-relay-runtime-source-tree.test.ts \ src/main/ssh/ssh-relay-runtime-source-scan.test.ts \ src/main/ssh/ssh-relay-runtime-source-stream.test.ts \ + src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-session.test.ts \ @@ -931,6 +932,7 @@ jobs: src/main/ssh/ssh-relay-runtime-source-tree.test.ts ` src/main/ssh/ssh-relay-runtime-source-scan.test.ts ` src/main/ssh/ssh-relay-runtime-source-stream.test.ts ` + src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-session.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 427b5e93b42..673b8990b4d 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -164,6 +164,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-runtime-source-tree.test.ts', 'ssh-relay-runtime-source-scan.test.ts', 'ssh-relay-runtime-source-stream.test.ts', + 'ssh-relay-runtime-posix-file-destination.test.ts', 'ssh-relay-runtime-sftp-file-destination.test.ts', 'ssh-relay-runtime-sftp-connection-transfer.test.ts', 'ssh-relay-runtime-sftp-session.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 60ea300fde8..a5c86899c41 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,15 +9,21 @@ 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 — exact full-size Linux -x64/arm64 OpenSSH SFTP is green; repeated adjacent Windows compiler-test timeout correction is in -progress, 2026-07-15, Codex implementation owner.** `E-M6-SFTP-LIVE-COMPOSITION-CI-001` records -complete exact-tree/mode/exclusive-root/cancellation/RSS proof against stock OpenSSH on native Linux -x64 and arm64. `E-M6-SFTP-LIVE-COMPOSITION-ADJACENT-CI-RED-001` records the only adjacent failure: -the unchanged real Windows C# compiler integration case exceeded Vitest's five-second default twice. -Current main already fixes that pre-existing test under reviewed commit `45a772cb4` / PR #8897 by -scoping 15 seconds only to the real compiler cases. Apply that exact test-only patch and require a -fresh exact-head computer-use pass before starting disconnected POSIX system-SSH transfer. +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected POSIX +system-SSH/no-tar per-file boundary is locally green and exact-head CI is next, 2026-07-15, Codex +implementation owner.** `E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001` records 25/25 purpose cases, 87 focused +cases with one declared skip, 628 broad relay cases with six declared skips, 281/281 release +contracts, typecheck, full lint, formatting, max-lines, diff, protected-resolver, and no-product- +consumer gates. The command uses only the four declared POSIX primitives and cancellation is bounded +to a 250 ms graceful / 2,000 ms total ceiling. Exact-head all-six native and adjacent Actions proof +is required before the next package. +`E-M6-SFTP-LIVE-COMPOSITION-CI-001` records complete exact-tree/mode/exclusive-root/cancellation/RSS +proof against stock OpenSSH on native Linux x64 and arm64. The repeated unrelated Windows compiler +timeout is closed at exact head `4945645e8` by computer-use run `29515352845` under +`E-M6-SFTP-LIVE-COMPOSITION-ADJACENT-CI-001`. `E-M6-POSIX-SYSTEM-SSH-FILE-AUDIT-001` limits the next +slice to one injected command channel and one exclusively created manifest file with bounded chunk, +EOF, mode, cancellation, and forced-settlement contracts. No real SSH connection, remote tree, +semantic probe, live/full-size claim, product path, mode/fallback, or default behavior is included. `E-M6-SFTP-LIVE-COMPOSITION-AUDIT-001` limits this package to one authenticated built-in `SshConnection`, one raw SFTP channel, the existing one-to-four file transfer, captured-transport- only force close with awaited teardown, and exact full-size Linux x64/arm64 runtime transfer against 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 b438fffc14b..72d7b245f9d 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: close exact-head live-SFTP adjacent regression evidence before starting disconnected POSIX system-SSH transfer**. 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: **LIVE SFTP GREEN / REPEATED ADJACENT WINDOWS TIMEOUT DIAGNOSED — 2026-07-15, Codex implementation owner** — `E-M6-SFTP-LIVE-COMPOSITION-CI-001` proves exact full-size Linux x64/arm64 transfer through stock OpenSSH with complete tree/mode/exclusive-root/cancellation/RSS evidence. `E-M6-SFTP-LIVE-COMPOSITION-ADJACENT-CI-RED-001` records the only adjacent failure: the unchanged real Windows launcher compilation case exceeded Vitest's five-second default on both attempts. Current `main` independently fixed that exact pre-existing test in reviewed commit `45a772cb4` / PR #8897 by scoping 15 seconds only to the real compiler integration cases. Apply that exact test-only patch, rerun exact head, and do not credit Windows/POSIX system SSH, high-RTT, `MaxSessions=1`, product/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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected POSIX system-SSH/no-tar per-file command and destination contract**. 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 NO-TAR FILE BOUNDARY LOCALLY GREEN / EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001` proves the isolated destination across 25 purpose cases, 628 broad relay cases, 281 release contracts, typecheck, lint, formatting, max-lines, diff, protected-resolver, and no-product-consumer gates. The module still accepts only an injected command channel and has no real SSH/tree/product caller, so exact-head all-six native and adjacent Actions evidence is required before the next package. 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) @@ -150,14 +150,12 @@ same change as the work it records. - Completed package: disconnected full host-evidence composition is green locally and on every primary native client under E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001 and E-M5-HOST-EVIDENCE-COMPOSITION-CI-001. It has no product caller. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit, RED, and local - GREEN complete under E-M6-SOURCE-STREAM-AUDIT-001, E-M6-SOURCE-STREAM-LOCAL-RED-001, and - E-M6-SOURCE-STREAM-LOCAL-001. E-M6-SOURCE-STREAM-CI-RED-001 exposes a Windows-only test-oracle - mismatch; its POSIX-only correction is locally green under - E-M6-SOURCE-STREAM-WINDOWS-ORACLE-CORRECTION-LOCAL-001. Commit/push it, then require fresh - exact-head all-six native/full-size and adjacent CI. Do not import SSH, open channels, construct - remote paths, create remote staging, or add product/settings/fallback/tuple/publication/default - behavior. +- Active package: **Local implementation complete / exact-head CI pending — 2026-07-15, Codex + implementation owner** — the disconnected POSIX no-tar per-file command/destination boundary is + green under E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001. It consumes the existing source-stream + destination interface and an injected command-channel factory only. Do not import or modify the + legacy tar/base64 directory uploader, wire `SshConnection`, create a remote tree, add a semantic + probe, or add product/settings/fallback/tuple/publication/default behavior. - 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 @@ -17410,14 +17408,171 @@ src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts` — PASS, 15/15 in 1. overlap this package. The same workflow's Ubuntu job passes, and the independent all-six native artifact jobs plus PR Checks and Golden E2E pass. - Independent diagnosis: current `origin/main` commit - `45a772cb42f8525ce0189fe63a509e562c5b6928` (`test(windows): allow native launcher compilation -time`, reviewed PR #8897) changes only the two real Windows compiler integration tests and scopes a + `45a772cb42f8525ce0189fe63a509e562c5b6928` + (`test(windows): allow native launcher compilation time`, reviewed PR #8897) changes only the two + real Windows compiler integration tests and scopes a 15-second timeout to them because cold hosted `csc.exe` startup exceeds the unit default. It is not an SFTP change and changes no production behavior. This feature branch predates that commit. - Disposition: apply the exact reviewed test-only patch without broad timeout relaxation, then require a fresh exact-head computer-use pass before advancing. No adjacent green evidence is credited from either failed attempt. +### E-M6-SFTP-LIVE-COMPOSITION-ADJACENT-CI-001 — scoped upstream test budget closes exact-head adjacent gate + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact evidence/test-only commit + `4945645e81af971d8ba874d83ee5975abfd6e79d`, computer-use run + [29515352845](https://github.com/stablyai/orca/actions/runs/29515352845). +- Windows result: job + [87678882484](https://github.com/stablyai/orca/actions/runs/29515352845/job/87678882484) passes the + 401 applicable tests with four declared skips, including both real Windows C# compiler integration + cases under their exact reviewed 15-second budgets. It then passes native verification, CLI and + packaged Electron builds, daemon boot, Windows daemon workspace-close repro, and Windows + computer-use E2E in 4 minutes 38 seconds. No product source or timeout outside the two compiler + cases changed. +- Control result: Ubuntu job + [87678882558](https://github.com/stablyai/orca/actions/runs/29515352845/job/87678882558) passes its + unit/native/build/daemon/Linux E2E path in 2 minutes 49 seconds. Local macOS runs passed three + applicable launcher cases with the two Windows cases declared skipped plus the eight-case relay + workflow oracle; formatting and diff gates passed before commit. +- Disposition: the repeated adjacent RED is resolved by the exact independently reviewed upstream + test-only correction. Prior live SFTP evidence remains anchored to `a72c32f63`; this commit changes + no SFTP/runtime/product code. The separately triggered all-six artifact rerun is redundant control + evidence and may continue while the next disconnected package begins. + +### E-M6-SFTP-LIVE-COMPOSITION-CONTROL-CI-001 — redundant exact-head control classified + +- Date/owner/head: 2026-07-15, Codex implementation owner; exact test/docs-only head + `4945645e81af971d8ba874d83ee5975abfd6e79d` after the adjacent correction above. +- Runtime artifact run: [29515352928](https://github.com/stablyai/orca/actions/runs/29515352928). + All six primary native jobs pass: Windows arm64 [87678883773](https://github.com/stablyai/orca/actions/runs/29515352928/job/87678883773), + Windows x64 [87678883819](https://github.com/stablyai/orca/actions/runs/29515352928/job/87678883819), + macOS arm64 [87678883853](https://github.com/stablyai/orca/actions/runs/29515352928/job/87678883853), + Linux arm64 [87678883887](https://github.com/stablyai/orca/actions/runs/29515352928/job/87678883887), + Linux x64 [87678883950](https://github.com/stablyai/orca/actions/runs/29515352928/job/87678883950), + and macOS x64 [87678883952](https://github.com/stablyai/orca/actions/runs/29515352928/job/87678883952). + Both Linux oldest-userland supplements pass in jobs 87680141129 and 87680141145, as does the + Windows x64 floor in job 87681071541. +- Declared residual: only Windows arm64 floor job + [87681071534](https://github.com/stablyai/orca/actions/runs/29515352928/job/87681071534) fails after + complete runtime smoke because the hosted runner reports build 26200 while the immutable contract + requires build 26100; platform and architecture pass, `osBuild` fails. This is the already recorded + runner gap, not a test-only correction or SFTP regression. +- Adjacent controls: PR Checks run + [29515352911](https://github.com/stablyai/orca/actions/runs/29515352911) passes lint, typecheck, Git + compatibility, tests, unpacked app build, and packaged CLI smoke. Golden E2E run + [29515352860](https://github.com/stablyai/orca/actions/runs/29515352860) passes both macOS job + 87678882828 and Linux job 87678882845. +- Disposition: this closes classification of the redundant control run only. Live SFTP evidence + remains anchored to `a72c32f63`; no POSIX system-SSH claim or later-package evidence is inferred. + +### E-M6-POSIX-SYSTEM-SSH-FILE-AUDIT-001 — isolated no-tar file channel boundary + +- Date/owner/base: 2026-07-15, Codex implementation owner; clean exact base + `4945645e81af971d8ba874d83ee5975abfd6e79d` after the adjacent gate above. +- Existing-code audit: `system-ssh-file-transfer.ts` is the proven legacy/default uploader and uses + local plus remote tar on POSIX and a whole-tree JSON/base64 buffer on Windows. Those paths remain + available but violate the new runtime bootstrap contract and must not be reused, narrowed, or + modified in this slice. `ssh-relay-runtime-source-stream.ts` already owns the exact scanned source, + one 64 KiB buffer per worker, hash/size/snapshot checks, and joined destination cleanup. +- New boundary: add one purpose-named POSIX file-destination module that implements only + `SshRelayRuntimeSourceDestination`. It receives an absolute POSIX manifest path, authenticated + mode (`0644` or `0755`), the exact caller signal, and an injected command-channel factory. The + generated POSIX `sh` command uses only `umask`, `set -C`, byte-preserving `cat`, and `chmod`; it + shell-quotes the path, exclusively creates the file with mode `0600` during transfer, and applies + the authenticated final mode only after stdin EOF and successful `cat` settlement. It never uses + Node, Python, Perl, tar, base64, a checksum tool, a whole-file buffer, or a remote download. +- Lifecycle contract: one write may be active; each write resolves only after the channel accepts + the exact non-empty chunk so the source worker may safely reuse its buffer. Close sends EOF once + and awaits a successful remote exit before declaring completion. Abort is idempotent, settles any + active write, requests graceful channel termination, escalates after a short fixed grace to forced + termination, and fails closed if the bounded total settlement window expires. A later tree owner, + not this file boundary, owns removal of an incomplete proved-owned path after the channel settles. +- Layer-A proof: purpose tests must cover hostile/relative/newline/NUL paths, invalid modes, exact + shell quoting and forbidden-command absence, exclusive/restrictive/final-mode command order, + zero-byte EOF, multi-chunk buffer callback order, concurrent-operation rejection, remote nonzero + exit, pre/mid-write/mid-close cancellation, graceful and forced settlement, final timeout, cleanup + failure propagation, idempotent abort, and no write after cancellation. Pin the purpose suite once + in both native workflow command families before implementation. +- Explicit residual boundary: no real SSH process or connection, directory/tree staging, primitive + semantic probe, `MaxSessions=1`, one-versus-four policy, tar optimization, full-size/live transfer, + bundled-Node remote verification/install/publish/launch, product/Beta/settings/fallback/tuple/ + publication/default behavior, Windows PowerShell path, or SignPath is included. Legacy remains the + sole production/default path. + +### E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-RED-001 — missing production boundary fails as designed + +- Date/owner/base: 2026-07-15, Codex implementation owner; exact uncommitted test/workflow state on + base `4945645e81af971d8ba874d83ee5975abfd6e79d` after + `E-M6-POSIX-SYSTEM-SSH-FILE-AUDIT-001`. +- Command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs`. +- Result: expected RED, exit 1. The new purpose suite collects zero cases and fails solely with + `Cannot find module './ssh-relay-runtime-posix-file-destination'`; that production module does not + exist yet. The independent release-workflow oracle passes all 8/8 cases, proving the purpose suite + appears exactly once in each native command family before implementation. +- Resource evidence: Vitest duration 671 ms; `/usr/bin/time -l` reports 1.86 seconds real, + 132,153,344 bytes maximum resident set size, zero swaps, and zero block input/output operations. +- Disposition: RED is causally narrow and accepted. Implement only the audited injected-channel + destination, then require purpose GREEN plus focused, broad, static, isolation, and exact-head + native workflow evidence. No live SSH, tree transfer, product path, or default-path claim follows + from this evidence. + +### E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-TYPE-RED-001 — test-only type boundary corrected + +- Date/owner/state: 2026-07-15, Codex implementation owner; first static pass on the uncommitted + implementation atop `4945645e81af971d8ba874d83ee5975abfd6e79d`. +- Command/result: `/usr/bin/time -l pnpm typecheck` — expected gate RED after runtime tests were + already green, exit 1 in 3.23 seconds real with 1,032,896,512-byte maximum RSS. TypeScript reports + only three purpose-test errors: the intentionally invalid numeric mode crosses the production + literal union, and two zero-argument `vi.fn` channel factories expose no command tuple to the + assertions. No production line fails typechecking. +- Correction: constrain the deliberate runtime-invalid mode at the test call boundary and declare + the two mock command/signal parameters. The 25-case purpose suite and repeated typecheck below pass + after that test-only correction. No product, transport, or runtime behavior changed. + +### E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001 — isolated file channel boundary is locally green + +- Date/owner/environment/base: 2026-07-15, Codex implementation owner; local macOS arm64, Node + 26.0.0, pnpm 10.24.0, uncommitted package atop exact base + `4945645e81af971d8ba874d83ee5975abfd6e79d`. +- Implementation: the 318-line purpose-named module accepts one absolute normalized POSIX manifest + path, authenticated `0644`/`0755`, exact signal, and injected channel factory. Its exact command + uses only `umask 077`, `set -C`, byte-preserving `cat`, and `chmod`; single-quote escaping is + covered. It awaits every non-empty write callback, sends EOF once, requires successful remote + settlement, and bounds cancellation to a 250 ms graceful window plus a 2,000 ms total ceiling with + force-close escalation. Cancellation never sends EOF and does not release an active retained + chunk until channel settlement or the fail-closed ceiling. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts` — PASS, 25/25 in + 275 ms Vitest / 1.31 seconds real, 132,743,168-byte maximum RSS, zero swaps, and zero block I/O. + Cases cover both final modes; exact/forbidden command content; hostile paths; empty/write-error/ + retained chunks; concurrent operations; zero-byte EOF; nonzero remote exit; pre-open, mid-open, + mid-write, and mid-close cancellation; graceful/forced/timeout settlement; cleanup failure; and + idempotent abort/no later EOF or write. +- Focused command: purpose, source stream/scan/tree, and native-workflow oracle — PASS, five files, + 87 cases plus one declared platform skip in 7.49 seconds Vitest / 8.98 seconds real with + 144,556,032-byte maximum RSS. The workflow oracle passes 8/8 and pins the suite exactly once in + both POSIX and PowerShell native command families. +- Broad commands: `src/main/ssh/ssh-relay-*.test.ts` — PASS, 47 files plus four declared skipped, + 628 cases plus six declared skips in 25.04 seconds Vitest / 26.50 seconds real with + 244,596,736-byte maximum RSS; `config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files and + 281/281 cases in 22.34 seconds Vitest / 24.48 seconds real with 190,545,920-byte maximum RSS. Only + the established stale-lock diagnostic and local Node 26 module deprecation appear. +- Static/isolation commands: repeated `/usr/bin/time -l pnpm typecheck` passes in 3.32 seconds real + with 1,241,726,976-byte maximum RSS. Targeted `oxlint`, targeted `oxfmt --check`, standalone + max-lines ratchet, `git diff --check`, protected-resolver empty diff, and no non-test consumer pass. + Full `pnpm lint` passes in 18.10 seconds real with 2,052,751,360-byte maximum RSS: switch + exhaustiveness, 41 reliability gates, the 355-entry max-lines ratchet, bundled-skill verification, + 9,837 localization references, locale parity, and zero-candidate localization coverage pass; only + pre-existing unrelated warnings remain. Module/test sizes are 318/325 lines with no bypass. +- Residual: this is an injected callback-channel contract, not a real SSH process, `SshConnection` + adapter, tree/root staging owner, semantic primitive probe, tar optimization, `MaxSessions=1`, + full-size/live transfer, or Windows PowerShell path. It has no non-test consumer. Remote bundled- + Node verification/install/publish/launch, product/Beta/fallback/tuple/publication/default behavior, + and SignPath remain absent; legacy remains the sole production/default path. Exact-head all-six + native and adjacent Actions evidence is required before the next bounded package. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -17476,11 +17631,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Apply current-main commit `45a772cb4`'s exact scoped Windows compiler-test timeout patch, commit and -push this evidence checkpoint, and require a fresh exact-head computer-use pass. Then audit the next -smallest disconnected POSIX system-SSH/no-tar transfer package before changing code. Do not check -the broad Milestone 6 SFTP item yet: Windows remote, cross-client, high-RTT, `MaxSessions=1`, live -channel/handle, and fault-injection cells remain open. Do not add an Electron/startup/product +Commit and push the locally green isolated POSIX no-tar per-file boundary, then require exact-head +all-six native artifact jobs, both Linux supplements, the Windows x64 floor, PR Checks, Golden E2E, +and computer-use evidence before auditing the next system-SSH adapter/tree slice. Do not +check the broad Milestone 6 SFTP or POSIX system-SSH items yet. 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/src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts b/src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts new file mode 100644 index 00000000000..5613151840c --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts @@ -0,0 +1,325 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { + openSshRelayRuntimePosixFileDestination, + SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS, + type SshRelayRuntimePosixFileChannel +} from './ssh-relay-runtime-posix-file-destination' + +type Callback = (error?: Error) => void + +function createChannel(): { + channel: SshRelayRuntimePosixFileChannel + resolve: () => void + reject: (error: Error) => void +} { + let resolveSettled: () => void = () => {} + let rejectSettled: (error: Error) => void = () => {} + const settled = new Promise((resolve, reject) => { + resolveSettled = resolve + rejectSettled = reject + }) + const channel: SshRelayRuntimePosixFileChannel = { + write: vi.fn((_chunk: Buffer, callback: Callback) => callback()), + end: vi.fn(), + settled, + requestClose: vi.fn(), + forceClose: vi.fn() + } + return { channel, resolve: resolveSettled, reject: rejectSettled } +} + +function openDestination( + channel: SshRelayRuntimePosixFileChannel, + options: { + remotePath?: string + mode?: number + signal?: AbortSignal + openChannel?: (command: string, signal: AbortSignal) => Promise + } = {} +) { + return openSshRelayRuntimePosixFileDestination({ + remotePath: options.remotePath ?? "/owned stage/bin/no'de", + mode: (options.mode ?? 0o755) as 0o644 | 0o755, + signal: options.signal ?? new AbortController().signal, + openChannel: options.openChannel ?? vi.fn(async () => channel) + }) +} + +afterEach(() => { + vi.useRealTimers() +}) + +describe('SSH relay runtime POSIX no-tar file destination', () => { + it('builds one exclusive restrictive command with exact quoting and final mode order', async () => { + const { channel, resolve } = createChannel() + const controller = new AbortController() + const openChannel = vi.fn(async (_command: string, _signal: AbortSignal) => channel) + const destination = await openDestination(channel, { + signal: controller.signal, + openChannel + }) + + expect(openChannel).toHaveBeenCalledWith( + "umask 077; set -C; cat > '/owned stage/bin/no'\\''de' && chmod 0755 '/owned stage/bin/no'\\''de'", + controller.signal + ) + const command = openChannel.mock.calls[0]?.[0] ?? '' + for (const forbidden of ['node ', 'python', 'perl', 'tar ', 'base64', 'sha256sum', 'shasum']) { + expect(command.toLowerCase()).not.toContain(forbidden) + } + + const closing = destination.close() + expect(channel.end).toHaveBeenCalledOnce() + resolve() + await closing + }) + + it('authenticates a non-executable final mode in the same command', async () => { + const { channel, resolve } = createChannel() + const openChannel = vi.fn(async (_command: string, _signal: AbortSignal) => channel) + const destination = await openDestination(channel, { + remotePath: '/owned-stage/目录/manifest.json', + mode: 0o644, + openChannel + }) + + expect(openChannel.mock.calls[0]?.[0]).toBe( + "umask 077; set -C; cat > '/owned-stage/目录/manifest.json' && chmod 0644 '/owned-stage/目录/manifest.json'" + ) + const closing = destination.close() + resolve() + await closing + }) + + it.each([ + ['', 0o755], + ['relative/file', 0o755], + ['/', 0o755], + ['/owned//file', 0o755], + ['/owned/../file', 0o755], + ['/owned/./file', 0o755], + ['/owned/file/', 0o755], + ['/owned/file\nnext', 0o755], + ['/owned/file\0next', 0o755], + ['/owned/file', 0o600] + ])('rejects hostile path %j or mode %d before opening a channel', async (remotePath, mode) => { + const { channel } = createChannel() + const openChannel = vi.fn(async () => channel) + + await expect(openDestination(channel, { remotePath, mode, openChannel })).rejects.toThrow( + /invalid/i + ) + expect(openChannel).not.toHaveBeenCalled() + }) + + it('sends EOF once for a zero-byte file and awaits remote settlement', async () => { + const { channel, resolve } = createChannel() + const destination = await openDestination(channel) + let settled = false + const closing = destination.close().then(() => { + settled = true + }) + + await Promise.resolve() + expect(channel.end).toHaveBeenCalledOnce() + expect(settled).toBe(false) + resolve() + await closing + await expect(destination.close()).rejects.toThrow(/closed/i) + expect(channel.end).toHaveBeenCalledOnce() + }) + + it('awaits exact chunk callbacks and rejects concurrent operations', async () => { + const { channel, resolve } = createChannel() + let firstCallback: Callback | undefined + vi.mocked(channel.write).mockImplementationOnce((_chunk, callback) => { + firstCallback = callback + }) + const destination = await openDestination(channel) + const first = Buffer.from('first') + let settled = false + const writing = destination.write(first).then(() => { + settled = true + }) + + await Promise.resolve() + expect(channel.write).toHaveBeenCalledWith(first, expect.any(Function)) + expect(settled).toBe(false) + await expect(destination.write(Buffer.from('second'))).rejects.toThrow(/concurrent/i) + await expect(destination.close()).rejects.toThrow(/concurrent/i) + firstCallback?.() + await writing + await destination.write(Buffer.from('second')) + const closing = destination.close() + resolve() + await closing + }) + + it('rejects empty chunks and propagates channel write failures', async () => { + const { channel, resolve } = createChannel() + vi.mocked(channel.write).mockImplementationOnce((_chunk, callback) => { + callback(new Error('channel write failed')) + }) + const destination = await openDestination(channel) + + await expect(destination.write(Buffer.alloc(0))).rejects.toThrow(/empty/i) + await expect(destination.write(Buffer.from('payload'))).rejects.toThrow('channel write failed') + const closing = destination.close() + resolve() + await closing + }) + + it('propagates remote nonzero settlement on close', async () => { + const { channel, reject } = createChannel() + const destination = await openDestination(channel) + const closing = destination.close() + reject(new Error('remote cat failed (exit 1)')) + + await expect(closing).rejects.toThrow('remote cat failed') + }) + + it('rejects pre-open cancellation without opening a channel', async () => { + const { channel } = createChannel() + const controller = new AbortController() + const reason = new Error('cancelled before open') + controller.abort(reason) + const openChannel = vi.fn(async () => channel) + + await expect(openDestination(channel, { signal: controller.signal, openChannel })).rejects.toBe( + reason + ) + expect(openChannel).not.toHaveBeenCalled() + }) + + it('settles a channel returned after mid-open cancellation', async () => { + const { channel, resolve } = createChannel() + const controller = new AbortController() + const reason = new Error('cancelled during open') + vi.mocked(channel.requestClose).mockImplementation(() => resolve()) + const openChannel = vi.fn(async () => { + controller.abort(reason) + return channel + }) + + await expect(openDestination(channel, { signal: controller.signal, openChannel })).rejects.toBe( + reason + ) + expect(channel.requestClose).toHaveBeenCalledOnce() + expect(channel.forceClose).not.toHaveBeenCalled() + }) + + it('holds a retained write buffer until cancellation settles the channel', async () => { + const { channel, resolve } = createChannel() + const controller = new AbortController() + const reason = new Error('cancelled during write') + vi.mocked(channel.write).mockImplementation(() => {}) + vi.mocked(channel.requestClose).mockImplementation(() => resolve()) + const destination = await openDestination(channel, { signal: controller.signal }) + const chunk = Buffer.alloc(64 * 1024, 7) + let settled = false + const writing = destination.write(chunk).finally(() => { + settled = true + }) + + controller.abort(reason) + await Promise.resolve() + expect(channel.requestClose).toHaveBeenCalledOnce() + await expect(writing).rejects.toBe(reason) + expect(settled).toBe(true) + await expect(destination.abort(reason)).resolves.toBeUndefined() + await expect(destination.write(Buffer.from('later'))).rejects.toThrow(/closed/i) + expect(channel.write).toHaveBeenCalledOnce() + }) + + it('cancels a close only after the command channel settles', async () => { + const { channel, resolve } = createChannel() + const controller = new AbortController() + const reason = new Error('cancelled during close') + vi.mocked(channel.requestClose).mockImplementation(() => resolve()) + const destination = await openDestination(channel, { signal: controller.signal }) + const closing = destination.close() + + controller.abort(reason) + await expect(destination.abort(reason)).resolves.toBeUndefined() + await expect(closing).rejects.toBe(reason) + expect(channel.end).toHaveBeenCalledOnce() + expect(channel.requestClose).toHaveBeenCalledOnce() + }) + + it('settles ordinary cancellation during the graceful window', async () => { + const { channel, resolve } = createChannel() + const controller = new AbortController() + vi.mocked(channel.requestClose).mockImplementation(() => resolve()) + const destination = await openDestination(channel, { signal: controller.signal }) + + controller.abort(new Error('cancelled')) + await expect(destination.abort(controller.signal.reason)).resolves.toBeUndefined() + expect(channel.requestClose).toHaveBeenCalledOnce() + expect(channel.forceClose).not.toHaveBeenCalled() + }) + + it('forces a channel that does not settle during the graceful window', async () => { + vi.useFakeTimers() + const { channel, resolve } = createChannel() + const controller = new AbortController() + vi.mocked(channel.forceClose).mockImplementation(() => resolve()) + const destination = await openDestination(channel, { signal: controller.signal }) + + controller.abort(new Error('cancelled')) + const aborting = destination.abort(controller.signal.reason) + await vi.advanceTimersByTimeAsync( + SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS.gracefulCloseMs + ) + await expect(aborting).resolves.toBeUndefined() + expect(channel.requestClose).toHaveBeenCalledOnce() + expect(channel.forceClose).toHaveBeenCalledOnce() + }) + + it('fails closed when forced channel settlement reaches the total ceiling', async () => { + vi.useFakeTimers() + const { channel } = createChannel() + const controller = new AbortController() + const destination = await openDestination(channel, { signal: controller.signal }) + + controller.abort(new Error('cancelled')) + const aborting = destination.abort(controller.signal.reason) + await vi.advanceTimersByTimeAsync(SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS.totalCloseMs) + + await expect(aborting).rejects.toThrow(/settlement timed out/i) + expect(channel.requestClose).toHaveBeenCalledOnce() + expect(channel.forceClose).toHaveBeenCalledOnce() + }) + + it('propagates cleanup request failures after the channel settles', async () => { + const { channel, resolve } = createChannel() + const controller = new AbortController() + vi.mocked(channel.requestClose).mockImplementation(() => { + resolve() + throw new Error('graceful close failed') + }) + const destination = await openDestination(channel, { signal: controller.signal }) + + controller.abort(new Error('cancelled')) + await expect(destination.abort(controller.signal.reason)).rejects.toThrow( + 'graceful close failed' + ) + }) + + it('makes abort idempotent and never sends EOF after cancellation', async () => { + const { channel, resolve } = createChannel() + const controller = new AbortController() + vi.mocked(channel.requestClose).mockImplementation(() => resolve()) + const destination = await openDestination(channel, { signal: controller.signal }) + const reason = new Error('cancelled') + + controller.abort(reason) + const first = destination.abort(reason) + const second = destination.abort(reason) + expect(first).toBe(second) + await first + expect(channel.requestClose).toHaveBeenCalledOnce() + expect(channel.end).not.toHaveBeenCalled() + await expect(destination.close()).rejects.toThrow(/closed/i) + }) +}) diff --git a/src/main/ssh/ssh-relay-runtime-posix-file-destination.ts b/src/main/ssh/ssh-relay-runtime-posix-file-destination.ts new file mode 100644 index 00000000000..1bf7858351b --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-posix-file-destination.ts @@ -0,0 +1,318 @@ +import type { SshRelayRuntimeSourceDestination } from './ssh-relay-runtime-source-stream' + +type ChannelWriteCallback = (error?: Error) => void + +export type SshRelayRuntimePosixFileChannel = Readonly<{ + write: (chunk: Buffer, callback: ChannelWriteCallback) => void + end: () => void + settled: Promise + requestClose: () => void + forceClose: () => void +}> + +export type OpenSshRelayRuntimePosixFileDestinationOptions = Readonly<{ + remotePath: string + mode: 0o644 | 0o755 + signal: AbortSignal + openChannel: (command: string, signal: AbortSignal) => Promise +}> + +export const SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS = Object.freeze({ + gracefulCloseMs: 250, + totalCloseMs: 2_000 +}) + +type DestinationState = 'open' | 'closing' | 'complete' | 'aborting' | 'aborted' + +type ChannelSettlement = Readonly<{ ok: true } | { ok: false; error: unknown }> + +type AbortOutcome = Readonly<{ reason: unknown }> + +function validateRemotePath(remotePath: string): void { + const segments = remotePath.split('/') + if ( + remotePath === '/' || + !remotePath.startsWith('/') || + remotePath.includes('\0') || + remotePath.includes('\n') || + remotePath.includes('\r') || + segments.slice(1).some((segment) => segment === '' || segment === '.' || segment === '..') + ) { + throw new Error('SSH relay runtime POSIX file destination path is invalid') + } +} + +function validateOptions(options: OpenSshRelayRuntimePosixFileDestinationOptions): void { + if ( + !options || + typeof options.remotePath !== 'string' || + typeof options.openChannel !== 'function' || + !options.signal + ) { + throw new Error('SSH relay runtime POSIX file destination input is invalid') + } + validateRemotePath(options.remotePath) + if (options.mode !== 0o644 && options.mode !== 0o755) { + throw new Error('SSH relay runtime POSIX file destination mode is invalid') + } +} + +function validateChannel(channel: SshRelayRuntimePosixFileChannel): void { + if ( + !channel || + typeof channel.write !== 'function' || + typeof channel.end !== 'function' || + typeof channel.settled?.then !== 'function' || + typeof channel.requestClose !== 'function' || + typeof channel.forceClose !== 'function' + ) { + throw new Error('SSH relay runtime POSIX file channel is invalid') + } +} + +function quotePosixShellArgument(value: string): string { + return `'${value.replaceAll("'", "'\\''")}'` +} + +function buildCommand(remotePath: string, mode: 0o644 | 0o755): string { + const quotedPath = quotePosixShellArgument(remotePath) + const finalMode = mode.toString(8).padStart(4, '0') + // Why: noclobber plus umask makes authenticated staging exclusive and non-readable until EOF. + return `umask 077; set -C; cat > ${quotedPath} && chmod ${finalMode} ${quotedPath}` +} + +function cleanupFailure(failures: readonly unknown[]): unknown | undefined { + if (failures.length === 0) { + return undefined + } + return failures.length === 1 + ? failures[0] + : new AggregateError(failures, 'SSH relay runtime POSIX file channel cleanup failed') +} + +function waitForSettlement( + settlement: Promise, + timeoutMs: number +): Promise { + return new Promise((resolve) => { + const timeout = setTimeout(() => resolve(false), timeoutMs) + void settlement.then(() => { + clearTimeout(timeout) + resolve(true) + }) + }) +} + +async function settleCancelledChannel( + channel: SshRelayRuntimePosixFileChannel, + settlement: Promise +): Promise { + const failures: unknown[] = [] + try { + channel.requestClose() + } catch (error) { + failures.push(error) + } + + const graceful = await waitForSettlement( + settlement, + SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS.gracefulCloseMs + ) + if (!graceful) { + try { + channel.forceClose() + } catch (error) { + failures.push(error) + } + const forced = await waitForSettlement( + settlement, + SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS.totalCloseMs - + SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS.gracefulCloseMs + ) + if (!forced) { + failures.unshift(new Error('SSH relay runtime POSIX file channel settlement timed out')) + } + } + + const failure = cleanupFailure(failures) + if (failure !== undefined) { + throw failure + } +} + +function createDestination( + options: OpenSshRelayRuntimePosixFileDestinationOptions, + channel: SshRelayRuntimePosixFileChannel +): SshRelayRuntimeSourceDestination { + const { signal } = options + const channelSettlement: Promise = channel.settled.then( + () => ({ ok: true }), + (error: unknown) => ({ ok: false, error }) + ) + let state: DestinationState = 'open' + let activeOperation: Promise | undefined + let abortPromise: Promise | undefined + let abortReason: unknown + let resolveAbortOutcome: (outcome: AbortOutcome) => void = () => {} + const abortOutcome = new Promise((resolve) => { + resolveAbortOutcome = resolve + }) + + const assertWritable = (): void => { + if (state !== 'open') { + throw new Error('SSH relay runtime POSIX file destination is closed') + } + if (activeOperation) { + throw new Error('SSH relay runtime POSIX file destination has a concurrent operation') + } + } + + const runExclusive = (operation: () => Promise): Promise => { + const pending = operation() + activeOperation = pending + void pending + .finally(() => { + if (activeOperation === pending) { + activeOperation = undefined + } + }) + .catch(() => {}) + return pending + } + + const waitForOperation = async (operation: Promise): Promise => { + const result = await Promise.race([ + operation.then( + () => ({ kind: 'complete' as const }), + (error: unknown) => ({ kind: 'failed' as const, error }) + ), + abortOutcome.then((outcome) => ({ kind: 'aborted' as const, outcome })) + ]) + if (result.kind === 'aborted') { + throw result.outcome.reason + } + if (state === 'aborting' || state === 'aborted') { + const outcome = await abortOutcome + throw outcome.reason + } + if (result.kind === 'failed') { + throw result.error + } + } + + const performAbort = async (): Promise => { + const reason = abortReason + let failure: unknown + try { + await settleCancelledChannel(channel, channelSettlement) + } catch (error) { + failure = error + } finally { + state = 'aborted' + signal.removeEventListener('abort', onSignalAbort) + // Why: retained transport buffers are released only after the channel has settled or timed out. + resolveAbortOutcome({ reason }) + } + if (failure !== undefined) { + throw failure + } + } + + const abort = (reason: unknown): Promise => { + if (abortPromise) { + return abortPromise + } + if (state === 'complete' || state === 'aborted') { + abortPromise = Promise.resolve() + return abortPromise + } + state = 'aborting' + abortReason = reason + abortPromise = performAbort() + return abortPromise + } + + function onSignalAbort(): void { + void abort(signal.reason).catch(() => {}) + } + + const write = (chunk: Buffer): Promise => { + try { + assertWritable() + } catch (error) { + return Promise.reject(error) + } + if (!Buffer.isBuffer(chunk) || chunk.length === 0) { + return Promise.reject(new Error('SSH relay runtime POSIX file destination chunk is empty')) + } + return runExclusive(async () => { + signal.throwIfAborted() + // Why: the callback is the byte-retention boundary before a source worker reuses its buffer. + const accepted = new Promise((resolve, reject) => { + channel.write(chunk, (error) => (error ? reject(error) : resolve())) + }) + await waitForOperation(accepted) + signal.throwIfAborted() + }) + } + + const close = (): Promise => { + try { + assertWritable() + } catch (error) { + return Promise.reject(error) + } + state = 'closing' + return runExclusive(async () => { + signal.throwIfAborted() + channel.end() + const settled = await Promise.race([ + channelSettlement, + abortOutcome.then((outcome) => ({ ok: false as const, error: outcome.reason })) + ]) + if (state === 'aborting' || state === 'aborted') { + const outcome = await abortOutcome + throw outcome.reason + } + if (!settled.ok) { + throw settled.error + } + signal.throwIfAborted() + state = 'complete' + signal.removeEventListener('abort', onSignalAbort) + }) + } + + signal.addEventListener('abort', onSignalAbort, { once: true }) + if (signal.aborted) { + onSignalAbort() + } + return Object.freeze({ write, close, abort }) +} + +export async function openSshRelayRuntimePosixFileDestination( + options: OpenSshRelayRuntimePosixFileDestinationOptions +): Promise { + validateOptions(options) + options.signal.throwIfAborted() + const channel = await options.openChannel( + buildCommand(options.remotePath, options.mode), + options.signal + ) + validateChannel(channel) + const destination = createDestination(options, channel) + try { + options.signal.throwIfAborted() + return destination + } catch (error) { + try { + await destination.abort(error) + } catch (cleanupError) { + throw new AggregateError( + [error, cleanupError], + 'SSH relay runtime POSIX file open cancellation cleanup failed' + ) + } + throw error + } +}