feat: acquire verified relay artifacts

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-07-16 02:58:45 -04:00
co-authored by Orca
parent 6715b5153c
commit c170ff92c1
8 changed files with 620 additions and 11 deletions
@@ -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 `
@@ -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',
@@ -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
@@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps
Date created: 2026-07-14<br>
Last updated: 2026-07-15<br>
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.<br>
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.<br>
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.<br>
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.<br>
Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)<br>
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.
@@ -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)
}
)
})
@@ -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<typeof artifact>): 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<SshRelayArtifactAcquisitionOperations['resolve']>(),
populate: vi.fn<SshRelayArtifactAcquisitionOperations['populate']>()
}
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)
})
})
@@ -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<SshRelayArtifactCacheEntry>
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<SshRelayArtifactAcquisition> {
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<SshRelayArtifactAcquisition> {
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
})
}
@@ -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<string, Buffer>
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)
})
}
}