mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +00:00
feat: acquire verified relay artifacts
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -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',
|
||||
|
||||
+21
-3
@@ -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
|
||||
|
||||
+101
-7
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user