diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index bab4b0d3517..7dbaa410423 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -218,6 +218,7 @@ jobs: node --check config/scripts/ssh-relay-runtime-aggregate-input.test.mjs node --check config/scripts/ssh-relay-runtime-draft-readback.mjs node --check config/scripts/ssh-relay-runtime-draft-readback.test.mjs + node --check config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs node --check config/scripts/ssh-relay-runtime-draft-upload.mjs node --check config/scripts/ssh-relay-runtime-draft-upload.test.mjs node --check config/scripts/ssh-relay-runtime-release-assets.mjs @@ -277,6 +278,7 @@ jobs: config/scripts/ssh-relay-runtime-draft-recovery.test.mjs \ config/scripts/ssh-relay-runtime-aggregate-input.test.mjs \ config/scripts/ssh-relay-runtime-draft-readback.test.mjs \ + config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs \ config/scripts/ssh-relay-runtime-draft-upload.test.mjs \ config/scripts/ssh-relay-runtime-release-assets.test.mjs \ config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs \ @@ -640,6 +642,8 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime draft read-back syntax check failed' } node --check config/scripts/ssh-relay-runtime-draft-readback.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime draft read-back test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime draft read-back materialization test syntax check failed' } node --check config/scripts/ssh-relay-runtime-draft-upload.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime draft upload syntax check failed' } node --check config/scripts/ssh-relay-runtime-draft-upload.test.mjs @@ -732,6 +736,7 @@ jobs: config/scripts/ssh-relay-runtime-draft-recovery.test.mjs ` config/scripts/ssh-relay-runtime-aggregate-input.test.mjs ` config/scripts/ssh-relay-runtime-draft-readback.test.mjs ` + config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs ` config/scripts/ssh-relay-runtime-draft-upload.test.mjs ` config/scripts/ssh-relay-runtime-release-assets.test.mjs ` config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs ` diff --git a/config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs b/config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs new file mode 100644 index 00000000000..dd1a52ca0c3 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs @@ -0,0 +1,158 @@ +import { createHash } from 'node:crypto' +import { mkdir, mkdtemp, readFile, readdir, realpath, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { materializeSshRelayRuntimeDraftReadback } from './ssh-relay-runtime-draft-readback.mjs' + +const REPO = 'stablyai/orca' +const RELEASE_ID = 42 +const TAG = 'v1.4.140-rc.1' +const TOKEN = 'secret-token' +const NAME = 'orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.xz' +const BYTES = Buffer.from('immutable release bytes') +const SECOND_NAME = 'orca-ssh-relay-runtime-manifest.json' +const SECOND_BYTES = Buffer.from('immutable manifest bytes') + +let root + +function expectedAsset(name = NAME, bytes = BYTES) { + return { + name, + sha256: `sha256:${createHash('sha256').update(bytes).digest('hex')}`, + size: bytes.length + } +} + +function release(expectedAssets) { + return { + id: RELEASE_ID, + tag_name: TAG, + draft: true, + assets: expectedAssets.map((asset, index) => ({ + id: 101 + index, + name: asset.name, + state: 'uploaded', + size: asset.size + })) + } +} + +function fetchFixture(expectedAssets, assetResponses) { + const fetchImpl = vi.fn().mockResolvedValueOnce(Response.json(release(expectedAssets))) + for (const response of assetResponses) { + fetchImpl.mockResolvedValueOnce(response) + } + return fetchImpl +} + +function input(expectedAssets, fetchImpl, overrides = {}) { + return { + repo: REPO, + releaseId: RELEASE_ID, + tag: TAG, + token: TOKEN, + expectedAssets, + outputDirectory: join(root, 'readback'), + fetchImpl, + ...overrides + } +} + +beforeEach(async () => { + root = await mkdtemp(join(tmpdir(), 'orca-relay-draft-readback-')) +}) + +afterEach(async () => { + await rm(root, { recursive: true, force: true }) + vi.restoreAllMocks() +}) + +describe('SSH relay runtime draft read-back materialization', () => { + it('streams each asset once and exposes its final name only after exact verification', async () => { + const expected = expectedAsset() + let finish + const body = new ReadableStream({ + start(controller) { + controller.enqueue(BYTES.subarray(0, 5)) + finish = () => { + controller.enqueue(BYTES.subarray(5)) + controller.close() + } + } + }) + const fetchImpl = fetchFixture([expected], [new Response(body)]) + const outputDirectory = join(root, 'readback') + const physicalOutput = join(await realpath(root), 'readback') + const materialization = materializeSshRelayRuntimeDraftReadback( + input([expected], fetchImpl, { outputDirectory }) + ) + + await vi.waitFor(async () => expect((await readdir(outputDirectory)).length).toBe(1)) + const namesDuringDownload = await readdir(outputDirectory) + expect(namesDuringDownload).not.toContain(NAME) + finish() + + await expect(materialization).resolves.toEqual({ + releaseId: RELEASE_ID, + tag: TAG, + materializedAssets: [{ ...expected, path: join(physicalOutput, NAME) }] + }) + expect(fetchImpl).toHaveBeenCalledTimes(2) + expect(await readdir(outputDirectory)).toEqual([NAME]) + expect(await readFile(join(outputDirectory, NAME))).toEqual(BYTES) + }) + + it('requires an absent output directory below an existing real parent before requesting GitHub', async () => { + const expected = expectedAsset() + const existingOutput = join(root, 'existing') + await mkdir(existingOutput) + + for (const outputDirectory of [existingOutput, join(root, 'missing-parent', 'readback')]) { + const fetchImpl = vi.fn() + await expect( + materializeSshRelayRuntimeDraftReadback(input([expected], fetchImpl, { outputDirectory })) + ).rejects.toThrow(/absent|parent|no such file/i) + expect(fetchImpl).not.toHaveBeenCalled() + } + }) + + it('removes the entire output when a later asset fails verification', async () => { + const expectedAssets = [expectedAsset(), expectedAsset(SECOND_NAME, SECOND_BYTES)] + const fetchImpl = fetchFixture(expectedAssets, [ + new Response(BYTES), + new Response(Buffer.from('changed manifest bytes')) + ]) + const outputDirectory = join(root, 'readback') + + await expect( + materializeSshRelayRuntimeDraftReadback(input(expectedAssets, fetchImpl, { outputDirectory })) + ).rejects.toThrow(/size|sha-?256/i) + await expect(readdir(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' }) + }) + + it('removes partial bytes when cancellation interrupts a streamed asset', async () => { + const expected = expectedAsset() + const controller = new AbortController() + const body = new ReadableStream({ + start(stream) { + stream.enqueue(BYTES.subarray(0, 5)) + } + }) + const outputDirectory = join(root, 'readback') + const materialization = materializeSshRelayRuntimeDraftReadback( + input([expected], fetchFixture([expected], [new Response(body)]), { + outputDirectory, + signal: controller.signal + }) + ) + + await vi.waitFor(async () => expect((await readdir(outputDirectory)).length).toBe(1)) + controller.abort(new Error('cancel materialization')) + + await expect(materialization).rejects.toThrow(/cancel materialization/i) + await expect(readdir(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' }) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-draft-readback.mjs b/config/scripts/ssh-relay-runtime-draft-readback.mjs index 47b14ed2b9d..f765f7d4239 100644 --- a/config/scripts/ssh-relay-runtime-draft-readback.mjs +++ b/config/scripts/ssh-relay-runtime-draft-readback.mjs @@ -1,4 +1,6 @@ import { createHash } from 'node:crypto' +import { lstat, mkdir, open, realpath, rename, rm } from 'node:fs/promises' +import { basename, dirname, join, resolve } from 'node:path' const API_VERSION = '2022-11-28' const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u @@ -174,7 +176,18 @@ async function fetchAsset({ repo, assetId, token, fetchImpl, signal }) { return redirected } -async function hashResponse(response, expected, signal) { +async function writeCompleteChunk(file, buffer) { + let offset = 0 + while (offset < buffer.length) { + const { bytesWritten } = await file.write(buffer, offset, buffer.length - offset, null) + if (bytesWritten <= 0) { + throw new Error('SSH relay runtime draft read-back could not persist returned bytes') + } + offset += bytesWritten + } +} + +async function hashResponse(response, expected, signal, file) { const contentLength = response.headers.get('content-length') if (contentLength !== null) { const parsed = Number(contentLength) @@ -189,14 +202,37 @@ async function hashResponse(response, expected, signal) { } const hash = createHash('sha256') let bytes = 0 - for await (const chunk of response.body) { - signal?.throwIfAborted() - const buffer = Buffer.from(chunk) - bytes += buffer.length - if (bytes > expected.size || bytes > MAX_ASSET_BYTES) { - throw new Error(`SSH relay runtime draft read-back size exceeded: ${expected.name}`) + const reader = response.body.getReader() + let bodyComplete = false + const cancelRead = () => { + void reader.cancel(signal.reason).catch(() => {}) + } + signal.addEventListener('abort', cancelRead, { once: true }) + try { + while (true) { + signal.throwIfAborted() + const { done, value } = await reader.read() + signal.throwIfAborted() + if (done) { + bodyComplete = true + break + } + const buffer = Buffer.from(value) + bytes += buffer.length + if (bytes > expected.size || bytes > MAX_ASSET_BYTES) { + throw new Error(`SSH relay runtime draft read-back size exceeded: ${expected.name}`) + } + hash.update(buffer) + if (file) { + await writeCompleteChunk(file, buffer) + } } - hash.update(buffer) + } finally { + signal.removeEventListener('abort', cancelRead) + if (!bodyComplete) { + await reader.cancel(signal.aborted ? signal.reason : undefined).catch(() => {}) + } + reader.releaseLock() } if (bytes !== expected.size) { throw new Error(`SSH relay runtime draft read-back size mismatch: ${expected.name}`) @@ -207,19 +243,13 @@ async function hashResponse(response, expected, signal) { } } -export async function verifySshRelayRuntimeDraftReadback({ - repo, - releaseId, - tag, - token, - expectedAssets, - fetchImpl = fetch, - signal -}) { - const effectiveSignal = signal +function effectiveReadbackSignal(signal) { + return signal ? AbortSignal.any([signal, AbortSignal.timeout(READBACK_TIMEOUT_MS)]) : AbortSignal.timeout(READBACK_TIMEOUT_MS) - effectiveSignal.throwIfAborted() +} + +function validateReadbackInput({ repo, releaseId, tag, token, expectedAssets }) { if (typeof repo !== 'string' || !REPOSITORY_PATTERN.test(repo)) { throw new Error('SSH relay runtime draft read-back repository is invalid') } @@ -232,15 +262,99 @@ export async function verifySshRelayRuntimeDraftReadback({ if (typeof token !== 'string' || token.length === 0) { throw new Error('SSH relay runtime draft read-back token is required') } - const expected = normalizeExpectedAssets(expectedAssets) - const release = await fetchRelease({ + return normalizeExpectedAssets(expectedAssets) +} + +async function validatedReleaseAssets({ + repo, + releaseId, + tag, + token, + expected, + fetchImpl, + signal +}) { + const release = await fetchRelease({ repo, releaseId, token, fetchImpl, signal }) + return validateRelease(release, releaseId, tag, expected) +} + +async function exclusiveOutputDirectory(outputDirectory) { + if (typeof outputDirectory !== 'string' || outputDirectory.length === 0) { + throw new Error('SSH relay runtime draft read-back output directory is required') + } + const absolute = resolve(outputDirectory) + let parent + try { + parent = resolve(await realpath(dirname(absolute))) + } catch (error) { + throw new Error( + 'SSH relay runtime draft read-back output parent must be an existing real directory', + { cause: error } + ) + } + const parentMetadata = await lstat(parent) + if (!parentMetadata.isDirectory() || parentMetadata.isSymbolicLink()) { + throw new Error('SSH relay runtime draft read-back output parent must be a real directory') + } + const output = resolve(parent, basename(absolute)) + try { + await lstat(output) + } catch (error) { + if (error.code === 'ENOENT') { + return output + } + throw error + } + throw new Error('SSH relay runtime draft read-back requires an exclusive absent output directory') +} + +async function downloadToTemporaryFile({ + repo, + token, + expected, + releaseAsset, + temporaryPath, + fetchImpl, + signal +}) { + const response = await fetchAsset({ + repo, + assetId: releaseAsset.id, + token, + fetchImpl, + signal + }) + const file = await open(temporaryPath, 'wx', 0o600) + try { + await hashResponse(response, expected, signal, file) + // Why: final names must expose only bytes that survived the complete streamed verification. + await file.sync() + } finally { + await file.close() + } +} + +export async function verifySshRelayRuntimeDraftReadback({ + repo, + releaseId, + tag, + token, + expectedAssets, + fetchImpl = fetch, + signal +}) { + const effectiveSignal = effectiveReadbackSignal(signal) + effectiveSignal.throwIfAborted() + const expected = validateReadbackInput({ repo, releaseId, tag, token, expectedAssets }) + const releaseAssets = await validatedReleaseAssets({ repo, releaseId, + tag, token, + expected, fetchImpl, signal: effectiveSignal }) - const releaseAssets = validateRelease(release, releaseId, tag, expected) for (const asset of expected) { effectiveSignal.throwIfAborted() const response = await fetchAsset({ @@ -254,3 +368,63 @@ export async function verifySshRelayRuntimeDraftReadback({ } return { releaseId, tag, downloadedAssets: expected } } + +export async function materializeSshRelayRuntimeDraftReadback({ + repo, + releaseId, + tag, + token, + expectedAssets, + outputDirectory, + fetchImpl = fetch, + signal +}) { + const effectiveSignal = effectiveReadbackSignal(signal) + effectiveSignal.throwIfAborted() + const expected = validateReadbackInput({ repo, releaseId, tag, token, expectedAssets }) + const output = await exclusiveOutputDirectory(outputDirectory) + let created = false + try { + await mkdir(output, { mode: 0o700 }) + created = true + const releaseAssets = await validatedReleaseAssets({ + repo, + releaseId, + tag, + token, + expected, + fetchImpl, + signal: effectiveSignal + }) + const temporaryAssets = [] + for (const [index, asset] of expected.entries()) { + effectiveSignal.throwIfAborted() + const temporaryPath = join(output, `.readback-partial-${index}`) + await downloadToTemporaryFile({ + repo, + token, + expected: asset, + releaseAsset: releaseAssets.get(asset.name), + temporaryPath, + fetchImpl, + signal: effectiveSignal + }) + temporaryAssets.push({ asset, temporaryPath, path: join(output, asset.name) }) + } + effectiveSignal.throwIfAborted() + for (const materialized of temporaryAssets) { + await rename(materialized.temporaryPath, materialized.path) + } + return { + releaseId, + tag, + materializedAssets: temporaryAssets.map(({ asset, path }) => ({ ...asset, path })) + } + } catch (error) { + if (created) { + // Why: later archive execution may consume only a complete read-back transaction. + await rm(output, { recursive: true, force: true }) + } + throw error + } +} diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md index a5e8b0a7630..5141d7f6af0 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md @@ -16,9 +16,12 @@ in progress, 2026-07-15, Codex implementation owner.** Exact-head run target-native artifact jobs and the required-asset gate's 13-test suite on every Node 24 runner under `E-M4-RELEASE-ASSETS-CI-001`. PR Checks and Golden E2E are green; the artifact workflow is red only for the retained Windows ARM build-26200-versus-26100 floor mismatch after successful runtime smoke. -Next, add exclusive local materialization to the existing authenticated read-back without a second -download. Production keys/environment/seed, publication, desktop/default consumers, tuple enablement, -and merge to `main` remain disconnected. +Exclusive one-pass local materialization is now locally green under +`E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001`; exact-head proof on all six native Node 24 jobs is +next. Production keys/environment/seed, publication, desktop/default consumers, tuple enablement, +and merge to `main` remain disconnected. Current pushed branch head is `16edb7e75`; the +implementation owner is authorized to drive commits, pushes, CI, and PR-contained rehearsals end to +end without merging. ## Safety status @@ -271,7 +274,11 @@ and merge to `main` remain disconnected. A disconnected bounded upload/recovery implementation is locally green under `E-M4-DRAFT-UPLOAD-LOCAL-001` and passes all six native Node 24 jobs under `E-M4-DRAFT-UPLOAD-CI-001`; no real release write or downloaded archive execution has occurred, - so this item remains open. + so this item remains open. The purpose-named transactional materialization RED is recorded + under `E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001`; one-pass persistence, exclusive + temporary/final naming, cancellation/failure cleanup, and exact returned paths pass locally + under `E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001`. Exact-head all-six Node 24 proof and + downloaded archive execution are still required. - [ ] Test timeouts, retries, approval denial, signing failure, partial output, and draft recovery. ### WP4 — Desktop resolver and verified cache diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md index 93f6ae371ac..5e4c79c877d 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md @@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
Last updated: 2026-07-15
-Current phase: Milestone 4 / Work Package 3 disconnected authenticated draft read-back materialization — **In progress — 2026-07-15, Codex implementation owner**. The relay-specific required-asset capability is closed locally and on all six exact-head native Node 24 jobs under E-M4-RELEASE-ASSETS-LOCAL-RED-001, E-M4-RELEASE-ASSETS-LOCAL-001, and E-M4-RELEASE-ASSETS-CI-001. It remains a disconnected pure metadata boundary; detached-signature byte encoding, the existing desktop required-asset gate, and workflow composition are unchanged. The next slice must add an exclusive local materialization boundary to the existing authenticated read-back so exact verified bytes can later enter archive extraction/execution without a second download. Begin with a purpose-named RED; keep release writes, publication, desktop consumers, and every tuple disconnected.
-Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `5460563ff48846cbf54218860fdca1cc3c7b8614` passes the new 13-test release-asset gate and full runtime construction on all six native jobs in run 29433808457 under E-M4-RELEASE-ASSETS-CI-001. PR Checks 29433808934 and both Golden E2E jobs in 29433808372 are green. Linux supplements and the Windows x64 floor pass. Windows arm64 runtime smoke passes in 7,936.874ms with 49,553,408-byte RSS; its separate floor job then retains the expected fail-closed hosted-build 26200 versus required 26100 mismatch. There is no command-line entrypoint, release API call, upload/publication caller, desktop consumer, tuple enablement, or production/default behavior change. The user authorizes commits, pushes, draft-PR updates, CI reruns, and protected rehearsals within PRs, but not merging to `main`. No accepted production key, protected environment/seed, native-signing trust proof, release write, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
+Current phase: Milestone 4 / Work Package 3 disconnected authenticated draft read-back materialization — **In progress — 2026-07-15, Codex implementation owner**. The relay-specific required-asset capability is closed locally and on all six exact-head native Node 24 jobs under E-M4-RELEASE-ASSETS-LOCAL-RED-001, E-M4-RELEASE-ASSETS-LOCAL-001, and E-M4-RELEASE-ASSETS-CI-001. The exclusive local materialization boundary is now locally green under E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001 and E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001; exact-head all-six native Node 24 proof remains required before this slice closes. It persists the existing authenticated response stream without a second download, exposes final names only after verification, and removes the complete output on failure or cancellation. Release writes, archive execution, publication, desktop consumers, and every tuple remain disconnected.
+Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — current pushed branch head `16edb7e755d35eeec561adbfff07587f5fc0bdf2` records E-M4-RELEASE-ASSETS-CI-001; its exact code head `5460563ff48846cbf54218860fdca1cc3c7b8614` passes the new 13-test release-asset gate and full runtime construction on all six native jobs in run 29433808457. PR Checks 29433808934 and both Golden E2E jobs in 29433808372 are green. Linux supplements and the Windows x64 floor pass. Windows arm64 runtime smoke passes in 7,936.874ms with 49,553,408-byte RSS; its separate floor job then retains the expected fail-closed hosted-build 26200 versus required 26100 mismatch. The intended materialization module/test, native test wiring, and living-checklist updates are locally green and awaiting commit/push plus exact-head CI; `git diff --check` passes and the protected resolver/desktop gate files have zero diff. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no command-line entrypoint, release API write, upload/publication caller, archive execution, desktop consumer, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, release write, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -116,10 +116,12 @@ same change as the work it records. identity, and consumer disconnection pass locally and on all six exact-head jobs under E-M4-RELEASE-ASSETS-LOCAL-RED-001, E-M4-RELEASE-ASSETS-LOCAL-001, and E-M4-RELEASE-ASSETS-CI-001. Release workflow composition remains open. -- Active package: Work Package 3 disconnected authenticated draft read-back materialization. Begin - with a purpose-named RED for exclusive output, exact byte persistence during the existing bounded - authenticated download, mutation cleanup, cancellation, and consumer disconnection. Do not add - archive execution, release publication, or a production caller in this slice. +- Active package: Work Package 3 disconnected authenticated draft read-back materialization. The + purpose-named RED and local GREEN are recorded under + E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001 and + E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001. Exact-head all-six native Node 24 execution is the + next gate. Do not add archive execution, release publication, or a production caller in this + slice. - 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 @@ -11490,6 +11492,91 @@ config/scripts/verify-release-required-assets.test.mjs` — PASS, 7 files / 64 t - Follow-up: checkpoint and push this evidence, then begin the disconnected authenticated draft read-back materialization RED. Retain all production/default disconnections. +### E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001 — Transactional materialization entrypoint is absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Worktree head before implementation: `16edb7e755d35eeec561adbfff07587f5fc0bdf2`; draft PR + #8741. The working tree contains only the session-checkpoint update, purpose-named RED test, and + POSIX/Windows native-job test wiring. +- Command: + `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs` +- Result: expected FAIL in 0.862s; 1 file / 4 tests fail only because + `materializeSshRelayRuntimeDraftReadback` is not a function. The existing verification-only + entrypoint remains present and unchanged. +- Contract pinned by the RED: one existing authenticated streamed download must persist exact bytes; + partial bytes cannot use the final asset name; the output directory must be absent beneath an + existing real parent before any GitHub request; a later asset failure or cancellation must remove + the entire output; success must return exact bounded asset paths. +- Does not prove: implementation, local GREEN, Node 24/native-runner portability, real authenticated + GitHub bytes, archive execution, release write/publication, desktop consumption, native trust, + SSH transfer/install, performance, or any enabled tuple. +- Follow-up: implement the new materialization entrypoint by sharing the existing bounded download + and hash pass. Preserve the verification-only API and every production/default disconnection. + +### E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001 — One-pass verified materialization passes locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted implementation and purpose-named test atop exact pushed head + `16edb7e755d35eeec561adbfff07587f5fc0bdf2`; draft PR #8741. +- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. In-memory + authenticated release/asset response fixtures and local temporary files only. No real network, + credential, release write, signer, protected environment, SSH remote, desktop consumer, + publication, or enabled tuple. +- First implementation command: `node --check config/scripts/ssh-relay-runtime-draft-readback.mjs +&& node --check config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs && pnpm +exec vitest run --config config/vitest.config.ts +config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs +config/scripts/ssh-relay-runtime-draft-readback.test.mjs` — expected correction signal: 1 failed / 13 + passed because the test expected macOS's logical `/var` alias while the security boundary correctly + returned physical `/private/var`. The test now derives the physical parent with `realpath`; no + production path check was weakened. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs +config/scripts/ssh-relay-runtime-draft-readback.test.mjs` — PASS, 2 files / 14 tests in 739ms + Vitest and 2.16s wall; maximum RSS 132,464,640 bytes, peak footprint 96,703,936 bytes, and zero + swaps. +- Adjacent release command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 +config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs +config/scripts/ssh-relay-runtime-draft-readback.test.mjs +config/scripts/ssh-relay-runtime-draft-upload.test.mjs +config/scripts/ssh-relay-runtime-draft-recovery.test.mjs +config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs +config/scripts/ssh-relay-runtime-workflow.test.mjs +config/scripts/ssh-relay-runtime-release-assets.test.mjs +config/scripts/verify-release-required-assets.test.mjs` — PASS, 8 files / 68 tests in 2.02s + Vitest and 3.52s wall; maximum RSS 132,612,096 bytes, peak footprint 96,802,168 bytes, and zero + swaps. +- Broad release command: `/usr/bin/time -l sh -c "rg --files config/scripts | rg +'/ssh-relay.*[.]test[.]mjs$' | xargs pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1"` — PASS, 54 files / 287 tests in 10.73s Vitest and 11.98s wall; maximum RSS + 189,743,104 bytes and zero swaps. +- Static gates: `node --check` passes the production module and purpose-named test. `/usr/bin/time -l +pnpm run typecheck` passes in 2.83s wall with 1,257,357,312-byte maximum RSS and zero swaps. + `/usr/bin/time -l pnpm run lint` passes in 10.78s wall with 2,021,834,752-byte maximum RSS and + zero swaps; all 41 reliability gates and the 355-entry max-lines ratchet pass, and the existing 26 + unrelated warnings remain. Focused `oxfmt --check` and `git diff --check` pass. +- Oracle proved: the output must be absent below an existing physical parent before any GitHub + request. Each authenticated response is streamed once into an exclusive temporary file while the + existing size and SHA-256 pass runs; the file is synced and final names are published only after + every asset verifies. A later asset failure or mid-stream cancellation removes the entire output. + Success returns only the bounded validated assets with exact physical paths. The verification-only + API remains byte-for-byte compatible at its return boundary and its 10-test suite stays green. +- Consumer-disconnection oracle: repository search finds the new entrypoint only in its purpose-named + test; both native artifact job families perform syntax and test execution only. There is no CLI, + workflow caller, archive execution, release API write, publication, desktop consumer, tuple + enablement, or production/default behavior change. The protected Node/npm resolver files and + existing desktop required-asset gate retain zero diff. +- Does not prove: Node 24/native-runner behavior, real authenticated GitHub bytes, detached-signature + encoding, downloaded archive execution, real release write/publication, native signing/trust, + oldest OS floors, desktop embedding, SSH transfer/install, performance against legacy, or any + enabled tuple. +- Follow-up: commit and push the exact package, then require syntax and 4-test materialization + execution plus the full contract/runtime construction on all six exact-head native Node 24 jobs. + Keep every production/default consumer disconnected. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -11548,14 +11635,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Checkpoint and push E-M4-RELEASE-ASSETS-CI-001. Then add a purpose-named RED for exclusive local -materialization during the existing authenticated draft read-back: create only an absent output -directory, persist the exact already-streamed bytes without a second download, publish no partial -file before size/SHA-256 success, clean up on failure/cancellation, and return bounded asset paths -for the later archive-execution slice. Do not add archive execution, a workflow caller, or a real -release write in this package. Keep the existing desktop `verify-release-required-assets.mjs` -behavior and detached-signature byte encoding unchanged. The protected environment, accepted -production keys, and secret remain unprovisioned, so live signing evidence stays BLOCKED. +Commit and push E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001 and +E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001, then require exact-head syntax and focused execution +on all six native Node 24 build jobs plus the existing full artifact construction, PR Checks, and +Golden E2E. Do not add archive execution, a workflow caller, or a real release write in this +package. Keep the existing desktop `verify-release-required-assets.mjs` behavior and +detached-signature byte encoding unchanged. The protected environment, accepted production keys, +and secret remain unprovisioned, so live signing evidence stays BLOCKED. Keep Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. Do not merge to `main`; retain every production/default gate.