diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml
index 42549c3eca4..c2cbd60d792 100644
--- a/.github/workflows/ssh-relay-runtime-artifacts.yml
+++ b/.github/workflows/ssh-relay-runtime-artifacts.yml
@@ -273,6 +273,8 @@ jobs:
src/main/ssh/ssh-relay-artifact-cache-resolution.test.ts \
src/main/ssh/ssh-relay-artifact-cache-population.test.ts \
src/main/ssh/ssh-relay-artifact-cache-population-integration.test.ts \
+ src/main/ssh/ssh-relay-artifact-acquisition.test.ts \
+ src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts \
src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts \
src/main/ssh/ssh-relay-official-manifest.test.ts \
src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts \
@@ -816,6 +818,8 @@ jobs:
src/main/ssh/ssh-relay-artifact-cache-resolution.test.ts `
src/main/ssh/ssh-relay-artifact-cache-population.test.ts `
src/main/ssh/ssh-relay-artifact-cache-population-integration.test.ts `
+ src/main/ssh/ssh-relay-artifact-acquisition.test.ts `
+ src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts `
src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts `
src/main/ssh/ssh-relay-official-manifest.test.ts `
src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts `
diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs
index b76eb938a0e..816923aee75 100644
--- a/config/scripts/ssh-relay-runtime-workflow.test.mjs
+++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs
@@ -131,6 +131,8 @@ describe('SSH relay runtime artifact workflow', () => {
'ssh-relay-artifact-cache-resolution.test.ts',
'ssh-relay-artifact-cache-population.test.ts',
'ssh-relay-artifact-cache-population-integration.test.ts',
+ 'ssh-relay-artifact-acquisition.test.ts',
+ 'ssh-relay-artifact-acquisition-integration.test.ts',
'ssh-relay-compiled-manifest-trust.test.ts',
'ssh-relay-official-manifest.test.ts',
'ssh-relay-manifest-accepted-keys.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 da603a87bf3..b0e26f06779 100644
--- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md
+++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md
@@ -9,9 +9,9 @@ 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 resolver/cache — audit the next smallest
-disconnected acquisition composition after exact-head acceptance of cold-cache population,
-2026-07-15, Codex implementation owner.**
+Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — checkpoint and prove the
+locally green disconnected warm/cold acquisition composition on exact-head native CI, 2026-07-15,
+Codex implementation owner.**
`E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit
`22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks,
Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests,
@@ -64,6 +64,24 @@ repeated Windows x64 lock-release race is closed. The next package is an audit o
disconnected acquisition composition only. Electron/startup, live proxy/network, SSH, settings,
fallback, tuple enablement, release publication, and default behavior remain absent, and legacy
remains the sole production path.
+`E-M5-ARTIFACT-CACHE-ACQUISITION-AUDIT-001` fixes the next narrow boundary: explicit verified
+manifest/host/cache-root inputs; unavailable/compatibility short circuits; a source-qualified leased
+ready result from a warm hit without download; and exactly one accepted cold-population call on a
+verified miss. Final identity and cancellation are checked before exposure, with lease release on
+failure. A real Linux/Windows cold→warm client-offline integration must fetch exactly once. Manifest
+loading, Electron/startup, proxy policy, SSH, settings, fallback classification, tuple enablement,
+publication, and defaults remain absent.
+`E-M5-ARTIFACT-ACQUISITION-LOCAL-RED-001` records the expected two-suite missing-module failure for
+nine purpose cases plus real Linux/Windows cold→warm client-offline integration before
+implementation.
+`E-M5-ARTIFACT-ACQUISITION-LOCAL-001` is green: 11/11 purpose/integration and 18/18 focused+
+workflow-oracle tests pass; 350/350 non-full-size relay and 280/280 release-script tests plus
+typecheck, lint, format, diff, and protected-resolver gates pass. Real Linux tar/Brotli and Windows
+ZIP paths perform exactly one verified cold fetch, release the lease, then acquire warm while the
+next client fetch is configured offline. Ready results are source-qualified and leased; identity,
+cancellation, and all lower-layer errors fail closed without classification. The package remains
+disconnected from manifest loading, Electron/startup, proxy policy, SSH, settings, fallback, tuple
+enablement, publication, and defaults. Exact-head all-six native proof is next.
`E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`:
all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden
E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The
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 f8ff6434356..b08e4f468e3 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 resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the next smallest disconnected acquisition composition after exact-head acceptance of cold-cache population**. Require the eventual startup caller to supply Orca's pre-`app.setName()` canonical user-data path; keep production manifest/resources, proxy, SSH/settings/tuples/release publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
-Session checkpoint: **CI CLOSED / NEXT AUDIT — 2026-07-15, Codex implementation owner** — `E-M5-ARTIFACT-CACHE-LOCK-RELEASE-CI-001` closes correction commit `bd240049ffb9de196ce7cbd0f772639f4ba310f3` and accepts the cold-cache population package: all six primary native Node 24 jobs, both Linux oldest-userland supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. Windows arm64 retains only the declared hosted build-26200 rejection against required 26100. The next package may audit a disconnected acquisition boundary only; do not add Electron/startup, SSH, setting, fallback, tuple enablement, release publication, or default-path behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
+Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: checkpoint and prove the locally green disconnected warm/cold acquisition composition on exact-head native CI**. Require the eventual startup caller to supply Orca's pre-`app.setName()` canonical user-data path; keep production manifest/resources, proxy, SSH/settings/tuples/release publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
+Session checkpoint: **LOCAL GREEN / CI REQUIRED — 2026-07-15, Codex implementation owner** — `E-M5-ARTIFACT-ACQUISITION-LOCAL-RED-001` precedes `E-M5-ARTIFACT-ACQUISITION-LOCAL-001`: 11/11 purpose/integration, 18/18 focused+workflow-oracle, 350/350 non-full-size relay, 280/280 release-script, typecheck, lint, format, diff, and protected-resolver gates pass. Real Linux tar/Brotli and Windows ZIP cases prove one cold verified fetch followed by warm acquisition while the client fetch is offline. No Electron/startup state, fallback classification, SSH/settings, tuple enablement, release publication, or default behavior is connected. Exact-head all-six native proof 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)
@@ -147,11 +147,11 @@ same change as the work it records.
local gates, and all-six native Node 24 proof are green under
E-M5-ARTIFACT-CACHE-ROOT-LOCAL-RED-001, E-M5-ARTIFACT-CACHE-ROOT-LOCAL-001, and
E-M5-ARTIFACT-CACHE-ROOT-CI-001. No Electron or product caller exists.
-- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit the next
- smallest disconnected acquisition composition now that the cache-lock correction and cold-cache
- population are exact-head accepted. The eventual startup caller, not an SSH module, must supply
- `getCanonicalUserDataPath()` captured before `app.setName()`; keep proxy integration, SSH, mode,
- tuple, publication, fallback, and defaults separately gated.
+- Active package: **In progress — 2026-07-15, Codex implementation owner** — checkpoint and prove
+ the locally green disconnected warm/cold acquisition composition on all six native clients. The
+ eventual startup caller, not an SSH module, must supply `getCanonicalUserDataPath()` captured
+ before `app.setName()`; keep proxy integration, SSH, mode, tuple, publication, fallback, and
+ defaults separately gated.
- 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
@@ -14736,6 +14736,100 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 4 files / 21 tests
enablement, release publication, or default behavior. Legacy remains the only production path;
the next package may only audit the next disconnected acquisition composition.
+### E-M5-ARTIFACT-CACHE-ACQUISITION-AUDIT-001 — warm/cold leased acquisition boundary
+
+- Date/owner: 2026-07-15, Codex implementation owner.
+- Audit: complete reads of the accepted warm resolution and cold population implementations/tests,
+ official-manifest and cache-root boundaries, selected-artifact identity, real Linux/Windows cache
+ fixtures, and the native workflow oracle. A production-caller search confirms both accepted
+ capabilities remain disconnected outside tests.
+- Finding: the smallest next composition consumes an explicit already-verified official manifest,
+ probed host evidence, absolute cache root, and optional cancellation signal. It returns
+ unavailable/compatibility-legacy unchanged, maps a verified warm hit to a source-qualified leased
+ ready result without download, and invokes the accepted cold population exactly once only for a
+ verified miss. A successful cold result is mapped to the same leased ready shape with `download`
+ source. No cache miss escapes this boundary.
+- Safety contract: final artifact/entry tuple and content identities must match; every returned entry
+ snapshot/result is frozen; cancellation or identity failure after a warm/cold lease is acquired
+ releases that lease before rejecting. Resolution, download, integrity, publication, lease, and
+ cleanup failures propagate unchanged without availability/fallback classification. Concurrent
+ cold misses retain the accepted unique staging and content-lock publication safety; download
+ deduplication is deliberately not invented in this composition.
+- Required RED: unavailable and compatibility short circuits, warm zero-download result, exact
+ miss→population order/call identity, warm/cold post-lease cancellation release, inconsistent
+ population identity failure/release, and fail-closed resolution/population errors. A real
+ Linux/Windows integration must prove cold download followed by warm client-offline acquisition
+ with exactly one client fetch.
+- Deliberately absent: official-manifest loading, canonical user-data/Electron/startup adaptation,
+ proxy/certificate policy, eviction scheduling, SSH transfer/install/launch, `ORCA_RELAY_PATH`,
+ per-target Beta settings, fallback state machine/reason classification, tuple enablement, release
+ publication, production resources, and default behavior. The primary HTML plan is unchanged.
+
+### E-M5-ARTIFACT-ACQUISITION-LOCAL-RED-001 — warm/cold composition is absent
+
+- Date/owner: 2026-07-15, Codex implementation owner.
+- Command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1
+ src/main/ssh/ssh-relay-artifact-acquisition.test.ts
+ src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts`.
+- Result: expected FAIL, two failed suites / zero tests in 616 ms because the purpose-named
+ `ssh-relay-artifact-acquisition` module does not exist.
+- Fixed contract: nine purpose cases define unavailable/legacy zero-population short circuits,
+ frozen cache/download ready results, exact resolve→populate miss ordering, pre-abort, warm/cold
+ post-lease cancellation release, inconsistent-entry rejection/release, and unclassified error
+ propagation. Two real Linux tar/Brotli and Windows ZIP cases require a cold verified fetch followed
+ by a warm acquisition while the client fetch is configured offline, with exactly one fetch total.
+- Consumer disconnection: only the two purpose/integration suites import the absent module. No
+ Electron/startup, product/SSH, setting, fallback, tuple, publication, or default caller exists.
+
+### E-M5-ARTIFACT-ACQUISITION-LOCAL-001 — disconnected warm/cold client acquisition
+
+- Date/owner: 2026-07-15, Codex implementation owner.
+- Implementation: `ssh-relay-artifact-acquisition.ts` composes only accepted cache resolution and
+ cold population behind explicit verified-manifest, host-evidence, cache-root, and cancellation
+ inputs. Unavailable/compatibility results return frozen without population; a warm hit returns a
+ frozen `ready/cache` leased entry; only a verified miss invokes population and returns the same
+ shape as `ready/download`. Artifact release/content/archive and entry tuple/content identities are
+ checked before exposure. Cancellation or identity failure after lease acquisition releases the
+ lease; every lower-layer error propagates without fallback classification.
+- Purpose/integration command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1
+ src/main/ssh/ssh-relay-artifact-acquisition.test.ts
+ src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts` — PASS, 2 files / 11 tests in
+ 1.66 s. Nine purpose cases cover both short circuits, warm/download result shape and operation
+ order, pre-abort, warm/cold post-lease cancellation, identity failure/release, and unclassified
+ resolver/population errors. Two real Linux tar/Brotli and Windows ZIP cases pass exact signed
+ fixture bytes through the default selector, downloader, strict publication/tree proof, lease, and
+ warm lookup: cold fetch count is one, the lease is released, and a second acquisition succeeds
+ from cache while the next client fetch is configured to fail; total fetch count remains one.
+- Focused plus native workflow oracle: `/usr/bin/time -l pnpm exec vitest run --config
+ config/vitest.config.ts --maxWorkers=1
+ src/main/ssh/ssh-relay-artifact-acquisition.test.ts
+ src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts
+ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 3 files / 18 tests in 2.90 s Vitest,
+ 5.50 s wall, 132,792,320-byte maximum RSS. Both purpose suites occur exactly once in POSIX and
+ PowerShell native commands.
+- Broader relay command: `/usr/bin/time -l pnpm exec vitest run --config
+ config/vitest.config.ts --maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS,
+ 32 files / 350 tests; 3 declared full-size files/tests skipped; accepted rerun 28.77 s Vitest,
+ 30.56 s wall, 243,253,248-byte maximum RSS. An earlier completed invocation returned only partial
+ captured output from the command runner and is not counted; the exact command was rerun to an
+ observed exit code 0 and complete summary.
+- Release-script command: `/usr/bin/time -l pnpm exec vitest run --config
+ config/vitest.config.ts --maxWorkers=1 --silent=true
+ config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 22.48 s Vitest,
+ 27.42 s wall, 195,969,024-byte maximum RSS.
+- Repository gates: targeted `pnpm exec oxfmt --write` completes on six implementation/workflow
+ files in 472 ms. `pnpm typecheck` and `pnpm lint` pass; lint includes switch exhaustiveness, 41
+ reliability gates, no new max-lines bypass, bundled skill-guide verification, 9,837 localization
+ references and locale parity, and zero localization coverage candidates. `git diff --check`
+ passes and the protected `ssh-remote-node-resolution.ts`/test pair has zero package diff. Local
+ Node 26.0.0 emits only the expected Node-24 engine warning; exact Node 24 proof remains pending.
+- Boundary/gate: the fixture exposes its already-verified manifest and exact host evidence only to
+ support real acquisition tests. There is still no official-manifest loader, Electron/startup or
+ product caller, proxy policy/live credential path, eviction scheduler, SSH transfer/install,
+ `ORCA_RELAY_PATH`, Beta setting, fallback classification, tuple enablement, release publication,
+ production resource, or default-path change. Legacy remains unchanged. Require exact-head all-six
+ native and adjacent PR/E2E proof before advancing.
+
### E-M5-ARTIFACT-CACHE-POPULATION-LOCAL-001 — disconnected cold-cache population
- Date/owner: 2026-07-15, Codex implementation owner.
diff --git a/src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts b/src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts
new file mode 100644
index 00000000000..b486f09c7e8
--- /dev/null
+++ b/src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts
@@ -0,0 +1,74 @@
+import { mkdtemp, readFile, rm } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+
+import { afterEach, describe, expect, it, vi } from 'vitest'
+
+import { acquireSshRelayArtifact } from './ssh-relay-artifact-acquisition'
+import { createSshRelayArtifactCacheEntryFixture } from './ssh-relay-artifact-cache-entry-fixture'
+
+const { netFetchMock } = vi.hoisted(() => ({ netFetchMock: vi.fn() }))
+
+vi.mock('electron', () => ({ net: { fetch: netFetchMock } }))
+
+const temporaryDirectories: string[] = []
+
+afterEach(async () => {
+ netFetchMock.mockReset()
+ await Promise.all(
+ temporaryDirectories
+ .splice(0)
+ .map((directory) => rm(directory, { recursive: true, force: true }))
+ )
+})
+
+describe('SSH relay artifact warm/cold acquisition integration', () => {
+ it.each(['linux', 'win32'] as const)(
+ 'downloads the %s fixture once and then acquires it while the client is offline',
+ async (os) => {
+ const root = await mkdtemp(join(tmpdir(), 'orca-relay-artifact-acquisition-integration-'))
+ temporaryDirectories.push(root)
+ const fixture = await createSshRelayArtifactCacheEntryFixture({ root, os })
+ const archive = await readFile(fixture.archivePath)
+ netFetchMock.mockResolvedValueOnce(
+ new Response(archive, {
+ status: 200,
+ headers: { 'content-length': String(archive.length) }
+ })
+ )
+ const input = {
+ officialManifest: fixture.officialManifest,
+ host: fixture.host,
+ cacheRoot: join(root, 'cache')
+ }
+
+ const cold = await acquireSshRelayArtifact(input)
+ if (cold.kind !== 'ready') {
+ throw new Error(`Expected ready cold acquisition, got ${cold.kind}`)
+ }
+ expect(cold).toMatchObject({
+ source: 'download',
+ artifact: { contentId: fixture.artifact.contentId },
+ entry: { contentId: fixture.artifact.contentId }
+ })
+ await cold.lease.release()
+
+ netFetchMock.mockRejectedValueOnce(new Error('client is offline'))
+ const warm = await acquireSshRelayArtifact(input)
+ if (warm.kind !== 'ready') {
+ throw new Error(`Expected ready warm acquisition, got ${warm.kind}`)
+ }
+ try {
+ expect(warm).toMatchObject({
+ source: 'cache',
+ artifact: { contentId: fixture.artifact.contentId },
+ entry: { contentId: fixture.artifact.contentId }
+ })
+ await expect(warm.lease.assertOwned()).resolves.toBeUndefined()
+ } finally {
+ await warm.lease.release()
+ }
+ expect(netFetchMock).toHaveBeenCalledTimes(1)
+ }
+ )
+})
diff --git a/src/main/ssh/ssh-relay-artifact-acquisition.test.ts b/src/main/ssh/ssh-relay-artifact-acquisition.test.ts
new file mode 100644
index 00000000000..ca6615777c7
--- /dev/null
+++ b/src/main/ssh/ssh-relay-artifact-acquisition.test.ts
@@ -0,0 +1,254 @@
+import { join } from 'node:path'
+import { tmpdir } from 'node:os'
+
+import nacl from 'tweetnacl'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+import {
+ acquireSshRelayArtifact,
+ type SshRelayArtifactAcquisitionOperations
+} from './ssh-relay-artifact-acquisition'
+import { createSshRelayArtifactTestManifest } from './ssh-relay-artifact-test-manifest'
+import type { SshRelayArtifactCacheEntry } from './ssh-relay-artifact-cache-entry-verification'
+import type { SshRelayArtifactCacheInUseLease } from './ssh-relay-artifact-cache-in-use-lease'
+import {
+ selectSshRelayArtifact,
+ type SshRelaySelectedArtifact
+} from './ssh-relay-artifact-selector'
+import type { SshRelayOfficialManifest } from './ssh-relay-official-manifest'
+import {
+ signSshRelayArtifactManifest,
+ sshRelayManifestKeyId,
+ verifySshRelayArtifactManifest
+} from './ssh-relay-manifest-signature'
+import type { SshRelayDigest } from './ssh-relay-runtime-identity'
+
+const keyPair = nacl.sign.keyPair.fromSeed(Uint8Array.from({ length: 32 }, (_, index) => index))
+const cacheRoot = join(tmpdir(), 'orca-relay-artifact-acquisition')
+const host = {
+ os: 'linux' as const,
+ architecture: 'x64' as const,
+ processTranslated: false,
+ kernelVersion: '6.8.0',
+ libc: { family: 'glibc' as const, version: '2.39' },
+ libstdcxxVersion: '6.0.33',
+ glibcxxVersion: '3.4.33'
+}
+
+function officialManifest(): SshRelayOfficialManifest {
+ const manifest = createSshRelayArtifactTestManifest()
+ manifest.signatures = [signSshRelayArtifactManifest(manifest, keyPair.secretKey)]
+ return Object.freeze({
+ manifest: verifySshRelayArtifactManifest(manifest, [
+ { keyId: sshRelayManifestKeyId(keyPair.publicKey), publicKey: keyPair.publicKey }
+ ]),
+ acceptedKeysSha256: `sha256:${'a'.repeat(64)}` as SshRelayDigest
+ })
+}
+
+function artifact(official: SshRelayOfficialManifest): SshRelaySelectedArtifact {
+ const selected = selectSshRelayArtifact(official.manifest, host)
+ if (selected.kind !== 'selected') {
+ throw new Error(`Expected selected acquisition fixture, got ${selected.reason}`)
+ }
+ return selected
+}
+
+function entryFor(selected: ReturnType): SshRelayArtifactCacheEntry {
+ const entryPath = join(cacheRoot, 'entries', selected.contentId.slice('sha256:'.length))
+ return {
+ contentId: selected.contentId,
+ tupleId: selected.tupleId,
+ entryPath,
+ archivePath: join(entryPath, selected.archive.name),
+ runtimeRoot: join(entryPath, 'runtime'),
+ proofPath: join(entryPath, 'proof.json'),
+ files: selected.archive.fileCount,
+ expandedBytes: selected.archive.expandedSize
+ }
+}
+
+function lease(token = 'b'.repeat(32)): SshRelayArtifactCacheInUseLease {
+ return {
+ leasePath: join(cacheRoot, 'in-use', token),
+ token,
+ assertOwned: vi.fn(async () => {}),
+ release: vi.fn(async () => {})
+ }
+}
+
+const operations = {
+ resolve: vi.fn(),
+ populate: vi.fn()
+}
+
+beforeEach(() => {
+ operations.resolve.mockReset()
+ operations.populate.mockReset()
+})
+
+describe('SSH relay artifact warm/cold acquisition', () => {
+ it('returns unavailable without attempting cold population', async () => {
+ operations.resolve.mockResolvedValue({
+ kind: 'unavailable',
+ reason: 'official-manifest-unavailable'
+ })
+
+ const result = await acquireSshRelayArtifact(
+ { officialManifest: null, host, cacheRoot },
+ operations
+ )
+
+ expect(result).toEqual({ kind: 'unavailable', reason: 'official-manifest-unavailable' })
+ expect(Object.isFrozen(result)).toBe(true)
+ expect(operations.populate).not.toHaveBeenCalled()
+ })
+
+ it('returns compatibility legacy without attempting cold population', async () => {
+ operations.resolve.mockResolvedValue({ kind: 'legacy', reason: 'kernel-too-old' })
+
+ await expect(
+ acquireSshRelayArtifact({ officialManifest: officialManifest(), host, cacheRoot }, operations)
+ ).resolves.toEqual({ kind: 'legacy', reason: 'kernel-too-old' })
+ expect(operations.populate).not.toHaveBeenCalled()
+ })
+
+ it('maps a warm hit to a frozen cache-sourced leased result without download', async () => {
+ const official = officialManifest()
+ const selected = artifact(official)
+ const entry = entryFor(selected)
+ const acquired = lease()
+ operations.resolve.mockResolvedValue({
+ kind: 'cache-hit',
+ artifact: selected,
+ entry,
+ lease: acquired
+ })
+
+ const result = await acquireSshRelayArtifact(
+ { officialManifest: official, host, cacheRoot },
+ operations
+ )
+
+ expect(result).toEqual({
+ kind: 'ready',
+ source: 'cache',
+ artifact: selected,
+ entry,
+ lease: acquired
+ })
+ expect(Object.isFrozen(result)).toBe(true)
+ expect(result.kind === 'ready' && Object.isFrozen(result.entry)).toBe(true)
+ expect(operations.populate).not.toHaveBeenCalled()
+ })
+
+ it('populates exactly once after a verified miss and returns a download-sourced lease', async () => {
+ const official = officialManifest()
+ const selected = artifact(official)
+ const entry = entryFor(selected)
+ const acquired = lease('c'.repeat(32))
+ operations.resolve.mockResolvedValue({ kind: 'cache-miss', artifact: selected })
+ operations.populate.mockResolvedValue({ artifact: selected, entry, lease: acquired })
+
+ const result = await acquireSshRelayArtifact(
+ { officialManifest: official, host, cacheRoot },
+ operations
+ )
+
+ expect(result).toMatchObject({ kind: 'ready', source: 'download', artifact: selected, entry })
+ expect(operations.resolve).toHaveBeenCalledTimes(1)
+ expect(operations.populate).toHaveBeenCalledTimes(1)
+ expect(operations.populate).toHaveBeenCalledWith({
+ cacheRoot,
+ artifact: selected,
+ signal: undefined
+ })
+ expect(operations.resolve.mock.invocationCallOrder[0]).toBeLessThan(
+ operations.populate.mock.invocationCallOrder[0]
+ )
+ })
+
+ it('settles a pre-aborted request before resolution', async () => {
+ const controller = new AbortController()
+ controller.abort(new Error('cancel artifact acquisition'))
+
+ await expect(
+ acquireSshRelayArtifact(
+ {
+ officialManifest: officialManifest(),
+ host,
+ cacheRoot,
+ signal: controller.signal
+ },
+ operations
+ )
+ ).rejects.toThrow(/cancel artifact acquisition/i)
+ expect(operations.resolve).not.toHaveBeenCalled()
+ })
+
+ it.each(['cache', 'download'] as const)(
+ 'releases a %s lease when cancellation wins before exposure',
+ async (source) => {
+ const official = officialManifest()
+ const selected = artifact(official)
+ const entry = entryFor(selected)
+ const acquired = lease(source === 'cache' ? 'd'.repeat(32) : 'e'.repeat(32))
+ const controller = new AbortController()
+ if (source === 'cache') {
+ operations.resolve.mockImplementation(async () => {
+ controller.abort(new Error('cancel after warm lease'))
+ return { kind: 'cache-hit', artifact: selected, entry, lease: acquired }
+ })
+ } else {
+ operations.resolve.mockResolvedValue({ kind: 'cache-miss', artifact: selected })
+ operations.populate.mockImplementation(async () => {
+ controller.abort(new Error('cancel after cold lease'))
+ return { artifact: selected, entry, lease: acquired }
+ })
+ }
+
+ await expect(
+ acquireSshRelayArtifact(
+ { officialManifest: official, host, cacheRoot, signal: controller.signal },
+ operations
+ )
+ ).rejects.toThrow(source === 'cache' ? /warm lease/i : /cold lease/i)
+ expect(acquired.release).toHaveBeenCalledTimes(1)
+ }
+ )
+
+ it('rejects inconsistent cold identity and releases the acquired lease', async () => {
+ const official = officialManifest()
+ const selected = artifact(official)
+ const entry = entryFor(selected)
+ const acquired = lease('f'.repeat(32))
+ operations.resolve.mockResolvedValue({ kind: 'cache-miss', artifact: selected })
+ operations.populate.mockResolvedValue({
+ artifact: selected,
+ entry: { ...entry, tupleId: 'win32-x64' },
+ lease: acquired
+ })
+
+ await expect(
+ acquireSshRelayArtifact({ officialManifest: official, host, cacheRoot }, operations)
+ ).rejects.toThrow(/identity|tuple|content/i)
+ expect(acquired.release).toHaveBeenCalledTimes(1)
+ })
+
+ it('propagates warm resolution and cold population failures without classification', async () => {
+ operations.resolve.mockRejectedValueOnce(new Error('cache integrity failure'))
+ await expect(
+ acquireSshRelayArtifact({ officialManifest: officialManifest(), host, cacheRoot }, operations)
+ ).rejects.toThrow(/cache integrity failure/i)
+
+ const official = officialManifest()
+ operations.resolve.mockResolvedValueOnce({
+ kind: 'cache-miss',
+ artifact: artifact(official)
+ })
+ operations.populate.mockRejectedValueOnce(new Error('certificate verification failed'))
+ await expect(
+ acquireSshRelayArtifact({ officialManifest: official, host, cacheRoot }, operations)
+ ).rejects.toThrow(/certificate verification failed/i)
+ })
+})
diff --git a/src/main/ssh/ssh-relay-artifact-acquisition.ts b/src/main/ssh/ssh-relay-artifact-acquisition.ts
new file mode 100644
index 00000000000..4208c53a437
--- /dev/null
+++ b/src/main/ssh/ssh-relay-artifact-acquisition.ts
@@ -0,0 +1,151 @@
+import type { SshRelayArtifactCacheEntry } from './ssh-relay-artifact-cache-entry-verification'
+import type { SshRelayArtifactCacheInUseLease } from './ssh-relay-artifact-cache-in-use-lease'
+import {
+ populateSshRelayArtifactCache,
+ type SshRelayArtifactCachePopulation
+} from './ssh-relay-artifact-cache-population'
+import { resolveSshRelayArtifactCache } from './ssh-relay-artifact-cache-resolution'
+import type {
+ SshRelayArtifactLegacyReason,
+ SshRelayHostEvidence,
+ SshRelaySelectedArtifact
+} from './ssh-relay-artifact-selector'
+import type { SshRelayOfficialManifest } from './ssh-relay-official-manifest'
+
+export type SshRelayArtifactAcquisitionOperations = Readonly<{
+ resolve: typeof resolveSshRelayArtifactCache
+ populate: typeof populateSshRelayArtifactCache
+}>
+
+const DEFAULT_OPERATIONS: SshRelayArtifactAcquisitionOperations = Object.freeze({
+ resolve: resolveSshRelayArtifactCache,
+ populate: populateSshRelayArtifactCache
+})
+
+export type SshRelayArtifactAcquisition =
+ | Readonly<{ kind: 'unavailable'; reason: 'official-manifest-unavailable' }>
+ | Readonly<{ kind: 'legacy'; reason: SshRelayArtifactLegacyReason }>
+ | Readonly<{
+ kind: 'ready'
+ source: 'cache' | 'download'
+ artifact: SshRelaySelectedArtifact
+ entry: Readonly
+ lease: SshRelayArtifactCacheInUseLease
+ }>
+
+function sameArtifactIdentity(
+ expected: SshRelaySelectedArtifact,
+ actual: SshRelaySelectedArtifact
+): boolean {
+ return (
+ actual.tupleId === expected.tupleId &&
+ actual.contentId === expected.contentId &&
+ actual.releaseTag === expected.releaseTag &&
+ actual.archive.name === expected.archive.name &&
+ actual.archive.sha256 === expected.archive.sha256
+ )
+}
+
+function sameEntryIdentity(
+ artifact: SshRelaySelectedArtifact,
+ entry: SshRelayArtifactCacheEntry
+): boolean {
+ return entry.tupleId === artifact.tupleId && entry.contentId === artifact.contentId
+}
+
+async function readyResult({
+ source,
+ expectedArtifact,
+ actualArtifact,
+ entry,
+ lease,
+ signal
+}: {
+ source: 'cache' | 'download'
+ expectedArtifact: SshRelaySelectedArtifact
+ actualArtifact: SshRelaySelectedArtifact
+ entry: SshRelayArtifactCacheEntry
+ lease: SshRelayArtifactCacheInUseLease
+ signal?: AbortSignal
+}): Promise {
+ try {
+ signal?.throwIfAborted()
+ if (
+ !sameArtifactIdentity(expectedArtifact, actualArtifact) ||
+ !sameEntryIdentity(expectedArtifact, entry)
+ ) {
+ throw new Error('SSH relay artifact acquisition identity is inconsistent')
+ }
+ return Object.freeze({
+ kind: 'ready',
+ source,
+ artifact: expectedArtifact,
+ entry: Object.freeze({ ...entry }),
+ lease
+ })
+ } catch (error) {
+ await lease.release().catch(() => {})
+ throw error
+ }
+}
+
+function unavailableResult(): SshRelayArtifactAcquisition {
+ return Object.freeze({ kind: 'unavailable', reason: 'official-manifest-unavailable' })
+}
+
+function legacyResult(reason: SshRelayArtifactLegacyReason): SshRelayArtifactAcquisition {
+ return Object.freeze({ kind: 'legacy', reason })
+}
+
+export async function acquireSshRelayArtifact(
+ {
+ officialManifest,
+ host,
+ cacheRoot,
+ signal
+ }: {
+ officialManifest: SshRelayOfficialManifest | null
+ host: SshRelayHostEvidence
+ cacheRoot: string
+ signal?: AbortSignal
+ },
+ operations: SshRelayArtifactAcquisitionOperations = DEFAULT_OPERATIONS
+): Promise {
+ signal?.throwIfAborted()
+ const resolution = await operations.resolve({ officialManifest, host, cacheRoot, signal })
+
+ if (resolution.kind === 'unavailable') {
+ signal?.throwIfAborted()
+ return unavailableResult()
+ }
+ if (resolution.kind === 'legacy') {
+ signal?.throwIfAborted()
+ return legacyResult(resolution.reason)
+ }
+ if (resolution.kind === 'cache-hit') {
+ return readyResult({
+ source: 'cache',
+ expectedArtifact: resolution.artifact,
+ actualArtifact: resolution.artifact,
+ entry: resolution.entry,
+ lease: resolution.lease,
+ signal
+ })
+ }
+
+ signal?.throwIfAborted()
+ // Why: availability and integrity classification belongs to the later fallback state machine.
+ const populated: SshRelayArtifactCachePopulation = await operations.populate({
+ cacheRoot,
+ artifact: resolution.artifact,
+ signal
+ })
+ return readyResult({
+ source: 'download',
+ expectedArtifact: resolution.artifact,
+ actualArtifact: populated.artifact,
+ entry: populated.entry,
+ lease: populated.lease,
+ signal
+ })
+}
diff --git a/src/main/ssh/ssh-relay-artifact-cache-entry-fixture.ts b/src/main/ssh/ssh-relay-artifact-cache-entry-fixture.ts
index de2bd48b420..8a49083ab45 100644
--- a/src/main/ssh/ssh-relay-artifact-cache-entry-fixture.ts
+++ b/src/main/ssh/ssh-relay-artifact-cache-entry-fixture.ts
@@ -16,6 +16,7 @@ import {
type SshRelayHostEvidence,
type SshRelaySelectedArtifact
} from './ssh-relay-artifact-selector'
+import type { SshRelayOfficialManifest } from './ssh-relay-official-manifest'
import {
signSshRelayArtifactManifest,
sshRelayManifestKeyId,
@@ -103,6 +104,8 @@ export type SshRelayArtifactCacheEntryFixture = {
archivePath: string
artifact: SshRelaySelectedArtifact
fileBytes: ReadonlyMap
+ host: SshRelayHostEvidence
+ officialManifest: SshRelayOfficialManifest
}
export async function createSshRelayArtifactCacheEntryFixture({
@@ -172,5 +175,14 @@ export async function createSshRelayArtifactCacheEntryFixture({
}
const archivePath = join(root, tuple.archive.name)
await writeFile(archivePath, archive, { mode: 0o600 })
- return { archivePath, artifact, fileBytes }
+ return {
+ archivePath,
+ artifact,
+ fileBytes,
+ host,
+ officialManifest: Object.freeze({
+ manifest: verified,
+ acceptedKeysSha256: sha256(keyPair.publicKey)
+ })
+ }
}