mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 08:03:22 +00:00
feat: define SSH relay runtime source tree
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -282,6 +282,7 @@ jobs:
|
||||
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-runtime-source-tree.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 \
|
||||
@@ -834,6 +835,7 @@ jobs:
|
||||
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-runtime-source-tree.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 `
|
||||
|
||||
@@ -140,6 +140,7 @@ describe('SSH relay runtime artifact workflow', () => {
|
||||
'ssh-relay-artifact-cache-population-integration.test.ts',
|
||||
'ssh-relay-artifact-acquisition.test.ts',
|
||||
'ssh-relay-artifact-acquisition-integration.test.ts',
|
||||
'ssh-relay-runtime-source-tree.test.ts',
|
||||
'ssh-relay-compiled-manifest-trust.test.ts',
|
||||
'ssh-relay-official-manifest.test.ts',
|
||||
'ssh-relay-manifest-accepted-keys.test.ts',
|
||||
|
||||
+20
-3
@@ -9,9 +9,26 @@ keeps commands, hashes, runner identities, timings, and failure details.
|
||||
A checked box means the work has evidence in the detailed ledger. Design approval alone does not
|
||||
complete a box.
|
||||
|
||||
Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — host-evidence
|
||||
composition CI is closed; audit only a disconnected transport-neutral verified source-tree contract
|
||||
next, 2026-07-15, Codex implementation owner.**
|
||||
Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected
|
||||
transport-neutral source-tree contract is locally green; exact-head native proof next, 2026-07-15,
|
||||
Codex implementation owner.**
|
||||
`E-M6-SOURCE-TREE-CONTRACT-AUDIT-001` fixes a pure ready-acquisition → immutable descriptor
|
||||
boundary: exact verified manifest/cache identity and signed limits, deterministic ASCII ordering,
|
||||
client-native paths, borrowed live-lease assertion, and later shared use by pre-scan plus SFTP,
|
||||
POSIX tar/no-tar, and Windows PowerShell/.NET transfer. It performs no I/O and owns no cleanup;
|
||||
filesystem enumeration, streaming, SSH, remote writes/install, product/mode/fallback/default wiring,
|
||||
publication, and SignPath remain absent.
|
||||
`E-M6-SOURCE-TREE-CONTRACT-LOCAL-RED-001` records the expected missing-module failure for ten purpose
|
||||
cases and the native-workflow oracle's six-pass/one-fail result: the suite occurs only in the oracle,
|
||||
not either runner command. `E-M6-SOURCE-TREE-CONTRACT-LOCAL-CONTENTION-001` rejects an overlapping
|
||||
local broad run whose existing suites hit 30-second timeouts under four concurrent heavy commands;
|
||||
timeouts were not relaxed. `E-M6-SOURCE-TREE-CONTRACT-LOCAL-001` is green after isolated reruns:
|
||||
10/10 purpose, 85/85 focused, 522/522 broader relay with four declared skips, 280/280 release-script,
|
||||
typecheck, full lint, format, max-lines, diff, protected-resolver, and no-product-import gates pass.
|
||||
The pure 123-line module rechecks ready acquisition identity/aggregates/root, freezes deterministic
|
||||
signed descriptors, and borrows without releasing the live cache lease. Exact-head native proof is
|
||||
next; filesystem pre-scan/streaming, SSH, remote writes/install, product/mode/fallback/default wiring,
|
||||
publication, and SignPath remain absent.
|
||||
`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,
|
||||
|
||||
+137
-12
@@ -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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: audit only the smallest disconnected transport-neutral verified source-tree contract after host-composition CI closure**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.<br>
|
||||
Session checkpoint: **HOST COMPOSITION CI CLOSED / TRANSFER CONTRACT AUDIT NEXT — 2026-07-15, Codex implementation owner** — `E-M5-HOST-EVIDENCE-COMPOSITION-CI-001` closes exact-head all-six native client proof, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 retains only the declared build-26200/26100 runner mismatch after full runtime proof. Audit only a pure verified source-tree contract next. Do not open SSH channels, transfer/install bytes, add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.<br>
|
||||
Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: exact-head native proof for the locally green disconnected transport-neutral verified source-tree contract**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.<br>
|
||||
Session checkpoint: **SOURCE-TREE CONTRACT LOCAL GREEN / EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-SOURCE-TREE-CONTRACT-LOCAL-001` passes purpose, workflow, focused, broad relay, release-script, typecheck, lint, format, max-lines, diff, protected-resolver, and no-product-caller gates after separately rerunning two contention-invalidated broad commands. Commit and push the isolated package, then require exact-head all-six native proof before checking the Milestone 6 item or advancing to pre-scan. Do not enumerate the filesystem, open SSH channels, transfer/install bytes, add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. 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)
|
||||
|
||||
@@ -150,9 +150,10 @@ same change as the work it records.
|
||||
- Completed package: disconnected full host-evidence composition is green locally and on every
|
||||
primary native client under E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001 and
|
||||
E-M5-HOST-EVIDENCE-COMPOSITION-CI-001. It has no product caller.
|
||||
- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit only Milestone
|
||||
6's smallest transport-neutral verified source-tree contract. Do not open SSH channels, transfer
|
||||
or install bytes, or add product/settings/fallback/tuple/publication/default behavior.
|
||||
- Active package: **In progress — 2026-07-15, Codex implementation owner** — exact-head native proof
|
||||
for Milestone 6's locally green pure transport-neutral verified source-tree contract. Do not
|
||||
enumerate the filesystem, open SSH channels, transfer or install bytes, or add product/settings/
|
||||
fallback/tuple/publication/default behavior.
|
||||
- Completed Work Package 2 gate: target-native Windows source-signature reports from exact-head
|
||||
artifact jobs 87267322867 and 87267322870 were independently downloaded and matched to their
|
||||
identities and signing-stage reports under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001. PR Checks
|
||||
@@ -1153,7 +1154,11 @@ Current behavior must not be assumed adequate for a much larger runtime:
|
||||
|
||||
### Shared transfer contract
|
||||
|
||||
- [ ] Define one transport-neutral source-tree contract based on the verified manifest.
|
||||
- [ ] Define one transport-neutral source-tree contract based on the verified manifest. **In
|
||||
progress — 2026-07-15, Codex implementation owner; audit complete under
|
||||
E-M6-SOURCE-TREE-CONTRACT-AUDIT-001; RED and local green proof recorded under
|
||||
E-M6-SOURCE-TREE-CONTRACT-LOCAL-RED-001 and E-M6-SOURCE-TREE-CONTRACT-LOCAL-001; exact-head
|
||||
native proof required before completion.**
|
||||
- [ ] Pre-scan and reject local source mutation, symlinks, special files, extra files, and path
|
||||
collisions before creating remote files.
|
||||
- [ ] Stream files with bounded buffers; prohibit whole-tree memory materialization.
|
||||
@@ -16080,6 +16085,126 @@ config/vitest.config.ts --maxWorkers=1` plus the composer, Windows compatibility
|
||||
`ORCA_RELAY_PATH`, tuple enablement, publication, defaults, and SignPath remain open. Legacy
|
||||
remains the sole production/default path and every tuple stays disabled.
|
||||
|
||||
### E-M6-SOURCE-TREE-CONTRACT-AUDIT-001 — disconnected authenticated source-tree projection
|
||||
|
||||
- Date/owner: 2026-07-15, Codex implementation owner.
|
||||
- Exact input boundary: accept only the `ready` result from the existing artifact acquisition. That
|
||||
result already binds a selected tuple from a signature-verified immutable manifest, a strict
|
||||
fully verified cache entry, and a live content-scoped in-use lease. Do not accept a raw manifest,
|
||||
tuple, cache path, archive, or caller-created file list.
|
||||
- Immutable projection: recheck tuple/content identity, cache aggregate counts, and the canonical
|
||||
`runtime` root before exposure. Project the authenticated entries into deeply frozen directory
|
||||
and file descriptors with client-native absolute paths. Use one explicit ASCII path order,
|
||||
independent of manifest insertion order and filesystem enumeration order. Preserve signed file
|
||||
role, exact size, SHA-256, and only the declared 0644/0755 modes.
|
||||
- Limits: inherit the already authenticated schema ceilings rather than adding a second policy:
|
||||
5,000 total entries, 250 MiB per file, 350 MiB expanded bytes, and exact signed file-count/
|
||||
expanded-byte aggregates. A mismatch fails closed before a descriptor is returned.
|
||||
- Ownership/cancellation: the projection is synchronous and performs no filesystem or network I/O,
|
||||
so it has no cancellation point and creates no resource to clean up. It exposes only a borrowed
|
||||
lease-ownership assertion; it never releases the acquisition's lease. The later orchestration
|
||||
owner must retain and release that lease only after pre-scan, the selected transfer family, and
|
||||
cleanup have all settled. The next asynchronous pre-scan/transfer package must accept one exact
|
||||
`AbortSignal`, check it throughout, and assert the borrowed lease before and after source reads.
|
||||
- Later consumers: the same contract is the sole expected-tree input for pre-scan and for bounded
|
||||
SFTP, POSIX tar/no-tar system-SSH, and Windows PowerShell/.NET system-SSH transfer. Transport code
|
||||
may choose framing/concurrency but may not reorder or weaken the authenticated descriptors.
|
||||
- Required RED/green proof: Linux and Windows signed tuples; deterministic ordering and native path
|
||||
construction; exact modes/roles/sizes/hashes/aggregates; deep freezing; ready-only input; identity,
|
||||
aggregate, and runtime-root rejection; borrowed lease assertion with zero implicit release; no
|
||||
filesystem/SSH/product import or caller; and exact inclusion in both native workflow families.
|
||||
- Deliberately absent: filesystem enumeration or mutation detection, file opening/streaming,
|
||||
channels/SFTP/child processes, remote paths/writes/install, progress, cleanup, settings/modes,
|
||||
fallback, Electron/startup, `ORCA_RELAY_PATH`, tuple enablement, publication, defaults, and
|
||||
SignPath. Legacy remains the sole production/default path and every tuple stays disabled.
|
||||
|
||||
### E-M6-SOURCE-TREE-CONTRACT-LOCAL-RED-001 — missing source-tree contract and native proof
|
||||
|
||||
- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0,
|
||||
pnpm 10.24.0, atop local evidence commit `e76c5f9e8` and the audit/test-only working diff. Exact
|
||||
Node 24 remains a later native CI gate.
|
||||
- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts
|
||||
--maxWorkers=1 src/main/ssh/ssh-relay-runtime-source-tree.test.ts` — EXPECTED FAIL, exit 1: the
|
||||
absent `ssh-relay-runtime-source-tree` module prevents collection of the ten audited cases; zero
|
||||
tests run in 600 ms Vitest / 3.20 seconds wall, 132,104,192-byte maximum RSS and 96,212,248-byte
|
||||
peak memory footprint.
|
||||
- Workflow-oracle command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts
|
||||
--maxWorkers=1 config/scripts/ssh-relay-runtime-workflow.test.mjs` — EXPECTED FAIL, exit 1: six
|
||||
workflow cases pass and the native-family inclusion oracle fails because the new purpose suite has
|
||||
one source occurrence rather than the required three; 544 ms Vitest / 3.18 seconds wall,
|
||||
131,956,736-byte maximum RSS and 96,245,280-byte peak memory footprint.
|
||||
- Scope at RED: `git diff --check` passed before the package; protected resolver files had no diff.
|
||||
No source-tree module, production import/caller, filesystem enumeration, SSH operation, transfer,
|
||||
remote write, settings/fallback, tuple, publication, or default behavior exists.
|
||||
|
||||
### E-M6-SOURCE-TREE-CONTRACT-LOCAL-CONTENTION-001 — rejected concurrent broad-gate run
|
||||
|
||||
- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64. The first broad
|
||||
relay, release-script, and full-lint commands were launched together while an unrelated local
|
||||
Vitest process was also active. Process inspection showed all four CPU/I/O-heavy commands
|
||||
overlapping for more than six minutes; these results are not accepted as implementation evidence.
|
||||
- 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` — INVALID/FAIL, 39 files passed,
|
||||
three skipped, and one existing cache-entry suite failed seven cases only at their 30-second test
|
||||
timeout; 515 tests passed and four were skipped in 449.11 seconds Vitest / 453.40 seconds wall.
|
||||
- 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` — INVALID/FAIL, 48 files
|
||||
passed and two existing suites failed nine cases at 30-second timeouts/teardown races; 271 tests
|
||||
passed with four unhandled cleanup errors in 445.50 seconds Vitest / 449.76 seconds wall.
|
||||
- Full lint nevertheless passed every command in 448.00 seconds wall. Because the two test failures
|
||||
were resource-contention artifacts outside this package, both broad test commands were rerun alone
|
||||
after process inspection confirmed no competing Vitest/lint process; only those isolated reruns
|
||||
count below. No timeout was increased and no test or production behavior was weakened.
|
||||
|
||||
### E-M6-SOURCE-TREE-CONTRACT-LOCAL-001 — pure authenticated source-tree contract is locally green
|
||||
|
||||
- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0,
|
||||
pnpm 10.24.0, atop local evidence commit `e76c5f9e8` plus this isolated package. Exact Node 24 and
|
||||
every client OS/architecture remain mandatory in native CI.
|
||||
- Implementation: the 123-line `ssh-relay-runtime-source-tree.ts` accepts only an acquisition-ready
|
||||
type exported by the existing acquisition module. It rechecks signed tuple/content identity,
|
||||
verified cache aggregates, and the exact cache `runtime` root; then projects Linux/Darwin/Windows
|
||||
manifest entries into separately ASCII-sorted, deeply frozen client-native directory/file
|
||||
descriptors. It preserves signed roles, modes, sizes, and hashes, exposes only a borrowed lease
|
||||
assertion, and never releases the owner lease. It imports only `node:path`, acquisition types, and
|
||||
runtime identity types; it performs no I/O and has no product caller.
|
||||
- Post-format purpose command: `/usr/bin/time -l pnpm exec vitest run --config
|
||||
config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-runtime-source-tree.test.ts` — PASS,
|
||||
one file / ten tests in 1.64 seconds Vitest / 6.64 seconds wall, 132,792,320-byte maximum RSS and
|
||||
96,998,776-byte peak memory footprint. It covers signed Linux/Windows tuples, reversed insertion
|
||||
order, client-native paths, exact descriptors/aggregates, deep freezing, lease borrowing, and
|
||||
ready/identity/aggregate/runtime-root rejection.
|
||||
- Workflow-oracle command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts
|
||||
--maxWorkers=1 config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, one file / seven tests in
|
||||
1.24 seconds Vitest / 6.23 seconds wall, 133,103,616-byte maximum RSS. Both native workflow command
|
||||
families now execute the purpose suite exactly once.
|
||||
- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts
|
||||
--maxWorkers=1` plus the source-tree, acquisition unit/integration, cache-entry, schema, signature,
|
||||
and workflow-oracle suites — PASS, seven files / 85 tests in 39.79 seconds Vitest. The observed
|
||||
188.64-second wrapper wall time came from the same local command scheduling period, but no focused
|
||||
test failed or timed out.
|
||||
- Isolated broader relay rerun: `/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, 40
|
||||
files passed / three declared full-size skips, 522 tests passed / four declared platform skips in
|
||||
54.54 seconds Vitest / 57.40 seconds wall, 222,511,104-byte maximum RSS. Only the established stale
|
||||
lock diagnostic and local Node 26 `module.register()` deprecation warning appear.
|
||||
- Isolated release-script rerun: `/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 35.05 seconds Vitest / 39.91 seconds wall, 190,300,160-byte maximum
|
||||
RSS.
|
||||
- Static gates: `/usr/bin/time -l pnpm typecheck` passes in 9.32 seconds wall with
|
||||
1,238,499,328-byte maximum RSS. `/usr/bin/time -l pnpm lint` passes every full lint,
|
||||
switch-exhaustiveness, 41 reliability, 355-entry max-lines, bundled-skill, 9,837 localization-
|
||||
reference, locale-parity, and zero-candidate localization gate; the 26 warnings remain only in
|
||||
untouched files. Targeted `oxlint`, targeted `oxfmt --check`, `git diff --check`, protected-
|
||||
resolver empty diff, no other `src/main` importer, and the 123/226-line implementation/test size
|
||||
checks pass. No max-lines suppression or vague module is added.
|
||||
- Boundary/residual gaps: this is a synchronous descriptor contract, not filesystem pre-scan,
|
||||
mutation detection, streaming, live SSH, SFTP/system-SSH, remote staging/install, or a full-size
|
||||
transfer. Exact-head all-six Node 24 client proof is required before accepting the item. Every
|
||||
product/startup/settings/fallback/tuple/publication/default path remains absent; legacy remains the
|
||||
sole production/default path, every tuple stays disabled, and SignPath stays deferred.
|
||||
|
||||
## Accepted Gaps
|
||||
|
||||
No product gap is accepted merely because it appears in this list. Each entry requires explicit
|
||||
@@ -16138,12 +16263,12 @@ The project is not complete until every applicable item below is checked with ev
|
||||
|
||||
## Next Required Action
|
||||
|
||||
Audit only Milestone 6's smallest disconnected transport-neutral verified source-tree contract.
|
||||
Determine the exact authenticated cache-entry/manifest inputs, immutable file ordering, mode and
|
||||
size/count limits, ownership/cancellation semantics, and which later transfer families consume it
|
||||
before adding tests or implementation. Do not open an SSH channel, transfer or install bytes, modify
|
||||
platform detection/selectors, add an Electron/startup consumer, mode wiring, fallback, tuple
|
||||
enablement, release publication, or default behavior.
|
||||
Commit and push the locally green Milestone 6 disconnected transport-neutral verified source-tree
|
||||
contract, then require exact-head all-six native client proof plus adjacent PR checks before accepting
|
||||
the item or advancing to the filesystem pre-scan package.
|
||||
Do not enumerate the filesystem, open an SSH channel, transfer or install bytes, modify platform
|
||||
detection/selectors, add an Electron/startup consumer, mode wiring, fallback, tuple enablement,
|
||||
release publication, or default behavior.
|
||||
Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets
|
||||
behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18,
|
||||
release-cut, desktop builds, publication, and every tuple separately gated.
|
||||
|
||||
@@ -22,16 +22,18 @@ const DEFAULT_OPERATIONS: SshRelayArtifactAcquisitionOperations = Object.freeze(
|
||||
populate: populateSshRelayArtifactCache
|
||||
})
|
||||
|
||||
export type SshRelayArtifactReadyAcquisition = Readonly<{
|
||||
kind: 'ready'
|
||||
source: 'cache' | 'download'
|
||||
artifact: SshRelaySelectedArtifact
|
||||
entry: Readonly<SshRelayArtifactCacheEntry>
|
||||
lease: SshRelayArtifactCacheInUseLease
|
||||
}>
|
||||
|
||||
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
|
||||
}>
|
||||
| SshRelayArtifactReadyAcquisition
|
||||
|
||||
function sameArtifactIdentity(
|
||||
expected: SshRelaySelectedArtifact,
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import nacl from 'tweetnacl'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { SshRelayArtifactReadyAcquisition } from './ssh-relay-artifact-acquisition'
|
||||
import type { SshRelayArtifactCacheEntry } from './ssh-relay-artifact-cache-entry-verification'
|
||||
import type { SshRelayArtifactCacheInUseLease } from './ssh-relay-artifact-cache-in-use-lease'
|
||||
import {
|
||||
createSshRelayArtifactTestManifest,
|
||||
createSshRelayWindowsArtifactTestManifest
|
||||
} from './ssh-relay-artifact-test-manifest'
|
||||
import { selectSshRelayArtifact } from './ssh-relay-artifact-selector'
|
||||
import {
|
||||
signSshRelayArtifactManifest,
|
||||
sshRelayManifestKeyId,
|
||||
verifySshRelayArtifactManifest
|
||||
} from './ssh-relay-manifest-signature'
|
||||
import { sshRelayRuntimeArchiveName } from './ssh-relay-release-asset'
|
||||
import { computeSshRelayRuntimeContentId, type SshRelayDigest } from './ssh-relay-runtime-identity'
|
||||
import { createSshRelayRuntimeSourceTree } from './ssh-relay-runtime-source-tree'
|
||||
|
||||
const keyPair = nacl.sign.keyPair.fromSeed(Uint8Array.from({ length: 32 }, (_, index) => index))
|
||||
const cacheRoot = join(tmpdir(), 'orca-relay-runtime-source-tree')
|
||||
|
||||
type Fixture = {
|
||||
acquisition: SshRelayArtifactReadyAcquisition
|
||||
assertOwned: ReturnType<typeof vi.fn>
|
||||
release: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
function readyFixture(
|
||||
os: 'linux' | 'win32',
|
||||
{ reverseEntries = false }: { reverseEntries?: boolean } = {}
|
||||
): Fixture {
|
||||
const manifest =
|
||||
os === 'win32'
|
||||
? createSshRelayWindowsArtifactTestManifest()
|
||||
: createSshRelayArtifactTestManifest()
|
||||
const tuple = manifest.tuples[0]
|
||||
if (reverseEntries) {
|
||||
tuple.entries.reverse()
|
||||
tuple.contentId = computeSshRelayRuntimeContentId(tuple)
|
||||
tuple.archive.name = sshRelayRuntimeArchiveName(tuple.tupleId, tuple.contentId)
|
||||
}
|
||||
manifest.signatures = [signSshRelayArtifactManifest(manifest, keyPair.secretKey)]
|
||||
const verified = verifySshRelayArtifactManifest(manifest, [
|
||||
{ keyId: sshRelayManifestKeyId(keyPair.publicKey), publicKey: keyPair.publicKey }
|
||||
])
|
||||
const host =
|
||||
os === 'win32'
|
||||
? ({
|
||||
os,
|
||||
architecture: 'x64',
|
||||
processTranslated: false,
|
||||
build: 22631,
|
||||
openSshVersion: '9.5p1',
|
||||
powerShellVersion: '5.1',
|
||||
dotNetFrameworkRelease: 528040
|
||||
} as const)
|
||||
: ({
|
||||
os,
|
||||
architecture: 'x64',
|
||||
processTranslated: false,
|
||||
kernelVersion: '6.8',
|
||||
libc: { family: 'glibc', version: '2.39' },
|
||||
libstdcxxVersion: '6.0.33',
|
||||
glibcxxVersion: '3.4.33'
|
||||
} as const)
|
||||
const artifact = selectSshRelayArtifact(verified, host)
|
||||
if (artifact.kind !== 'selected') {
|
||||
throw new Error(`Expected selected source-tree fixture, got ${artifact.reason}`)
|
||||
}
|
||||
const entryPath = join(cacheRoot, 'entries', artifact.contentId.slice('sha256:'.length))
|
||||
const entry: SshRelayArtifactCacheEntry = {
|
||||
contentId: artifact.contentId,
|
||||
tupleId: artifact.tupleId,
|
||||
entryPath,
|
||||
archivePath: join(entryPath, artifact.archive.name),
|
||||
runtimeRoot: join(entryPath, 'runtime'),
|
||||
proofPath: join(entryPath, 'proof.json'),
|
||||
files: artifact.archive.fileCount,
|
||||
expandedBytes: artifact.archive.expandedSize
|
||||
}
|
||||
const assertOwned = vi.fn(async () => {})
|
||||
const release = vi.fn(async () => {})
|
||||
const lease: SshRelayArtifactCacheInUseLease = {
|
||||
leasePath: join(cacheRoot, 'in-use', 'a'.repeat(32)),
|
||||
token: 'a'.repeat(32),
|
||||
assertOwned,
|
||||
release
|
||||
}
|
||||
return {
|
||||
acquisition: { kind: 'ready', source: 'cache', artifact, entry, lease },
|
||||
assertOwned,
|
||||
release
|
||||
}
|
||||
}
|
||||
|
||||
function asciiSorted(paths: string[]): string[] {
|
||||
return [...paths].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0))
|
||||
}
|
||||
|
||||
const inconsistentCacheEntries: [string, Partial<SshRelayArtifactCacheEntry>][] = [
|
||||
['tuple identity', { tupleId: 'win32-x64' }],
|
||||
['content identity', { contentId: `sha256:${'f'.repeat(64)}` as SshRelayDigest }],
|
||||
['file count', { files: 1 }],
|
||||
['expanded byte count', { expandedBytes: 1 }]
|
||||
]
|
||||
|
||||
describe('SSH relay runtime source-tree contract', () => {
|
||||
it.each(['linux', 'win32'] as const)(
|
||||
'projects an authenticated %s acquisition in immutable ASCII order',
|
||||
(os) => {
|
||||
const { acquisition } = readyFixture(os, { reverseEntries: true })
|
||||
|
||||
const tree = createSshRelayRuntimeSourceTree(acquisition)
|
||||
|
||||
const expectedDirectories = asciiSorted(
|
||||
acquisition.artifact.tuple.entries
|
||||
.filter((entry) => entry.type === 'directory')
|
||||
.map((entry) => entry.path)
|
||||
)
|
||||
const expectedFiles = asciiSorted(
|
||||
acquisition.artifact.tuple.entries
|
||||
.filter((entry) => entry.type === 'file')
|
||||
.map((entry) => entry.path)
|
||||
)
|
||||
expect(tree).toMatchObject({
|
||||
tupleId: acquisition.artifact.tupleId,
|
||||
contentId: acquisition.artifact.contentId,
|
||||
releaseTag: acquisition.artifact.releaseTag,
|
||||
os: acquisition.artifact.tuple.os,
|
||||
architecture: acquisition.artifact.tuple.architecture,
|
||||
runtimeRoot: acquisition.entry.runtimeRoot,
|
||||
fileCount: acquisition.entry.files,
|
||||
expandedBytes: acquisition.entry.expandedBytes
|
||||
})
|
||||
expect(tree.directories.map((entry) => entry.path)).toEqual(expectedDirectories)
|
||||
expect(tree.files.map((entry) => entry.path)).toEqual(expectedFiles)
|
||||
for (const descriptor of [...tree.directories, ...tree.files]) {
|
||||
expect(descriptor.localPath).toBe(
|
||||
join(acquisition.entry.runtimeRoot, ...descriptor.path.split('/'))
|
||||
)
|
||||
}
|
||||
expect(tree.files).toEqual(
|
||||
expectedFiles.map((path) => {
|
||||
const entry = acquisition.artifact.tuple.entries.find(
|
||||
(candidate) => candidate.path === path
|
||||
)
|
||||
if (!entry || entry.type !== 'file') {
|
||||
throw new Error(`Missing expected signed file: ${path}`)
|
||||
}
|
||||
return {
|
||||
path,
|
||||
localPath: join(acquisition.entry.runtimeRoot, ...path.split('/')),
|
||||
type: 'file',
|
||||
role: entry.role,
|
||||
size: entry.size,
|
||||
mode: entry.mode,
|
||||
sha256: entry.sha256
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it('deeply freezes descriptors without mutating signed manifest insertion order', () => {
|
||||
const { acquisition } = readyFixture('linux')
|
||||
const originalOrder = acquisition.artifact.tuple.entries.map((entry) => entry.path)
|
||||
|
||||
const tree = createSshRelayRuntimeSourceTree(acquisition)
|
||||
|
||||
expect(Object.isFrozen(tree)).toBe(true)
|
||||
expect(Object.isFrozen(tree.directories)).toBe(true)
|
||||
expect(Object.isFrozen(tree.files)).toBe(true)
|
||||
expect(tree.directories.every(Object.isFrozen)).toBe(true)
|
||||
expect(tree.files.every(Object.isFrozen)).toBe(true)
|
||||
expect(acquisition.artifact.tuple.entries.map((entry) => entry.path)).toEqual(originalOrder)
|
||||
})
|
||||
|
||||
it('borrows lease ownership without asserting or releasing it implicitly', async () => {
|
||||
const { acquisition, assertOwned, release } = readyFixture('linux')
|
||||
|
||||
const tree = createSshRelayRuntimeSourceTree(acquisition)
|
||||
|
||||
expect(assertOwned).not.toHaveBeenCalled()
|
||||
expect(release).not.toHaveBeenCalled()
|
||||
await tree.assertLeaseOwned()
|
||||
expect(assertOwned).toHaveBeenCalledTimes(1)
|
||||
expect(release).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects a non-ready acquisition', () => {
|
||||
expect(() =>
|
||||
createSshRelayRuntimeSourceTree({
|
||||
kind: 'legacy',
|
||||
reason: 'kernel-too-old'
|
||||
} as unknown as SshRelayArtifactReadyAcquisition)
|
||||
).toThrow(/ready/i)
|
||||
})
|
||||
|
||||
it.each(inconsistentCacheEntries)('rejects inconsistent cache %s', (_name, entryPatch) => {
|
||||
const { acquisition } = readyFixture('linux')
|
||||
|
||||
expect(() =>
|
||||
createSshRelayRuntimeSourceTree({
|
||||
...acquisition,
|
||||
entry: { ...acquisition.entry, ...entryPatch }
|
||||
})
|
||||
).toThrow(/identity|file count|expanded byte/i)
|
||||
})
|
||||
|
||||
it('rejects a noncanonical cache runtime root', () => {
|
||||
const { acquisition } = readyFixture('linux')
|
||||
|
||||
expect(() =>
|
||||
createSshRelayRuntimeSourceTree({
|
||||
...acquisition,
|
||||
entry: { ...acquisition.entry, runtimeRoot: join(acquisition.entry.entryPath, 'other') }
|
||||
})
|
||||
).toThrow(/runtime root/i)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,123 @@
|
||||
import { join } from 'node:path'
|
||||
|
||||
import type { SshRelayArtifactReadyAcquisition } from './ssh-relay-artifact-acquisition'
|
||||
import type {
|
||||
SshRelayDigest,
|
||||
SshRelayRuntimeFileRole,
|
||||
SshRelayRuntimeTupleId
|
||||
} from './ssh-relay-runtime-identity'
|
||||
|
||||
export type SshRelayRuntimeSourceDirectory = Readonly<{
|
||||
path: string
|
||||
localPath: string
|
||||
type: 'directory'
|
||||
mode: 0o755
|
||||
}>
|
||||
|
||||
export type SshRelayRuntimeSourceFile = Readonly<{
|
||||
path: string
|
||||
localPath: string
|
||||
type: 'file'
|
||||
role: SshRelayRuntimeFileRole
|
||||
size: number
|
||||
mode: 0o644 | 0o755
|
||||
sha256: SshRelayDigest
|
||||
}>
|
||||
|
||||
export type SshRelayRuntimeSourceTree = Readonly<{
|
||||
tupleId: SshRelayRuntimeTupleId
|
||||
contentId: SshRelayDigest
|
||||
releaseTag: string
|
||||
os: 'linux' | 'darwin' | 'win32'
|
||||
architecture: 'x64' | 'arm64'
|
||||
runtimeRoot: string
|
||||
directories: readonly SshRelayRuntimeSourceDirectory[]
|
||||
files: readonly SshRelayRuntimeSourceFile[]
|
||||
fileCount: number
|
||||
expandedBytes: number
|
||||
assertLeaseOwned: () => Promise<void>
|
||||
}>
|
||||
|
||||
function compareAscii(left: { path: string }, right: { path: string }): number {
|
||||
return left.path < right.path ? -1 : left.path > right.path ? 1 : 0
|
||||
}
|
||||
|
||||
export function createSshRelayRuntimeSourceTree(
|
||||
acquisition: SshRelayArtifactReadyAcquisition
|
||||
): SshRelayRuntimeSourceTree {
|
||||
if (!acquisition || acquisition.kind !== 'ready') {
|
||||
throw new Error('SSH relay runtime source tree requires a ready artifact acquisition')
|
||||
}
|
||||
const { artifact, entry, lease } = acquisition
|
||||
if (
|
||||
artifact.tupleId !== artifact.tuple.tupleId ||
|
||||
artifact.contentId !== artifact.tuple.contentId ||
|
||||
entry.tupleId !== artifact.tupleId ||
|
||||
entry.contentId !== artifact.contentId
|
||||
) {
|
||||
throw new Error('SSH relay runtime source tree has inconsistent artifact identity')
|
||||
}
|
||||
if (entry.runtimeRoot !== join(entry.entryPath, 'runtime')) {
|
||||
throw new Error('SSH relay runtime source tree has a noncanonical cache runtime root')
|
||||
}
|
||||
|
||||
const directories: SshRelayRuntimeSourceDirectory[] = []
|
||||
const files: SshRelayRuntimeSourceFile[] = []
|
||||
let expandedBytes = 0
|
||||
for (const manifestEntry of artifact.tuple.entries) {
|
||||
const localPath = join(entry.runtimeRoot, ...manifestEntry.path.split('/'))
|
||||
if (manifestEntry.type === 'directory') {
|
||||
directories.push(
|
||||
Object.freeze({
|
||||
path: manifestEntry.path,
|
||||
localPath,
|
||||
type: manifestEntry.type,
|
||||
mode: manifestEntry.mode
|
||||
})
|
||||
)
|
||||
continue
|
||||
}
|
||||
expandedBytes += manifestEntry.size
|
||||
files.push(
|
||||
Object.freeze({
|
||||
path: manifestEntry.path,
|
||||
localPath,
|
||||
type: manifestEntry.type,
|
||||
role: manifestEntry.role,
|
||||
size: manifestEntry.size,
|
||||
mode: manifestEntry.mode,
|
||||
sha256: manifestEntry.sha256
|
||||
})
|
||||
)
|
||||
}
|
||||
if (files.length !== artifact.archive.fileCount || entry.files !== artifact.archive.fileCount) {
|
||||
throw new Error('SSH relay runtime source tree file count disagrees with the signed artifact')
|
||||
}
|
||||
if (
|
||||
expandedBytes !== artifact.archive.expandedSize ||
|
||||
entry.expandedBytes !== artifact.archive.expandedSize
|
||||
) {
|
||||
throw new Error(
|
||||
'SSH relay runtime source tree expanded byte count disagrees with the signed artifact'
|
||||
)
|
||||
}
|
||||
|
||||
directories.sort(compareAscii)
|
||||
files.sort(compareAscii)
|
||||
// Why: transports borrow the live cache lease; only their orchestration owner may release it
|
||||
// after transfer and cleanup have both settled.
|
||||
const assertLeaseOwned = (): Promise<void> => lease.assertOwned()
|
||||
return Object.freeze({
|
||||
tupleId: artifact.tupleId,
|
||||
contentId: artifact.contentId,
|
||||
releaseTag: artifact.releaseTag,
|
||||
os: artifact.tuple.os,
|
||||
architecture: artifact.tuple.architecture,
|
||||
runtimeRoot: entry.runtimeRoot,
|
||||
directories: Object.freeze(directories),
|
||||
files: Object.freeze(files),
|
||||
fileCount: files.length,
|
||||
expandedBytes,
|
||||
assertLeaseOwned
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user