diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml
index 3312e25d890..1378fb72afb 100644
--- a/.github/workflows/ssh-relay-runtime-artifacts.yml
+++ b/.github/workflows/ssh-relay-runtime-artifacts.yml
@@ -263,6 +263,7 @@ jobs:
src/main/ssh/ssh-relay-release-asset.test.ts \
src/main/ssh/ssh-relay-artifact-selector.test.ts \
src/main/ssh/ssh-relay-artifact-download.test.ts \
+ src/main/ssh/ssh-relay-artifact-extraction.test.ts \
src/main/ssh/ssh-relay-runtime-identity.test.ts \
config/scripts/ssh-relay-runtime-compatibility.test.mjs \
config/scripts/ssh-relay-node-release-verification.test.mjs \
@@ -501,6 +502,23 @@ jobs:
cp "$first_output"/*.tar.br "$first_output"/*.spdx.json "$first_output"/*.provenance.json "$first_output"/*.identity.json \
"$signing_report" "$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}/"
+ - name: Measure full-size desktop extraction boundary
+ shell: bash
+ run: |
+ set -euo pipefail
+ first_output="$RUNNER_TEMP/ssh-relay-runtime/${{ matrix.tuple }}/first"
+ identity="$first_output/orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json"
+ archive=$(find "$first_output" -maxdepth 1 -name '*.tar.br' -print -quit)
+ test -n "$archive"
+ extraction_output="$RUNNER_TEMP/ssh-relay-runtime/${{ matrix.tuple }}/desktop-extraction-measurement"
+ # Why: exact payload size can expose memory behavior that synthetic archive fixtures cannot.
+ ORCA_SSH_RELAY_FULL_SIZE_ARCHIVE="$archive" \
+ ORCA_SSH_RELAY_FULL_SIZE_IDENTITY="$identity" \
+ ORCA_SSH_RELAY_FULL_SIZE_OUTPUT="$extraction_output" \
+ pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 --reporter=verbose \
+ src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts
+ test ! -e "$extraction_output"
+
- name: Upload unpublished artifact evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
@@ -769,6 +787,7 @@ jobs:
src/main/ssh/ssh-relay-release-asset.test.ts `
src/main/ssh/ssh-relay-artifact-selector.test.ts `
src/main/ssh/ssh-relay-artifact-download.test.ts `
+ src/main/ssh/ssh-relay-artifact-extraction.test.ts `
src/main/ssh/ssh-relay-runtime-identity.test.ts `
config/scripts/ssh-relay-runtime-compatibility.test.mjs `
config/scripts/ssh-relay-node-release-verification.test.mjs `
@@ -1010,6 +1029,26 @@ jobs:
Copy-Item -LiteralPath $signingReport -Destination $evidence
Copy-Item -LiteralPath $sourceSignatureReport -Destination $evidence
+ - name: Measure full-size desktop extraction boundary
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Stop'
+ $firstOutput = Join-Path $env:RUNNER_TEMP 'ssh-relay-runtime/${{ matrix.tuple }}/first'
+ $identity = Join-Path $firstOutput 'orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json'
+ $archive = Get-ChildItem -LiteralPath $firstOutput -Filter '*.zip' -File | Select-Object -First 1
+ if (-not $archive) { throw 'runtime ZIP is missing for desktop extraction measurement' }
+ $extractionOutput = Join-Path $env:RUNNER_TEMP 'ssh-relay-runtime/${{ matrix.tuple }}/desktop-extraction-measurement'
+ # Why: exact payload size can expose memory behavior that synthetic archive fixtures cannot.
+ $env:ORCA_SSH_RELAY_FULL_SIZE_ARCHIVE = $archive.FullName
+ $env:ORCA_SSH_RELAY_FULL_SIZE_IDENTITY = $identity
+ $env:ORCA_SSH_RELAY_FULL_SIZE_OUTPUT = $extractionOutput
+ pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 --reporter=verbose `
+ src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts
+ if ($LASTEXITCODE -ne 0) { throw 'full-size desktop extraction measurement failed' }
+ if (Test-Path -LiteralPath $extractionOutput) {
+ throw 'full-size desktop extraction measurement cleanup failed'
+ }
+
- name: Upload unpublished artifact evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs
index 1a2a6de185d..f43c628604e 100644
--- a/config/scripts/ssh-relay-runtime-workflow.test.mjs
+++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs
@@ -94,9 +94,25 @@ describe('SSH relay runtime artifact workflow', () => {
const uploadIndex = steps.findIndex(
(step) => step.name === 'Upload unpublished artifact evidence'
)
+ const measurementIndex = steps.findIndex(
+ (step) => step.name === 'Measure full-size desktop extraction boundary'
+ )
+ const windowsBuildIndex = windowsSteps.findIndex(
+ (step) => step.name === 'Build twice, inspect, smoke, and compare exact runtime'
+ )
+ const windowsMeasurementIndex = windowsSteps.findIndex(
+ (step) => step.name === 'Measure full-size desktop extraction boundary'
+ )
+ const windowsUploadIndex = windowsSteps.findIndex(
+ (step) => step.name === 'Upload unpublished artifact evidence'
+ )
expect(buildIndex).toBeGreaterThan(-1)
- expect(uploadIndex).toBeGreaterThan(buildIndex)
+ expect(windowsBuildIndex).toBeGreaterThan(-1)
+ expect(measurementIndex).toBeGreaterThan(buildIndex)
+ expect(uploadIndex).toBeGreaterThan(measurementIndex)
+ expect(windowsMeasurementIndex).toBeGreaterThan(windowsBuildIndex)
+ expect(windowsUploadIndex).toBeGreaterThan(windowsMeasurementIndex)
expect(source).toContain('verify-ssh-relay-runtime.mjs')
expect(source).toContain('ssh-relay-runtime-workflow.test.mjs')
for (const testName of [
@@ -105,12 +121,16 @@ describe('SSH relay runtime artifact workflow', () => {
'ssh-relay-release-asset.test.ts',
'ssh-relay-artifact-selector.test.ts',
'ssh-relay-artifact-download.test.ts',
+ 'ssh-relay-artifact-extraction.test.ts',
'ssh-relay-runtime-identity.test.ts'
]) {
// Why: portable desktop selection contracts need proof on every native runner family, not
// only the local client architecture or the generic Linux PR job.
expect(source.split(`src/main/ssh/${testName}`)).toHaveLength(3)
}
+ expect(
+ source.split('src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts')
+ ).toHaveLength(3)
expect(source).toContain('pnpm install --frozen-lockfile --ignore-scripts')
expect(source).toContain('--connect-timeout 20 --max-time 300 --retry 2')
expect(source).toContain('mkdir -p "$output_root"')
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 7176802a3f2..95294f2efce 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,16 +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 5 / Work Package 4 desktop cache boundary — in-progress disconnected
-extraction/cache contract audit, 2026-07-15, Codex implementation owner.** Exact-head run
+Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — in-progress native-gate
+preparation for disconnected extraction, 2026-07-15, Codex implementation owner.** Exact-head run
[29449869519](https://github.com/stablyai/orca/actions/runs/29449869519) closes deterministic bounded
`.tar.br` under `E-M5-PORTABLE-ARCHIVE-CI-001`: all six primary Node 24 jobs and both Linux
oldest-userland supplements pass. PR Checks and Golden E2E are green; Windows x64 floor passes and
Windows arm64 passes smoke before the retained hosted build-26200 versus required-26100 rejection.
-The next safe slice is only the isolated desktop extraction/cache resource and failure-semantics
-audit followed by a purpose-named RED. Desktop consumers, SSH transfer/install, mode wiring,
-tuple enablement, publication, production keys/environment/seed, and merge to `main` remain
-disconnected.
+The isolated desktop extractor is locally green under `E-M5-ARTIFACT-EXTRACTION-LOCAL-001` and
+`E-M5-ARTIFACT-EXTRACTION-BROAD-LOCAL-001`: 17 focused tests cover both archive families, hostile
+inputs, exact tree/modes, mutation, cancellation, cleanup, and ownership; broad relay/release/static
+gates pass. An exact downloaded 122,027,869-byte Darwin arm64 tree extracts in 878.73ms with
+49,283,072-byte incremental RSS under `E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001`. Both native job
+families now require synthetic cross-family and post-build full-size gates under
+`E-M5-ARTIFACT-EXTRACTION-CI-WIRING-LOCAL-001`. Next is exact-head all-six Node 24 execution. Desktop
+consumers, cache publication, SSH transfer/install, mode wiring, tuple enablement, publication,
+production keys/environment/seed, and merge to `main` remain disconnected.
## Safety status
@@ -307,9 +312,15 @@ disconnected.
- [ ] Stream bounded downloads; verify signature, size, archive hash, and extracted tree.
The disconnected Electron downloader is locally green under
`E-M5-ARTIFACT-DOWNLOAD-LOCAL-RED-001` and `E-M5-ARTIFACT-DOWNLOAD-LOCAL-001`; exact-head
- all-six native CI is closed under `E-M5-ARTIFACT-DOWNLOAD-CI-001`. Packaged signature loading
- and extracted-tree verification remain open.
+ all-six native CI is closed under `E-M5-ARTIFACT-DOWNLOAD-CI-001`. Disconnected strict
+ TAR/Brotli and ZIP extraction plus complete-tree verification are locally green under
+ `E-M5-ARTIFACT-EXTRACTION-LOCAL-001`; an exact full-size Actions payload is locally within time
+ and memory budgets under `E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001`. Native all-six
+ execution and packaged signature loading remain open.
- [ ] Add exclusive staging, atomic publication, quarantine, locking, and the 2 GiB cache policy.
+ Exclusive owned staging and failure/cancellation cleanup are locally green under
+ `E-M5-ARTIFACT-EXTRACTION-LOCAL-001`; publication, quarantine, locking, and eviction are not
+ implemented yet.
- [ ] Prove verified cached bytes can be transferred while the client is offline.
- [ ] Preserve `ORCA_RELAY_PATH` behind the official-build trust boundary.
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 50e14f97e3c..9a94d3a3a4a 100644
--- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md
+++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md
@@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps
Date created: 2026-07-14
Last updated: 2026-07-15
-Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: disconnected desktop extraction/cache contract audit and RED**. Exact-head run 29449869519 closes the portable POSIX artifact prerequisite under E-M5-PORTABLE-ARCHIVE-CI-001: all six primary Node 24 jobs and both Linux oldest-userland supplements pass deterministic build, strict inspection/extraction, bundled Node/PTY/watcher smoke, equality, and read-back. Audit the existing desktop boundaries and resource/failure contracts, then add only a purpose-named missing extraction/cache-staging RED. Do not add a desktop caller, cache publication, SSH transfer/install, tuple enablement, mode wiring, or default behavior.
-Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact implementation/evidence head `3080eda77fd6a11ed87f536c1963e5b67abe7621` passes artifact run 29449869519 on all six primary native jobs and both Linux supplements. PR Checks 29449869451 and both Golden E2E jobs in run 29449869407 are green. Windows x64 oldest-floor execution passes; Windows arm64 reconstructs and smokes the exact runtime in 7,884.492ms with 49,733,632-byte RSS, then rejects hosted build 26200 against required build 26100. E-M5-PORTABLE-ARCHIVE-CI-001 records exact jobs, runner images, tests, artifact IDs/sizes/hashes, and smoke metrics. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no release write, publication caller, desktop consumer, cache publication/extraction, SSH transfer/install, tuple enablement, or production/default behavior change. Legacy remains the production default.
+Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: disconnected desktop extraction exact-head native gate**. Focused, broad, static, exact downloaded-payload, and native-workflow wiring gates pass locally under E-M5-ARTIFACT-EXTRACTION-LOCAL-001, E-M5-ARTIFACT-EXTRACTION-BROAD-LOCAL-001, E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001, and E-M5-ARTIFACT-EXTRACTION-CI-WIRING-LOCAL-001. Checkpoint the exact implementation SHA, push it, and require all-six Node 24 synthetic cross-family plus native full-size evidence before cache publication begins. Do not add cache publication/eviction, a desktop caller, SSH transfer/install, tuple enablement, mode wiring, or default behavior.
+Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — the 17-test focused suite, 81-test artifact suite, 249-pass relay suite, 279-test release suite, typecheck, lint, and diff gates pass. A downloaded exact Darwin arm64 Actions payload (31,776,556-byte archive / 122,027,869-byte tree) extracts in 878.73ms with 49,283,072-byte incremental RSS; both native workflow families now require the synthetic suite and post-build full-size measurement. Native Node 24 evidence remains open. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no release write, publication caller, cache publication, SSH transfer/install, tuple enablement, or production/default behavior change. 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)
@@ -138,10 +138,10 @@ same change as the work it records.
archive, correction, all-six primary jobs, and both Linux oldest-userland supplements. Windows ZIP
and upstream Node `.tar.xz` remain unchanged.
- Active package: **In progress — 2026-07-15, Codex implementation owner** — disconnected desktop
- extraction/cache contract audit and purpose-named RED. Inventory existing cache/path/atomic-file
- boundaries, formalize resource and integrity failure oracles, and prove the missing contract before
- implementation. Keep cache publication, desktop call sites, SSH behavior, mode wiring, tuple
- enablement, release writes, and defaults intact.
+ extraction capability after E-M5-ARTIFACT-EXTRACTION-AUDIT-001 and
+ E-M5-ARTIFACT-EXTRACTION-LOCAL-RED-001. Implement only strict TAR/Brotli and ZIP staging plus full
+ signed-tree verification and bounded cleanup. Keep cache publication/eviction, desktop call sites,
+ SSH behavior, mode wiring, tuple enablement, release writes, and defaults intact.
- 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
@@ -1084,8 +1084,11 @@ Suggested concrete modules:
- [x] Verify archive SHA-256 before extraction.
E-M5-ARTIFACT-DOWNLOAD-LOCAL-001 and E-M5-ARTIFACT-DOWNLOAD-CI-001.
- [ ] Extract into an exclusive temporary directory with all archive-safety checks from Milestone 2.
- Begin only after the `.tar.br` prerequisite has exact-head all-six artifact evidence.
+ Implemented locally under E-M5-ARTIFACT-EXTRACTION-LOCAL-001 after the `.tar.br` prerequisite;
+ native cross-client and exact full-size evidence remain required before this item closes.
- [ ] Verify the exact expanded tree before atomically publishing the cache entry.
+ The disconnected full-tree verifier passes locally under E-M5-ARTIFACT-EXTRACTION-LOCAL-001;
+ atomic cache publication is deliberately absent and remains a later package.
- [ ] Coordinate cache ownership across multiple Orca processes and windows.
- [ ] Recover stale download/extraction locks without deleting an active writer.
- [ ] Never select partial files, partial directories, or entries with a failed verification record.
@@ -12547,6 +12550,222 @@ config/scripts/ssh-relay-runtime-portable-archive.test.mjs`; changed-file `oxfmt
- Follow-up: checkpoint this evidence and begin only the disconnected desktop extraction/cache
boundary audit and purpose-named RED.
+### E-M5-ARTIFACT-EXTRACTION-AUDIT-001 — Desktop extraction boundary and package split
+
+- Date: 2026-07-15
+- Owner: Codex implementation owner
+- Source: exact local/pushed head `a93e67413a2a1446e8e0f7348af1fdf72737287d` after portable archive
+ evidence; draft PR #8741.
+- Exact audit commands: `rg --files src/main/ssh config/scripts | rg 'ssh-relay|relay-runtime' |
+sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete reads of the desktop
+ schema/consistency/path-policy/selector/downloader plus release TAR/Brotli, ZIP, extraction, and
+ full-tree verification modules.
+- Findings:
+ - the desktop has a signature-verified immutable selected-artifact value and an exclusive,
+ streaming size/hash-verified downloader, but no extraction or cache module and no consumer;
+ - release-job extractors under `config/scripts` are not packaged desktop modules and use the
+ five-minute release boundary rather than the two-minute desktop extraction budget;
+ - the verified selected artifact already carries the exact content ID, archive name/size/hash,
+ entry paths/types/modes/sizes/hashes, file count, expanded size, and portable native-attestation
+ hashes required for cross-client verification;
+ - extraction must revalidate the regular archive file and its signed size/hash before staging,
+ strictly inspect the authenticated archive family, extract only into an exclusive leaf, stream a
+ complete no-extra-file tree verification, detect source mutation, and remove only staging it
+ created on every failure/cancellation;
+ - native execution/trust probes are deliberately not part of desktop cross-client extraction.
+ Their signed hashes are bound by tuple consistency; target-native execution remains a later
+ remote-install gate.
+- Package split: `ssh-relay-artifact-extraction.ts` owns input state, timeout, exclusive staging, and
+ cleanup; `ssh-relay-artifact-tar-extraction.ts` owns strict bounded TAR/Brotli behavior;
+ `ssh-relay-artifact-zip-extraction.ts` owns strict bounded ZIP behavior; and
+ `ssh-relay-artifact-tree-verification.ts` owns streamed complete-tree verification. Cache locking,
+ publication records, quarantine, eviction, and lookup stay in the later
+ `ssh-relay-artifact-cache.ts` package.
+- Budgets: two-minute extraction timeout; 65,536-byte stream chunks; 64 MiB maximum measured desktop
+ incremental RSS; no buffer over 1 MiB; archive/expanded/file/count/path limits remain signed-schema
+ constraints. This package must expose the constants for contract and measurement tests.
+- Consumer-disconnection oracle: this audit and the next capability do not create a cache root,
+ publish an entry, load an embedded key/manifest, call the downloader, transfer over SSH, enable a
+ tuple/mode, or change the legacy default.
+- Does not prove: implementation, hostile archive behavior, packaged Electron behavior, full-size
+ memory/latency, cache concurrency/publication/quarantine/eviction, live SSH, native trust, or any
+ enabled tuple.
+- Follow-up: add the purpose-named cross-family RED, then implement only the disconnected extraction
+ capability across the concrete modules above.
+
+### E-M5-ARTIFACT-EXTRACTION-LOCAL-RED-001 — Packaged desktop extraction capability is absent
+
+- Date: 2026-07-15
+- Owner: Codex implementation owner
+- Source: uncommitted purpose-named
+ `src/main/ssh/ssh-relay-artifact-extraction.test.ts` atop audit head `a93e67413`.
+- Exact command:
+ `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-artifact-extraction.test.ts`
+- Environment: macOS 26.2 arm64, Node v26.0.0, pnpm 10.24.0, Vitest 4.1.5.
+- Result: required RED; one failed suite / zero collected tests in 1.75s Vitest / 7.67s wall with
+ 132,104,192-byte maximum RSS and zero swaps. Import fails because
+ `./ssh-relay-artifact-extraction` does not exist.
+- Contract encoded before implementation:
+ - synthesize real TAR/Brotli and ZIP archives on every client OS;
+ - expose exact 120,000ms / 65,536-byte / 64 MiB incremental-RSS / 1 MiB maximum-buffer budgets;
+ - revalidate signed archive size/hash, strictly reject mismatched and undeclared entries, verify
+ every extracted file byte, and accept no extra tree entry;
+ - create a fresh parent but an exclusive final staging leaf; settle pre-cancellation; remove only
+ owned partial output and preserve an existing owner.
+- Does not prove: implementation, collected test behavior, Node 24/native clients, full-size memory,
+ packaged Electron extraction, cache publication/concurrency, SSH, or any enabled tuple.
+- Follow-up: implement the smallest disconnected extraction capability without cache or consumer
+ wiring, then run focused/release/desktop/static gates and require native CI.
+
+### E-M5-ARTIFACT-EXTRACTION-LOCAL-001 — Disconnected desktop extraction passes locally
+
+- Date: 2026-07-15
+- Owner: Codex implementation owner
+- Source: uncommitted extraction capability and purpose suite atop pushed head
+ `a93e67413a2a1446e8e0f7348af1fdf72737287d`; the exact implementation SHA will be appended after
+ the local broad gates pass and this evidence is checkpointed.
+- Environment: macOS 26.2 arm64, Node v26.0.0, pnpm 10.24.0, Vitest 4.1.5. Node 24/native client
+ execution remains the next gate.
+- Exact focused command:
+ `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-artifact-extraction.test.ts`
+- Focused result: one passed file / 17 passed tests in 1.65s Vitest / 4.15s wall; 158,105,600-byte
+ maximum RSS, 96,179,552-byte peak memory footprint, zero swaps, and 353 page faults. The observed
+ process maximum is 26,001,408 bytes above the 132,104,192-byte missing-module RED process baseline,
+ below the 64 MiB incremental contract. This is a synthetic 8 MiB cancellation/mutation fixture,
+ not the separately required exact full-size artifact measurement.
+- Exact static commands:
+ `pnpm exec oxlint src/main/ssh/ssh-relay-artifact-extraction.ts src/main/ssh/ssh-relay-artifact-tar-extraction.ts src/main/ssh/ssh-relay-artifact-tree-verification.ts src/main/ssh/ssh-relay-artifact-zip-reader.ts src/main/ssh/ssh-relay-artifact-zip-extraction.ts src/main/ssh/ssh-relay-artifact-extraction.test.ts`;
+ `pnpm typecheck`.
+- Static result: both commands exit zero. The implementation is split by concrete responsibility;
+ no max-lines disable or vague helper/common module was added.
+- Oracle proved:
+ - only the immutable selected tuple/archive identity crosses the boundary; the exact signed regular
+ archive size/hash and filesystem state are checked before staging and again after extraction;
+ - TAR/Brotli and ZIP are strictly inspected and streamed on every client OS into an exclusive leaf
+ beneath a resolved parent; POSIX modes and every signed path/type/size/hash are revalidated;
+ - extra/traversal/truncated/case-fold-colliding entries, ZIP symbolic links/encryption shapes,
+ wrong bytes, archive mutation, and existing-owner collisions fail closed;
+ - pre-cancellation and cancellation after staging begins settle, remove only owned partial output,
+ and leave no eligible partial runtime tree;
+ - extraction retains the declared 120,000ms timeout, 65,536-byte chunks, 64 MiB measured
+ incremental-RSS budget, and 1 MiB maximum-write-buffer contract.
+- Consumer-disconnection oracle: there is no cache root or publication record, downloader/manifest
+ loader caller, SSH transfer/install, runtime execution, target setting, Beta option, tuple
+ enablement, release write, or artifact publication. Legacy remains the only production/default
+ path.
+- Does not prove: Node 24 or native Windows/Linux/macOS clients, packaged Electron behavior, exact
+ full-size runtime memory/latency, cache locking/publication/quarantine/eviction, live SSH, native
+ trust, or any enabled tuple.
+- Follow-up: run the broad relay/release/static gates, prove the focused source suite is invoked by
+ both POSIX and Windows native artifact job families, then collect all-six Node 24 and exact
+ full-size payload evidence before beginning cache publication.
+
+### E-M5-ARTIFACT-EXTRACTION-BROAD-RED-001 — Broad run exposes late rejection observer
+
+- Date: 2026-07-15
+- Owner: Codex implementation owner
+- Source: uncommitted E-M5-ARTIFACT-EXTRACTION-LOCAL-001 tree atop `a93e67413`.
+- Exact command:
+ `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-artifact-*.test.ts`
+- Result: required RED; three passed files / one failed file, 80 passed tests / one failed test, and
+ one unhandled rejection in 2.76s. The authenticated mutation test correctly fails closed with
+ `unexpected EOF`, but can reject during its awaited `writeFile` before the later Vitest assertion
+ attaches, so the runner reports `PromiseRejectionHandledWarning` and an unhandled rejection.
+- Classification: purpose-test timing defect, not a product extraction acceptance. The owned staging
+ tree is still removed and the mutation is rejected.
+- Correction: attach the Vitest rejection observer when extraction starts, before waiting for the
+ staging path or mutating/aborting; accept the valid fail-closed `unexpected EOF` diagnostic.
+- Follow-up: rerun the exact broad artifact command and require zero unhandled errors before any CI
+ checkpoint.
+
+### E-M5-ARTIFACT-EXTRACTION-BROAD-LOCAL-001 — Broad local relay and release gates pass
+
+- Date: 2026-07-15
+- Owner: Codex implementation owner
+- Source: corrected local extraction tree atop `a93e67413`; exact implementation SHA pending the
+ checkpoint commit.
+- Exact commands and results:
+ - `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-artifact-*.test.ts`:
+ four passed files / 81 passed tests in 10.40s after the mutation observer correction, with zero
+ unhandled errors;
+ - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts`:
+ 17 passed files / one platform-skipped file, 249 passed tests / one platform-skipped test in
+ 21.60s Vitest / 24.97s wall, 244,383,744-byte maximum process RSS and zero swaps;
+ - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs`:
+ 50 passed files / 279 passed tests in 45.04s Vitest / 47.58s wall, 190,840,832-byte maximum
+ process RSS and zero swaps;
+ - `pnpm typecheck`; `pnpm lint`; `git diff --check`: all exit zero. Full lint retains only existing
+ warnings outside this package; reliability gates, max-lines ratchet, bundled skills, localization
+ catalog, and localization coverage pass.
+- Diff oracle: no diff exists in the preserved Milestone 0 resolver pair or
+ `config/scripts/verify-release-required-assets.mjs` and its test. Only the concrete extraction,
+ measurement, workflow-wiring, and living-plan files are in scope.
+- Does not prove: Node 24/native clients, exact full-size payload behavior, cache, live SSH, native
+ trust, or any enabled tuple.
+- Follow-up: measure an exact downloaded payload, close native workflow wiring, then checkpoint and
+ push the implementation for all-six execution.
+
+### E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001 — Exact downloaded payload stays within budget
+
+- Date: 2026-07-15
+- Owner: Codex implementation owner
+- Source artifact: Actions artifact ID 8357083684, `ssh-relay-runtime-darwin-arm64`, downloaded from
+ exact implementation run [29449869519](https://github.com/stablyai/orca/actions/runs/29449869519)
+ using `gh run download 29449869519 --repo stablyai/orca --name ssh-relay-runtime-darwin-arm64`.
+- Payload: `darwin-arm64`, archive SHA-256
+ `2031bea0f820107f8c92ab62a446fe0ebbeb64e78b63b6707c0610216ac68e3d`, 31,776,556 compressed
+ bytes, 122,027,869 expanded bytes, and 35 files.
+- Exact measurement command: set `ORCA_SSH_RELAY_FULL_SIZE_ARCHIVE`,
+ `ORCA_SSH_RELAY_FULL_SIZE_IDENTITY`, and an absent owned `ORCA_SSH_RELAY_FULL_SIZE_OUTPUT`, then
+ run `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 --reporter=verbose src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts`.
+- Result: four consecutive post-harness runs pass locally. The retained verbose run extracts,
+ complete-tree verifies, and cleans the exact payload in 878.731667ms with a 72,679,424-byte
+ baseline RSS, 121,962,496-byte sampled peak RSS, and 49,283,072-byte incremental RSS. This is below
+ the 120,000ms and 67,108,864-byte limits.
+- Harness boundary: the measurement uses the authenticated Actions identity only to construct the
+ top-level selected-artifact shape; its explicit why-comment prohibits product use. The production
+ extractor still rehashes the archive, inspects it, creates exclusive staging, verifies the complete
+ tree, rehashes the archive, and cleans output. Signature selection itself is separately proven by
+ E-M5-OFFLINE-SELECTION-LOCAL-001/CI-001.
+- Does not prove: Node 24, other native clients or archive families at full size, packaged Electron,
+ cache, SSH, or an enabled tuple.
+- Follow-up: require this measurement after native build and before upload on all six artifact jobs;
+ retain synthetic both-family execution on every client runner.
+
+### E-M5-ARTIFACT-EXTRACTION-CI-WIRING-RED-001 — Native jobs omit extraction proof
+
+- Date: 2026-07-15
+- Owner: Codex implementation owner
+- Exact command:
+ `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-workflow.test.mjs`
+- Result: required RED; one failed / six passed tests in 483ms. The new source contract expects the
+ purpose suite in both native job families, but the workflow contains zero occurrences, so the
+ split count is one rather than three.
+- Follow-up: invoke the synthetic cross-family suite in both native contract steps and add a
+ post-build full-size budget measurement before artifact upload in both job families.
+
+### E-M5-ARTIFACT-EXTRACTION-CI-WIRING-LOCAL-001 — Both native job families require extraction gates
+
+- Date: 2026-07-15
+- Owner: Codex implementation owner
+- Source: local workflow/test/full-size harness atop `a93e67413`; exact implementation SHA pending
+ the checkpoint commit.
+- Exact command:
+ `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-workflow.test.mjs`
+- Result: one passed file / seven passed tests in 404ms after formatting; typecheck also exits zero.
+- Oracle proved: all four POSIX native clients and both Windows native clients run the 17-test
+ synthetic TAR/Brotli-plus-ZIP suite under Node 24. After each exact native build, a separate step
+ runs the one-test full-size measurement against that job's first verified archive/identity, checks
+ 120,000ms / 64 MiB, cleans its owned output, and must pass before upload. The workflow contract
+ requires both measurement steps to occur after build and before upload.
+- Consumer-disconnection oracle: workflow artifacts remain unpublished Actions evidence; no release
+ write, packaged desktop consumer, cache publication, SSH transfer/install, tuple, or mode is
+ connected.
+- Does not prove: actual runner execution, cross-family full-size payloads on every client, packaged
+ Electron, cache, SSH, or any enabled tuple.
+- Follow-up: checkpoint, push, and collect exact-head per-job synthetic/full-size metrics from all six
+ native jobs before beginning the cache-publication package.
+
## Accepted Gaps
No product gap is accepted merely because it appears in this list. Each entry requires explicit
@@ -12605,9 +12824,10 @@ The project is not complete until every applicable item below is checked with ev
## Next Required Action
-Audit the disconnected desktop extraction/cache boundary and add only the purpose-named missing
-resource/failure-semantics RED. Do not implement a desktop call site, cache publication, SSH
-transfer/install, mode wiring, tuple enablement, or default behavior in that audit/test package. Keep
+Run the broader release/desktop/static gates for the locally green disconnected extraction
+capability, wire its purpose suite into both native artifact job families, and collect exact-head
+all-six Node 24 plus full-size payload evidence. Do not add a desktop call site, cache
+publication/eviction, SSH transfer/install, mode wiring, tuple enablement, 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, release-cut, desktop builds, publication,
diff --git a/src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts b/src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts
new file mode 100644
index 00000000000..bd4f4e2c08a
--- /dev/null
+++ b/src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts
@@ -0,0 +1,114 @@
+import { readFile, rm, stat } from 'node:fs/promises'
+import { performance } from 'node:perf_hooks'
+
+import { describe, expect, it } from 'vitest'
+
+import {
+ extractSshRelayArtifact,
+ SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS
+} from './ssh-relay-artifact-extraction'
+import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector'
+
+type MeasurementIdentity = {
+ tupleId: SshRelaySelectedArtifact['tupleId']
+ contentId: SshRelaySelectedArtifact['contentId']
+ os: SshRelaySelectedArtifact['tuple']['os']
+ archive: SshRelaySelectedArtifact['tuple']['archive']
+ entries: SshRelaySelectedArtifact['tuple']['entries']
+}
+
+const archivePath = process.env.ORCA_SSH_RELAY_FULL_SIZE_ARCHIVE
+const identityPath = process.env.ORCA_SSH_RELAY_FULL_SIZE_IDENTITY
+const outputDirectory = process.env.ORCA_SSH_RELAY_FULL_SIZE_OUTPUT
+const hasMeasurementInput = Boolean(archivePath && identityPath && outputDirectory)
+
+function measurementIdentity(input: unknown): MeasurementIdentity {
+ if (
+ typeof input !== 'object' ||
+ input === null ||
+ !('tupleId' in input) ||
+ !('contentId' in input) ||
+ !('os' in input) ||
+ !('archive' in input) ||
+ !('entries' in input) ||
+ !Array.isArray(input.entries)
+ ) {
+ throw new Error('Full-size extraction measurement identity is incomplete')
+ }
+ return input as MeasurementIdentity
+}
+
+function measurementArtifact(identity: MeasurementIdentity): SshRelaySelectedArtifact {
+ const tuple = identity as unknown as SshRelaySelectedArtifact['tuple']
+ // Why: this runner measures exact Actions artifact resources; it is never a product trust bypass.
+ return Object.freeze({
+ kind: 'selected',
+ tupleId: identity.tupleId,
+ contentId: identity.contentId,
+ releaseTag: 'measurement-only',
+ archive: Object.freeze({
+ ...identity.archive,
+ downloadUrl: 'https://invalid.example/measurement-only'
+ }),
+ tuple
+ })
+}
+
+describe.skipIf(!hasMeasurementInput)('SSH relay full-size artifact extraction', () => {
+ it(
+ 'stays within the desktop time and incremental-memory budgets',
+ async () => {
+ const identity = measurementIdentity(
+ JSON.parse(await readFile(identityPath as string, 'utf8')) as unknown
+ )
+ await expect(stat(outputDirectory as string)).rejects.toMatchObject({ code: 'ENOENT' })
+ const baselineRss = process.memoryUsage().rss
+ let peakRss = baselineRss
+ const sample = (): void => {
+ peakRss = Math.max(peakRss, process.memoryUsage().rss)
+ }
+ const sampler = setInterval(sample, 1)
+ const startedAt = performance.now()
+ let result
+ try {
+ result = await extractSshRelayArtifact({
+ artifact: measurementArtifact(identity),
+ archivePath: archivePath as string,
+ outputDirectory: outputDirectory as string
+ })
+ } finally {
+ clearInterval(sampler)
+ sample()
+ }
+ const elapsedMs = performance.now() - startedAt
+ const incrementalRssBytes = Math.max(0, peakRss - baselineRss)
+ console.log(
+ `ssh_relay_full_size_extraction=${JSON.stringify({
+ tupleId: identity.tupleId,
+ archiveBytes: identity.archive.size,
+ expandedBytes: identity.archive.expandedSize,
+ files: identity.archive.fileCount,
+ elapsedMs,
+ baselineRss,
+ peakRss,
+ incrementalRssBytes
+ })}`
+ )
+ try {
+ expect(result).toMatchObject({
+ tupleId: identity.tupleId,
+ contentId: identity.contentId,
+ files: identity.archive.fileCount,
+ expandedBytes: identity.archive.expandedSize
+ })
+ expect(elapsedMs).toBeLessThanOrEqual(SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS.timeoutMs)
+ expect(incrementalRssBytes).toBeLessThanOrEqual(
+ SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS.maximumIncrementalMemoryBytes
+ )
+ } finally {
+ await rm(outputDirectory as string, { recursive: true })
+ }
+ },
+ SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS.timeoutMs + 10_000
+ )
+})
diff --git a/src/main/ssh/ssh-relay-artifact-extraction.test.ts b/src/main/ssh/ssh-relay-artifact-extraction.test.ts
new file mode 100644
index 00000000000..44d1519a918
--- /dev/null
+++ b/src/main/ssh/ssh-relay-artifact-extraction.test.ts
@@ -0,0 +1,503 @@
+import { createHash } from 'node:crypto'
+import { lstat, mkdtemp, readFile, realpath, rm, stat, writeFile } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { brotliCompressSync, constants as zlibConstants } from 'node:zlib'
+
+import nacl from 'tweetnacl'
+import { afterEach, describe, expect, it } from 'vitest'
+import yazl from 'yazl'
+
+import {
+ extractSshRelayArtifact,
+ SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS
+} from './ssh-relay-artifact-extraction'
+import type { SshRelayArtifactManifest, SshRelayRuntimeTuple } from './ssh-relay-artifact-schema'
+import {
+ createSshRelayArtifactTestManifest,
+ createSshRelayWindowsArtifactTestManifest
+} from './ssh-relay-artifact-test-manifest'
+import {
+ selectSshRelayArtifact,
+ type SshRelayHostEvidence,
+ type SshRelaySelectedArtifact
+} from './ssh-relay-artifact-selector'
+import {
+ signSshRelayArtifactManifest,
+ sshRelayManifestKeyId,
+ verifySshRelayArtifactManifest
+} from './ssh-relay-manifest-signature'
+import { sshRelayRuntimeArchiveName } from './ssh-relay-release-asset'
+import { computeSshRelayRuntimeContentId, type SshRelayDigest } from './ssh-relay-runtime-identity'
+
+const keyPair = nacl.sign.keyPair.fromSeed(Uint8Array.from({ length: 32 }, (_, index) => index))
+const temporaryDirectories: string[] = []
+
+function sha256(bytes: Uint8Array): SshRelayDigest {
+ return `sha256:${createHash('sha256').update(bytes).digest('hex')}`
+}
+
+function tarString(header: Buffer, value: string, offset: number, length: number): void {
+ header.write(value, offset, length, 'utf8')
+}
+
+function tarOctal(header: Buffer, value: number, offset: number, length: number): void {
+ header.write(value.toString(8).padStart(length - 1, '0'), offset, length - 1, 'ascii')
+ header[offset + length - 1] = 0
+}
+
+function tarHeader(entry: SshRelayRuntimeTuple['entries'][number], size: number): Buffer {
+ const header = Buffer.alloc(512)
+ tarString(header, entry.type === 'directory' ? `${entry.path}/` : entry.path, 0, 100)
+ tarOctal(header, entry.mode, 100, 8)
+ tarOctal(header, 0, 108, 8)
+ tarOctal(header, 0, 116, 8)
+ tarOctal(header, size, 124, 12)
+ tarOctal(header, 0, 136, 12)
+ header.fill(0x20, 148, 156)
+ header[156] = entry.type === 'directory' ? 0x35 : 0x30
+ tarString(header, 'ustar\0', 257, 6)
+ tarString(header, '00', 263, 2)
+ const checksum = header.reduce((total, byte) => total + byte, 0)
+ header.write(`${checksum.toString(8).padStart(6, '0')}\0 `, 148, 8, 'ascii')
+ return header
+}
+
+function tarBrotli(
+ tuple: SshRelayRuntimeTuple,
+ fileBytes: ReadonlyMap,
+ extraPath?: string
+): Buffer {
+ const blocks: Buffer[] = []
+ for (const entry of tuple.entries) {
+ const bytes = entry.type === 'file' ? fileBytes.get(entry.path) : undefined
+ if (entry.type === 'file' && !bytes) {
+ throw new Error(`Missing test bytes for ${entry.path}`)
+ }
+ blocks.push(tarHeader(entry, bytes?.length ?? 0))
+ if (bytes) {
+ blocks.push(bytes, Buffer.alloc((512 - (bytes.length % 512)) % 512))
+ }
+ }
+ if (extraPath) {
+ const bytes = Buffer.from('undeclared archive bytes')
+ blocks.push(
+ tarHeader(
+ {
+ path: extraPath,
+ type: 'file',
+ role: 'license',
+ mode: 0o644,
+ size: 0,
+ sha256: sha256(bytes)
+ },
+ bytes.length
+ ),
+ bytes,
+ Buffer.alloc((512 - (bytes.length % 512)) % 512)
+ )
+ }
+ blocks.push(Buffer.alloc(1024))
+ return brotliCompressSync(Buffer.concat(blocks), {
+ params: {
+ [zlibConstants.BROTLI_PARAM_QUALITY]: 9,
+ [zlibConstants.BROTLI_PARAM_LGWIN]: 20
+ }
+ })
+}
+
+async function zipArchive(
+ tuple: SshRelayRuntimeTuple,
+ fileBytes: ReadonlyMap,
+ {
+ extraPath,
+ symlinkPath
+ }: {
+ extraPath?: string
+ symlinkPath?: string
+ }
+): Promise {
+ const zip = new yazl.ZipFile()
+ const mtime = new Date('2026-07-14T00:00:00.000Z')
+ for (const entry of tuple.entries) {
+ const options = { mode: entry.mode, mtime, forceDosTimestamp: true }
+ if (entry.type === 'directory') {
+ zip.addEmptyDirectory(entry.path, options)
+ } else {
+ const bytes = fileBytes.get(entry.path)
+ if (!bytes) {
+ throw new Error(`Missing test bytes for ${entry.path}`)
+ }
+ zip.addBuffer(bytes, entry.path, {
+ ...options,
+ mode: entry.path === symlinkPath ? 0o120777 : entry.mode,
+ compress: true,
+ compressionLevel: 9
+ })
+ }
+ }
+ if (extraPath) {
+ zip.addBuffer(Buffer.from('undeclared archive bytes'), extraPath, {
+ mode: 0o644,
+ mtime,
+ forceDosTimestamp: true
+ })
+ }
+ zip.end({ forceZip64Format: false })
+ const chunks: Buffer[] = []
+ for await (const chunk of zip.outputStream) {
+ chunks.push(Buffer.from(chunk))
+ }
+ return Buffer.concat(chunks)
+}
+
+function markZipEntriesEncrypted(archive: Buffer): Buffer {
+ const changed = Buffer.from(archive)
+ for (let offset = 0; offset <= changed.length - 10; offset += 1) {
+ const signature = changed.readUInt32LE(offset)
+ const flagOffset =
+ signature === 0x04034b50 ? offset + 6 : signature === 0x02014b50 ? offset + 8 : -1
+ if (flagOffset >= 0) {
+ changed.writeUInt16LE(changed.readUInt16LE(flagOffset) | 0x0001, flagOffset)
+ }
+ }
+ return changed
+}
+
+type Fixture = {
+ archivePath: string
+ artifact: SshRelaySelectedArtifact
+ fileBytes: ReadonlyMap
+ root: string
+}
+
+async function fixture({
+ os,
+ mismatchedPath,
+ extraPath,
+ largeFileBytes,
+ truncated,
+ encryptedZip,
+ symlinkPath
+}: {
+ os: 'linux' | 'win32'
+ mismatchedPath?: string
+ extraPath?: string
+ largeFileBytes?: number
+ truncated?: boolean
+ encryptedZip?: boolean
+ symlinkPath?: string
+}): Promise {
+ const root = await mkdtemp(join(tmpdir(), 'orca-relay-extraction-'))
+ temporaryDirectories.push(root)
+ const manifest: SshRelayArtifactManifest =
+ os === 'win32'
+ ? createSshRelayWindowsArtifactTestManifest()
+ : createSshRelayArtifactTestManifest()
+ const tuple = manifest.tuples[0]
+ const fileBytes = new Map()
+ const firstFilePath = tuple.entries.find((entry) => entry.type === 'file')?.path
+ for (const entry of tuple.entries) {
+ if (entry.type !== 'file') {
+ continue
+ }
+ const bytes =
+ entry.path === firstFilePath && largeFileBytes
+ ? Buffer.alloc(largeFileBytes, 0x61)
+ : Buffer.from(`desktop extraction fixture:${tuple.tupleId}:${entry.path}`)
+ fileBytes.set(entry.path, bytes)
+ entry.size = bytes.length
+ entry.sha256 = sha256(bytes)
+ }
+ for (const attestation of tuple.nativeVerification.files) {
+ const entry = tuple.entries.find((candidate) => candidate.path === attestation.path)
+ if (!entry || entry.type !== 'file') {
+ throw new Error(`Missing attested fixture entry: ${attestation.path}`)
+ }
+ attestation.sha256 = entry.sha256
+ }
+ tuple.contentId = computeSshRelayRuntimeContentId(tuple)
+ tuple.archive.name = sshRelayRuntimeArchiveName(tuple.tupleId, tuple.contentId)
+ const files = tuple.entries.filter((entry) => entry.type === 'file')
+ tuple.archive.fileCount = files.length
+ tuple.archive.expandedSize = files.reduce((total, entry) => total + entry.size, 0)
+
+ const archiveFileBytes = new Map(fileBytes)
+ if (mismatchedPath) {
+ archiveFileBytes.set(mismatchedPath, Buffer.from('authenticated archive has wrong tree bytes'))
+ }
+ let archive =
+ os === 'win32'
+ ? await zipArchive(tuple, archiveFileBytes, { extraPath, symlinkPath })
+ : tarBrotli(tuple, archiveFileBytes, extraPath)
+ if (encryptedZip) {
+ archive = markZipEntriesEncrypted(archive)
+ }
+ if (truncated) {
+ archive = archive.subarray(0, Math.max(1, archive.length - 64))
+ }
+ tuple.archive.size = archive.length
+ tuple.archive.sha256 = sha256(archive)
+ manifest.signatures = [signSshRelayArtifactManifest(manifest, keyPair.secretKey)]
+ const verified = verifySshRelayArtifactManifest(manifest, [
+ { keyId: sshRelayManifestKeyId(keyPair.publicKey), publicKey: keyPair.publicKey }
+ ])
+ const host: SshRelayHostEvidence =
+ os === 'win32'
+ ? {
+ os,
+ architecture: 'x64',
+ processTranslated: false,
+ build: 22631,
+ openSshVersion: '9.5p1',
+ powerShellVersion: '5.1',
+ dotNetFrameworkRelease: 528040
+ }
+ : {
+ os,
+ architecture: 'x64',
+ processTranslated: false,
+ kernelVersion: '6.8',
+ libc: { family: 'glibc', version: '2.39' },
+ libstdcxxVersion: '6.0.33',
+ glibcxxVersion: '3.4.33'
+ }
+ const selected = selectSshRelayArtifact(verified, host)
+ if (selected.kind !== 'selected') {
+ throw new Error(`Expected selected fixture, got ${selected.reason}`)
+ }
+ const archivePath = join(root, tuple.archive.name)
+ await writeFile(archivePath, archive, { mode: 0o600 })
+ return { archivePath, artifact: selected, fileBytes, root }
+}
+
+async function waitForPath(path: string): Promise {
+ const deadline = Date.now() + 5_000
+ for (;;) {
+ try {
+ await stat(path)
+ return
+ } catch (error) {
+ if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT') {
+ throw error
+ }
+ if (Date.now() >= deadline) {
+ throw new Error(`Timed out waiting for test path: ${path}`)
+ }
+ await new Promise((resolve) => setTimeout(resolve, 1))
+ }
+ }
+}
+
+afterEach(async () => {
+ await Promise.all(
+ temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true }))
+ )
+})
+
+describe('SSH relay artifact extraction', () => {
+ it('declares the desktop extraction resource contract', () => {
+ expect(SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS).toEqual({
+ timeoutMs: 2 * 60_000,
+ chunkBytes: 64 * 1024,
+ maximumIncrementalMemoryBytes: 64 * 1024 * 1024,
+ maximumWriteBufferBytes: 1024 * 1024
+ })
+ })
+
+ it.each(['linux', 'win32'] as const)(
+ 'extracts and verifies an authenticated %s tree into an exclusive staging directory',
+ async (os) => {
+ const value = await fixture({ os })
+ const outputDirectory = join(value.root, 'fresh-parent', 'runtime')
+
+ const result = await extractSshRelayArtifact({
+ artifact: value.artifact,
+ archivePath: value.archivePath,
+ outputDirectory
+ })
+ expect(result).toEqual({
+ tupleId: value.artifact.tupleId,
+ contentId: value.artifact.contentId,
+ runtimeRoot: await realpath(outputDirectory),
+ files: value.artifact.tuple.archive.fileCount,
+ expandedBytes: value.artifact.tuple.archive.expandedSize
+ })
+ for (const [path, bytes] of value.fileBytes) {
+ expect(await readFile(join(outputDirectory, ...path.split('/')))).toEqual(bytes)
+ }
+ if (process.platform !== 'win32') {
+ for (const entry of value.artifact.tuple.entries) {
+ const metadata = await lstat(join(outputDirectory, ...entry.path.split('/')))
+ expect(metadata.mode & 0o777, entry.path).toBe(entry.mode)
+ }
+ }
+ }
+ )
+
+ it('rejects changed archive bytes before creating staging output', async () => {
+ const value = await fixture({ os: 'linux' })
+ await writeFile(value.archivePath, 'changed after authenticated download')
+ const outputDirectory = join(value.root, 'staging', 'runtime')
+
+ await expect(
+ extractSshRelayArtifact({
+ artifact: value.artifact,
+ archivePath: value.archivePath,
+ outputDirectory
+ })
+ ).rejects.toThrow(/size|sha-?256|archive/i)
+ await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' })
+ })
+
+ it.each(['linux', 'win32'] as const)(
+ 'rejects an authenticated %s archive whose extracted tree disagrees with the manifest',
+ async (os) => {
+ const value = await fixture({ os, mismatchedPath: 'relay.js' })
+ const outputDirectory = join(value.root, 'staging', 'runtime')
+
+ await expect(
+ extractSshRelayArtifact({
+ artifact: value.artifact,
+ archivePath: value.archivePath,
+ outputDirectory
+ })
+ ).rejects.toThrow(/integrity|sha-?256|size|tree/i)
+ await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' })
+ }
+ )
+
+ it.each(['linux', 'win32'] as const)(
+ 'rejects an authenticated %s archive with an undeclared entry',
+ async (os) => {
+ const value = await fixture({ os, extraPath: 'unexpected.txt' })
+ const outputDirectory = join(value.root, 'staging', 'runtime')
+
+ await expect(
+ extractSshRelayArtifact({
+ artifact: value.artifact,
+ archivePath: value.archivePath,
+ outputDirectory
+ })
+ ).rejects.toThrow(/entry-count|extra|undeclared/i)
+ await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' })
+ }
+ )
+
+ it('rejects an authenticated TAR traversal without writing outside owned staging', async () => {
+ const value = await fixture({ os: 'linux', extraPath: '../escaped.txt' })
+ const outputDirectory = join(value.root, 'staging', 'runtime')
+ const escapedPath = join(value.root, 'staging', 'escaped.txt')
+
+ await expect(
+ extractSshRelayArtifact({
+ artifact: value.artifact,
+ archivePath: value.archivePath,
+ outputDirectory
+ })
+ ).rejects.toThrow(/extra|path|undeclared|traversal/i)
+ await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' })
+ await expect(stat(escapedPath)).rejects.toMatchObject({ code: 'ENOENT' })
+ })
+
+ it.each(['linux', 'win32'] as const)(
+ 'rejects an authenticated truncated %s archive and removes owned staging',
+ async (os) => {
+ const value = await fixture({ os, truncated: true })
+ const outputDirectory = join(value.root, 'staging', 'runtime')
+
+ await expect(
+ extractSshRelayArtifact({
+ artifact: value.artifact,
+ archivePath: value.archivePath,
+ outputDirectory
+ })
+ ).rejects.toThrow(/archive|brotli|end|invalid|tar|zip/i)
+ await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' })
+ }
+ )
+
+ it.each([
+ ['case-fold collision', { extraPath: 'RELAY.JS' }, /case-fold|duplicate/i],
+ ['symbolic link', { symlinkPath: 'relay.js' }, /symbolic link|type|mode/i],
+ // yauzl may reject the encrypted-size shape before exposing the flagged entry to our guard.
+ ['encrypted entry', { encryptedZip: true }, /encrypted|size mismatch/i]
+ ] as const)('rejects an authenticated ZIP %s', async (_name, options, expectedError) => {
+ const value = await fixture({ os: 'win32', ...options })
+ const outputDirectory = join(value.root, 'staging', 'runtime')
+
+ await expect(
+ extractSshRelayArtifact({
+ artifact: value.artifact,
+ archivePath: value.archivePath,
+ outputDirectory
+ })
+ ).rejects.toThrow(expectedError)
+ await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' })
+ })
+
+ it('detects archive mutation after staging begins and removes its partial tree', async () => {
+ const value = await fixture({ os: 'win32', largeFileBytes: 8 * 1024 * 1024 })
+ const outputDirectory = join(value.root, 'staging', 'runtime')
+ const rejection = expect(
+ extractSshRelayArtifact({
+ artifact: value.artifact,
+ archivePath: value.archivePath,
+ outputDirectory
+ })
+ ).rejects.toThrow(/archive|central directory|changed|invalid|unexpected EOF|zip/i)
+
+ await waitForPath(outputDirectory)
+ await writeFile(value.archivePath, Buffer.alloc(value.artifact.archive.size, 0x62))
+
+ await rejection
+ await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' })
+ }, 15_000)
+
+ it('settles in-flight cancellation and removes only its partial staging tree', async () => {
+ const value = await fixture({ os: 'win32', largeFileBytes: 8 * 1024 * 1024 })
+ const outputDirectory = join(value.root, 'staging', 'runtime')
+ const controller = new AbortController()
+ const rejection = expect(
+ extractSshRelayArtifact({
+ artifact: value.artifact,
+ archivePath: value.archivePath,
+ outputDirectory,
+ signal: controller.signal
+ })
+ ).rejects.toThrow(/cancel|abort/i)
+
+ await waitForPath(outputDirectory)
+ controller.abort(new Error('cancel in-flight desktop extraction'))
+
+ await rejection
+ await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' })
+ }, 15_000)
+
+ it('settles pre-cancellation and preserves an existing output owner', async () => {
+ const value = await fixture({ os: 'linux' })
+ const controller = new AbortController()
+ controller.abort(new Error('cancel desktop extraction'))
+ const cancelledOutput = join(value.root, 'cancelled', 'runtime')
+ await expect(
+ extractSshRelayArtifact({
+ artifact: value.artifact,
+ archivePath: value.archivePath,
+ outputDirectory: cancelledOutput,
+ signal: controller.signal
+ })
+ ).rejects.toThrow(/cancel desktop extraction/i)
+ await expect(stat(cancelledOutput)).rejects.toMatchObject({ code: 'ENOENT' })
+
+ const existingOutput = join(value.root, 'existing')
+ await writeFile(existingOutput, 'owner bytes')
+ await expect(
+ extractSshRelayArtifact({
+ artifact: value.artifact,
+ archivePath: value.archivePath,
+ outputDirectory: existingOutput
+ })
+ ).rejects.toMatchObject({ code: 'EEXIST' })
+ expect(await readFile(existingOutput, 'utf8')).toBe('owner bytes')
+ })
+})
diff --git a/src/main/ssh/ssh-relay-artifact-extraction.ts b/src/main/ssh/ssh-relay-artifact-extraction.ts
new file mode 100644
index 00000000000..8561d7c0d5f
--- /dev/null
+++ b/src/main/ssh/ssh-relay-artifact-extraction.ts
@@ -0,0 +1,177 @@
+import { createHash } from 'node:crypto'
+import { createReadStream } from 'node:fs'
+import { lstat, mkdir, realpath, rm } from 'node:fs/promises'
+import { basename, dirname, resolve } from 'node:path'
+
+import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector'
+import {
+ extractSshRelayTarBrotli,
+ inspectSshRelayTarBrotli
+} from './ssh-relay-artifact-tar-extraction'
+import { verifySshRelayArtifactTree } from './ssh-relay-artifact-tree-verification'
+import { extractSshRelayZip, inspectSshRelayZip } from './ssh-relay-artifact-zip-extraction'
+
+const EXTRACTION_TIMEOUT_MS = 2 * 60_000
+const CHUNK_BYTES = 64 * 1024
+const MAXIMUM_INCREMENTAL_MEMORY_BYTES = 64 * 1024 * 1024
+const MAXIMUM_WRITE_BUFFER_BYTES = 1024 * 1024
+
+type ArchiveState = {
+ dev: bigint
+ ino: bigint
+ size: bigint
+ mtimeNs: bigint
+ ctimeNs: bigint
+}
+
+function sameArchiveState(left: ArchiveState, right: ArchiveState): boolean {
+ return (
+ left.dev === right.dev &&
+ left.ino === right.ino &&
+ left.size === right.size &&
+ left.mtimeNs === right.mtimeNs &&
+ left.ctimeNs === right.ctimeNs
+ )
+}
+
+async function describeArchive(
+ archivePath: string,
+ expectedSize: number,
+ signal: AbortSignal
+): Promise<{ sha256: string; state: ArchiveState }> {
+ signal.throwIfAborted()
+ const before = await lstat(archivePath, { bigint: true })
+ if (!before.isFile() || before.isSymbolicLink() || before.size !== BigInt(expectedSize)) {
+ throw new Error('SSH relay extraction input must be the exact signed regular archive file')
+ }
+ const digest = createHash('sha256')
+ let size = 0
+ for await (const chunk of createReadStream(archivePath, {
+ highWaterMark: CHUNK_BYTES,
+ signal
+ })) {
+ size += chunk.length
+ if (size > expectedSize) {
+ throw new Error('SSH relay extraction input exceeds its signed size')
+ }
+ digest.update(chunk)
+ }
+ const after = await lstat(archivePath, { bigint: true })
+ if (!sameArchiveState(before, after) || size !== expectedSize) {
+ throw new Error('SSH relay extraction input changed while hashing')
+ }
+ return { sha256: `sha256:${digest.digest('hex')}`, state: after }
+}
+
+function assertSelectedArtifact(artifact: SshRelaySelectedArtifact): void {
+ // Why: only the recursively frozen signature-verified selector result may cross this boundary.
+ if (
+ artifact.tupleId !== artifact.tuple.tupleId ||
+ artifact.contentId !== artifact.tuple.contentId ||
+ artifact.archive.name !== artifact.tuple.archive.name ||
+ artifact.archive.size !== artifact.tuple.archive.size ||
+ artifact.archive.sha256 !== artifact.tuple.archive.sha256
+ ) {
+ throw new Error('SSH relay selected artifact identity is inconsistent')
+ }
+}
+
+export type SshRelayArtifactExtractionResult = {
+ tupleId: SshRelaySelectedArtifact['tupleId']
+ contentId: SshRelaySelectedArtifact['contentId']
+ runtimeRoot: string
+ files: number
+ expandedBytes: number
+}
+
+export async function extractSshRelayArtifact({
+ artifact,
+ archivePath,
+ outputDirectory,
+ signal
+}: {
+ artifact: SshRelaySelectedArtifact
+ archivePath: string
+ outputDirectory: string
+ signal?: AbortSignal
+}): Promise {
+ const effectiveSignal = signal
+ ? AbortSignal.any([signal, AbortSignal.timeout(EXTRACTION_TIMEOUT_MS)])
+ : AbortSignal.timeout(EXTRACTION_TIMEOUT_MS)
+ effectiveSignal.throwIfAborted()
+ assertSelectedArtifact(artifact)
+ const absoluteArchive = resolve(archivePath)
+ const absoluteOutput = resolve(outputDirectory)
+ const before = await describeArchive(absoluteArchive, artifact.archive.size, effectiveSignal)
+ if (before.sha256 !== artifact.archive.sha256) {
+ throw new Error('SSH relay extraction input SHA-256 disagrees with the signed manifest')
+ }
+
+ const outputParent = dirname(absoluteOutput)
+ await mkdir(outputParent, { recursive: true, mode: 0o700 })
+ const physicalParent = await realpath(outputParent)
+ const physicalOutput = resolve(physicalParent, basename(absoluteOutput))
+ let outputCreated = false
+ try {
+ // Why: no cache publisher or concurrent extractor may observe a shared partial runtime tree.
+ await mkdir(physicalOutput, { mode: 0o700 })
+ outputCreated = true
+ if (artifact.tuple.os === 'win32') {
+ await inspectSshRelayZip({
+ archivePath: absoluteArchive,
+ tuple: artifact.tuple,
+ signal: effectiveSignal
+ })
+ await extractSshRelayZip({
+ archivePath: absoluteArchive,
+ outputDirectory: physicalOutput,
+ tuple: artifact.tuple,
+ signal: effectiveSignal
+ })
+ } else {
+ await inspectSshRelayTarBrotli({
+ archivePath: absoluteArchive,
+ tuple: artifact.tuple,
+ signal: effectiveSignal,
+ chunkBytes: CHUNK_BYTES
+ })
+ await extractSshRelayTarBrotli({
+ archivePath: absoluteArchive,
+ outputDirectory: physicalOutput,
+ tuple: artifact.tuple,
+ signal: effectiveSignal,
+ chunkBytes: CHUNK_BYTES
+ })
+ }
+ const tree = await verifySshRelayArtifactTree({
+ runtimeRoot: physicalOutput,
+ tuple: artifact.tuple,
+ signal: effectiveSignal,
+ chunkBytes: CHUNK_BYTES
+ })
+ const after = await describeArchive(absoluteArchive, artifact.archive.size, effectiveSignal)
+ if (!sameArchiveState(before.state, after.state) || before.sha256 !== after.sha256) {
+ throw new Error('SSH relay extraction input changed during extraction')
+ }
+ effectiveSignal.throwIfAborted()
+ return {
+ tupleId: artifact.tupleId,
+ contentId: artifact.contentId,
+ runtimeRoot: physicalOutput,
+ ...tree
+ }
+ } catch (error) {
+ if (outputCreated) {
+ // Why: only a fully verified tree may remain eligible for later atomic cache publication.
+ await rm(physicalOutput, { recursive: true, force: true })
+ }
+ throw error
+ }
+}
+
+export const SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS = Object.freeze({
+ timeoutMs: EXTRACTION_TIMEOUT_MS,
+ chunkBytes: CHUNK_BYTES,
+ maximumIncrementalMemoryBytes: MAXIMUM_INCREMENTAL_MEMORY_BYTES,
+ maximumWriteBufferBytes: MAXIMUM_WRITE_BUFFER_BYTES
+})
diff --git a/src/main/ssh/ssh-relay-artifact-tar-extraction.ts b/src/main/ssh/ssh-relay-artifact-tar-extraction.ts
new file mode 100644
index 00000000000..09ba5921302
--- /dev/null
+++ b/src/main/ssh/ssh-relay-artifact-tar-extraction.ts
@@ -0,0 +1,130 @@
+import { createHash } from 'node:crypto'
+import { createReadStream } from 'node:fs'
+import { pipeline } from 'node:stream/promises'
+import { createBrotliDecompress } from 'node:zlib'
+
+import { extract, Parser, ReadEntry } from 'tar'
+
+import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector'
+
+type SelectedTuple = SshRelaySelectedArtifact['tuple']
+
+function normalizedTarPath(entry: ReadEntry): string {
+ return entry.path.endsWith('/') ? entry.path.slice(0, -1) : entry.path
+}
+
+function assertTarEntry(
+ entry: ReadEntry,
+ expected: ReadonlyMap,
+ seen: Set
+): SelectedTuple['entries'][number] {
+ const path = normalizedTarPath(entry)
+ const declared = expected.get(path)
+ if (!declared || seen.has(path)) {
+ throw new Error(`SSH relay TAR has an extra or duplicate entry: ${path}`)
+ }
+ seen.add(path)
+ const expectedType = declared.type === 'file' ? 'File' : 'Directory'
+ if (
+ entry.type !== expectedType ||
+ entry.mode === undefined ||
+ (entry.mode & 0o777) !== declared.mode
+ ) {
+ throw new Error(`SSH relay TAR type or mode mismatch: ${path}`)
+ }
+ if (declared.type === 'file' && entry.size !== declared.size) {
+ throw new Error(`SSH relay TAR size mismatch: ${path}`)
+ }
+ return declared
+}
+
+async function decompressTar(
+ archivePath: string,
+ destination: NodeJS.WritableStream,
+ signal: AbortSignal,
+ chunkBytes: number
+): Promise {
+ await pipeline(
+ createReadStream(archivePath, { highWaterMark: chunkBytes }),
+ createBrotliDecompress({ chunkSize: chunkBytes }),
+ destination,
+ { signal }
+ )
+}
+
+export async function inspectSshRelayTarBrotli({
+ archivePath,
+ tuple,
+ signal,
+ chunkBytes
+}: {
+ archivePath: string
+ tuple: SelectedTuple
+ signal: AbortSignal
+ chunkBytes: number
+}): Promise {
+ const expected = new Map(tuple.entries.map((entry) => [entry.path, entry]))
+ const seen = new Set()
+ const parser = new Parser({ strict: true })
+ parser.on('entry', (entry: ReadEntry) => {
+ try {
+ const declared = assertTarEntry(entry, expected, seen)
+ if (declared.type === 'directory') {
+ entry.resume()
+ return
+ }
+ const digest = createHash('sha256')
+ entry.on('data', (chunk: Buffer) => digest.update(chunk))
+ entry.once('error', (error) => parser.abort(error))
+ entry.once('end', () => {
+ const actual = `sha256:${digest.digest('hex')}`
+ if (actual !== declared.sha256) {
+ parser.abort(new Error(`SSH relay TAR file integrity mismatch: ${entry.path}`))
+ }
+ })
+ } catch (error) {
+ parser.abort(error instanceof Error ? error : new Error(String(error)))
+ }
+ })
+ await decompressTar(archivePath, parser, signal, chunkBytes)
+ const missing = tuple.entries.find((entry) => !seen.has(entry.path))
+ if (missing) {
+ throw new Error(`SSH relay TAR is missing a declared entry: ${missing.path}`)
+ }
+}
+
+export async function extractSshRelayTarBrotli({
+ archivePath,
+ outputDirectory,
+ tuple,
+ signal,
+ chunkBytes
+}: {
+ archivePath: string
+ outputDirectory: string
+ tuple: SelectedTuple
+ signal: AbortSignal
+ chunkBytes: number
+}): Promise {
+ const expected = new Map(tuple.entries.map((entry) => [entry.path, entry]))
+ const seen = new Set()
+ const unpack = extract({
+ cwd: outputDirectory,
+ strict: true,
+ preserveOwner: false,
+ noChmod: false,
+ unlink: false,
+ filter: (_path, entry) => {
+ if (!(entry instanceof ReadEntry)) {
+ throw new Error('SSH relay TAR extraction requires a streamed archive entry')
+ }
+ assertTarEntry(entry, expected, seen)
+ return true
+ }
+ })
+ await decompressTar(archivePath, unpack, signal, chunkBytes)
+ const missing = tuple.entries.find((entry) => !seen.has(entry.path))
+ if (missing) {
+ throw new Error(`SSH relay TAR is missing a declared entry: ${missing.path}`)
+ }
+}
diff --git a/src/main/ssh/ssh-relay-artifact-tree-verification.ts b/src/main/ssh/ssh-relay-artifact-tree-verification.ts
new file mode 100644
index 00000000000..9c8c84d24f4
--- /dev/null
+++ b/src/main/ssh/ssh-relay-artifact-tree-verification.ts
@@ -0,0 +1,103 @@
+import { createHash } from 'node:crypto'
+import { createReadStream } from 'node:fs'
+import { lstat, readdir } from 'node:fs/promises'
+import { join, relative, sep } from 'node:path'
+
+import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector'
+
+export type SshRelayArtifactTreeVerification = {
+ files: number
+ expandedBytes: number
+}
+
+async function hashFile(
+ path: string,
+ expectedSize: number,
+ signal: AbortSignal,
+ chunkBytes: number
+): Promise {
+ const digest = createHash('sha256')
+ let size = 0
+ for await (const chunk of createReadStream(path, { highWaterMark: chunkBytes, signal })) {
+ size += chunk.length
+ if (size > expectedSize) {
+ throw new Error('SSH relay extracted tree file exceeds its signed size')
+ }
+ digest.update(chunk)
+ }
+ if (size !== expectedSize) {
+ throw new Error('SSH relay extracted tree file size disagrees with its signed size')
+ }
+ return `sha256:${digest.digest('hex')}`
+}
+
+export async function verifySshRelayArtifactTree({
+ runtimeRoot,
+ tuple,
+ signal,
+ chunkBytes
+}: {
+ runtimeRoot: string
+ tuple: SshRelaySelectedArtifact['tuple']
+ signal: AbortSignal
+ chunkBytes: number
+}): Promise {
+ const expected = new Map(tuple.entries.map((entry) => [entry.path, entry]))
+ const foldedPaths = new Set()
+ let files = 0
+ let expandedBytes = 0
+
+ async function visit(directory: string): Promise {
+ signal.throwIfAborted()
+ const children = await readdir(directory, { withFileTypes: true })
+ for (const child of children) {
+ signal.throwIfAborted()
+ const absolutePath = join(directory, child.name)
+ const path = relative(runtimeRoot, absolutePath).split(sep).join('/')
+ const folded = path.toLowerCase()
+ if (foldedPaths.has(folded)) {
+ throw new Error(`SSH relay extracted tree has a case-fold collision: ${path}`)
+ }
+ foldedPaths.add(folded)
+ const declared = expected.get(path)
+ if (!declared) {
+ throw new Error(`SSH relay extracted tree has an undeclared entry: ${path}`)
+ }
+ expected.delete(path)
+ const metadata = await lstat(absolutePath)
+ const actualType = metadata.isDirectory()
+ ? 'directory'
+ : metadata.isFile()
+ ? 'file'
+ : 'unsupported'
+ if (actualType !== declared.type) {
+ throw new Error(`SSH relay extracted tree type mismatch: ${path}`)
+ }
+ if (process.platform !== 'win32' && (metadata.mode & 0o777) !== declared.mode) {
+ throw new Error(`SSH relay extracted tree mode mismatch: ${path}`)
+ }
+ if (declared.type === 'directory') {
+ await visit(absolutePath)
+ continue
+ }
+ if (metadata.size !== declared.size) {
+ throw new Error(`SSH relay extracted tree size mismatch: ${path}`)
+ }
+ if ((await hashFile(absolutePath, declared.size, signal, chunkBytes)) !== declared.sha256) {
+ throw new Error(`SSH relay extracted tree integrity mismatch: ${path}`)
+ }
+ files += 1
+ expandedBytes += declared.size
+ }
+ }
+
+ await visit(runtimeRoot)
+ const missing = expected.keys().next().value as string | undefined
+ if (missing) {
+ throw new Error(`SSH relay extracted tree is missing a declared entry: ${missing}`)
+ }
+ if (files !== tuple.archive.fileCount || expandedBytes !== tuple.archive.expandedSize) {
+ throw new Error('SSH relay extracted tree aggregate size or file count is inconsistent')
+ }
+ return { files, expandedBytes }
+}
diff --git a/src/main/ssh/ssh-relay-artifact-zip-extraction.ts b/src/main/ssh/ssh-relay-artifact-zip-extraction.ts
new file mode 100644
index 00000000000..f98f347584c
--- /dev/null
+++ b/src/main/ssh/ssh-relay-artifact-zip-extraction.ts
@@ -0,0 +1,103 @@
+import { chmod, mkdir } from 'node:fs/promises'
+import { join } from 'node:path'
+
+import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector'
+import { type SshRelayZipLimits, visitSshRelayZip } from './ssh-relay-artifact-zip-reader'
+
+type SelectedTuple = SshRelaySelectedArtifact['tuple']
+
+function zipLimits(tuple: SelectedTuple): SshRelayZipLimits {
+ return {
+ maximumArchiveBytes: tuple.archive.size,
+ maximumEntries: tuple.entries.length,
+ maximumExpandedBytes: tuple.archive.expandedSize,
+ maximumFileBytes: Math.max(
+ ...tuple.entries.map((entry) => (entry.type === 'file' ? entry.size : 0))
+ ),
+ maximumDepth: 32,
+ maximumPathBytes: 240
+ }
+}
+
+async function visitExpectedZip({
+ archivePath,
+ tuple,
+ outputDirectory,
+ signal
+}: {
+ archivePath: string
+ tuple: SelectedTuple
+ outputDirectory?: string
+ signal: AbortSignal
+}): Promise {
+ const expected = new Map(tuple.entries.map((entry) => [entry.path, entry]))
+ const seen = new Set()
+ const result = await visitSshRelayZip(
+ archivePath,
+ zipLimits(tuple),
+ async (actual, consume) => {
+ const declared = expected.get(actual.path)
+ if (!declared || seen.has(actual.path)) {
+ throw new Error(`SSH relay ZIP has an extra or duplicate entry: ${actual.path}`)
+ }
+ seen.add(actual.path)
+ if (
+ actual.type !== declared.type ||
+ actual.unixMode === null ||
+ (actual.unixMode & 0o777) !== declared.mode
+ ) {
+ throw new Error(`SSH relay ZIP type or mode mismatch: ${actual.path}`)
+ }
+ if (declared.type === 'directory') {
+ if (outputDirectory) {
+ const outputPath = join(outputDirectory, ...actual.path.split('/'))
+ await mkdir(outputPath, { recursive: true, mode: declared.mode })
+ if (process.platform !== 'win32') {
+ await chmod(outputPath, declared.mode)
+ }
+ }
+ return
+ }
+ if (actual.size !== declared.size) {
+ throw new Error(`SSH relay ZIP size mismatch: ${actual.path}`)
+ }
+ const outputPath = outputDirectory
+ ? join(outputDirectory, ...actual.path.split('/'))
+ : undefined
+ if ((await consume({ outputPath, mode: declared.mode })) !== declared.sha256) {
+ throw new Error(`SSH relay ZIP file integrity mismatch: ${actual.path}`)
+ }
+ if (outputPath && process.platform !== 'win32') {
+ await chmod(outputPath, declared.mode)
+ }
+ },
+ signal
+ )
+ const missing = tuple.entries.find((entry) => !seen.has(entry.path))
+ if (missing) {
+ throw new Error(`SSH relay ZIP is missing a declared entry: ${missing.path}`)
+ }
+ if (
+ result.files !== tuple.archive.fileCount ||
+ result.expandedBytes !== tuple.archive.expandedSize
+ ) {
+ throw new Error('SSH relay ZIP aggregate size or file count is inconsistent')
+ }
+}
+
+export async function inspectSshRelayZip(options: {
+ archivePath: string
+ tuple: SelectedTuple
+ signal: AbortSignal
+}): Promise {
+ await visitExpectedZip(options)
+}
+
+export async function extractSshRelayZip(options: {
+ archivePath: string
+ outputDirectory: string
+ tuple: SelectedTuple
+ signal: AbortSignal
+}): Promise {
+ await visitExpectedZip(options)
+}
diff --git a/src/main/ssh/ssh-relay-artifact-zip-reader.ts b/src/main/ssh/ssh-relay-artifact-zip-reader.ts
new file mode 100644
index 00000000000..f0391fd60bc
--- /dev/null
+++ b/src/main/ssh/ssh-relay-artifact-zip-reader.ts
@@ -0,0 +1,285 @@
+import { createHash } from 'node:crypto'
+import type { EventEmitter } from 'node:events'
+import { createWriteStream } from 'node:fs'
+import { mkdir, stat } from 'node:fs/promises'
+import { createRequire } from 'node:module'
+import { dirname } from 'node:path'
+import { type Readable, Transform, Writable } from 'node:stream'
+import { pipeline } from 'node:stream/promises'
+import { crc32 } from 'node:zlib'
+
+type ZipEntry = {
+ fileName: Buffer
+ versionMadeBy: number
+ externalFileAttributes: number
+ generalPurposeBitFlag: number
+ compressionMethod: number
+ uncompressedSize: number
+ compressedSize: number
+ crc32: number
+}
+
+type ZipFile = EventEmitter & {
+ readEntry(): void
+ close(): void
+ openReadStream(entry: ZipEntry, callback: (error: Error | null, stream?: Readable) => void): void
+}
+
+type YauzlModule = {
+ open(
+ path: string,
+ options: {
+ autoClose: boolean
+ lazyEntries: boolean
+ decodeStrings: boolean
+ strictFileNames: boolean
+ validateEntrySizes: boolean
+ },
+ callback: (error: Error | null, zip?: ZipFile) => void
+ ): void
+}
+
+export type SshRelayZipLimits = {
+ maximumArchiveBytes: number
+ maximumEntries: number
+ maximumExpandedBytes: number
+ maximumFileBytes: number
+ maximumDepth: number
+ maximumPathBytes: number
+}
+
+export type SshRelayZipEntryInfo = {
+ path: string
+ type: 'directory' | 'file'
+ size: number
+ crc32: number
+ unixMode: number | null
+}
+
+const yauzl = createRequire(import.meta.url)('yauzl') as YauzlModule
+const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true })
+const ENCRYPTION_FLAGS = 0x0001 | 0x0040 | 0x2000
+const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i
+
+function openZip(path: string): Promise {
+ return new Promise((resolve, reject) => {
+ yauzl.open(
+ path,
+ {
+ autoClose: false,
+ lazyEntries: true,
+ decodeStrings: false,
+ strictFileNames: true,
+ validateEntrySizes: true
+ },
+ (error, zip) =>
+ error ? reject(error) : zip ? resolve(zip) : reject(new Error('ZIP missing'))
+ )
+ })
+}
+
+function nextEntry(zip: ZipFile): Promise {
+ return new Promise((resolve, reject) => {
+ const cleanup = (): void => {
+ zip.off('entry', onEntry)
+ zip.off('end', onEnd)
+ zip.off('error', onError)
+ }
+ const onEntry = (entry: ZipEntry): void => {
+ cleanup()
+ resolve(entry)
+ }
+ const onEnd = (): void => {
+ cleanup()
+ resolve(null)
+ }
+ const onError = (error: Error): void => {
+ cleanup()
+ reject(error)
+ }
+ zip.once('entry', onEntry)
+ zip.once('end', onEnd)
+ zip.once('error', onError)
+ zip.readEntry()
+ })
+}
+
+function openEntryStream(zip: ZipFile, entry: ZipEntry): Promise {
+ return new Promise((resolve, reject) => {
+ zip.openReadStream(entry, (error, stream) =>
+ error ? reject(error) : stream ? resolve(stream) : reject(new Error('ZIP stream missing'))
+ )
+ })
+}
+
+function decodeEntryName(entry: ZipEntry): string {
+ try {
+ return UTF8_DECODER.decode(entry.fileName)
+ } catch {
+ throw new Error('SSH relay ZIP entry name must be valid UTF-8')
+ }
+}
+
+function portableEntryPath(name: string, limits: SshRelayZipLimits): string {
+ if (
+ !name ||
+ Buffer.byteLength(name) > limits.maximumPathBytes ||
+ name.startsWith('/') ||
+ name.startsWith('\\') ||
+ /^[A-Za-z]:/.test(name)
+ ) {
+ throw new Error('SSH relay ZIP entry path is not a bounded portable relative path')
+ }
+ for (const character of name) {
+ const code = character.charCodeAt(0)
+ if (code <= 0x1f || code === 0x7f || character === '\\') {
+ throw new Error('SSH relay ZIP entry path contains a control character or backslash')
+ }
+ }
+ const path = name.endsWith('/') ? name.slice(0, -1) : name
+ const segments = path.split('/')
+ if (
+ segments.length > limits.maximumDepth ||
+ segments.some(
+ (segment) =>
+ !segment ||
+ segment === '.' ||
+ segment === '..' ||
+ segment.includes(':') ||
+ segment.endsWith('.') ||
+ segment.endsWith(' ') ||
+ WINDOWS_DEVICE_NAME.test(segment)
+ )
+ ) {
+ throw new Error('SSH relay ZIP entry path contains an unsafe path segment')
+ }
+ return path
+}
+
+function entryInfo(entry: ZipEntry, path: string): SshRelayZipEntryInfo {
+ const hostSystem = entry.versionMadeBy >>> 8
+ const unixMode = hostSystem === 3 ? (entry.externalFileAttributes >>> 16) & 0xffff : null
+ if (unixMode !== null && (unixMode & 0o170000) === 0o120000) {
+ throw new Error(`SSH relay ZIP contains a prohibited symbolic link: ${path}`)
+ }
+ if ((entry.generalPurposeBitFlag & ENCRYPTION_FLAGS) !== 0) {
+ throw new Error(`SSH relay ZIP contains an encrypted entry: ${path}`)
+ }
+ if (entry.compressionMethod !== 0 && entry.compressionMethod !== 8) {
+ throw new Error(`SSH relay ZIP uses an unsupported compression method: ${path}`)
+ }
+ const directory = entry.fileName.at(-1) === 0x2f
+ if (directory && (entry.uncompressedSize !== 0 || entry.compressedSize !== 0)) {
+ throw new Error(`SSH relay ZIP directory contains file data: ${path}`)
+ }
+ return {
+ path,
+ type: directory ? 'directory' : 'file',
+ size: entry.uncompressedSize,
+ crc32: entry.crc32 >>> 0,
+ unixMode
+ }
+}
+
+async function consumeEntry({
+ zip,
+ entry,
+ info,
+ outputPath,
+ mode,
+ signal
+}: {
+ zip: ZipFile
+ entry: ZipEntry
+ info: SshRelayZipEntryInfo
+ outputPath?: string
+ mode?: number
+ signal: AbortSignal
+}): Promise {
+ const input = await openEntryStream(zip, entry)
+ let bytes = 0
+ let checksum = 0
+ const digest = createHash('sha256')
+ const verifier = new Transform({
+ transform(chunk: Buffer, _encoding, callback) {
+ bytes += chunk.length
+ if (bytes > info.size) {
+ callback(new Error(`SSH relay ZIP entry expanded beyond its signed size: ${info.path}`))
+ return
+ }
+ checksum = crc32(chunk, checksum)
+ digest.update(chunk)
+ callback(null, chunk)
+ }
+ })
+ const sink = outputPath
+ ? (await mkdir(dirname(outputPath), { recursive: true }),
+ createWriteStream(outputPath, { flags: 'wx', mode: mode ?? 0o600 }))
+ : new Writable({ write: (_chunk, _encoding, callback) => callback() })
+ await pipeline(input, verifier, sink, { signal })
+ if (bytes !== info.size || checksum >>> 0 !== info.crc32) {
+ throw new Error(`SSH relay ZIP entry size or CRC-32 mismatch: ${info.path}`)
+ }
+ return `sha256:${digest.digest('hex')}`
+}
+
+export async function visitSshRelayZip(
+ archivePath: string,
+ limits: SshRelayZipLimits,
+ visitor: (
+ actual: SshRelayZipEntryInfo,
+ consume: (options?: { outputPath?: string; mode?: number }) => Promise
+ ) => Promise,
+ signal: AbortSignal
+): Promise<{ files: number; expandedBytes: number }> {
+ const metadata = await stat(archivePath)
+ if (!metadata.isFile() || metadata.size === 0 || metadata.size > limits.maximumArchiveBytes) {
+ throw new Error('SSH relay ZIP exceeds its compressed-size limit')
+ }
+ const zip = await openZip(archivePath)
+ const foldedPaths = new Set()
+ let entries = 0
+ let files = 0
+ let expandedBytes = 0
+ try {
+ for (let entry = await nextEntry(zip); entry; entry = await nextEntry(zip)) {
+ signal.throwIfAborted()
+ const path = portableEntryPath(decodeEntryName(entry), limits)
+ const folded = path.toLowerCase()
+ if (foldedPaths.has(folded)) {
+ throw new Error(`SSH relay ZIP has a duplicate or case-fold collision: ${path}`)
+ }
+ foldedPaths.add(folded)
+ const info = entryInfo(entry, path)
+ entries += 1
+ if (entries > limits.maximumEntries) {
+ throw new Error('SSH relay ZIP exceeds the entry-count limit')
+ }
+ if (info.type === 'file') {
+ files += 1
+ if (!Number.isSafeInteger(info.size) || info.size > limits.maximumFileBytes) {
+ throw new Error(`SSH relay ZIP file exceeds the per-file size limit: ${path}`)
+ }
+ expandedBytes += info.size
+ if (!Number.isSafeInteger(expandedBytes) || expandedBytes > limits.maximumExpandedBytes) {
+ throw new Error('SSH relay ZIP exceeds the expanded-size limit')
+ }
+ }
+ let consumed = info.type === 'directory'
+ const consume = async (options?: { outputPath?: string; mode?: number }): Promise => {
+ if (consumed) {
+ throw new Error(`SSH relay ZIP entry was consumed more than once: ${path}`)
+ }
+ consumed = true
+ return consumeEntry({ zip, entry, info, ...options, signal })
+ }
+ await visitor(info, consume)
+ if (!consumed) {
+ throw new Error(`SSH relay ZIP visitor did not verify file bytes: ${path}`)
+ }
+ }
+ return { files, expandedBytes }
+ } finally {
+ zip.close()
+ }
+}