diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 2a522a034a2..09293f303d6 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -147,6 +147,10 @@ jobs: node --check config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs node --check config/scripts/ssh-relay-runtime-draft-recovery.mjs node --check config/scripts/ssh-relay-runtime-draft-recovery.test.mjs + node --check config/scripts/ssh-relay-runtime-aggregate-input.mjs + 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 pnpm exec vitest run --config config/vitest.config.ts \ config/scripts/ssh-relay-node-release-verification.test.mjs \ config/scripts/ssh-relay-node-tar-inspection.test.mjs \ @@ -170,6 +174,8 @@ jobs: config/scripts/ssh-relay-runtime-windows-source-signature-verification.test.mjs \ config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs \ 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-closure.test.mjs \ config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ @@ -493,6 +499,14 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime draft recovery syntax check failed' } node --check config/scripts/ssh-relay-runtime-draft-recovery.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime draft recovery test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-aggregate-input.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime aggregate input syntax check failed' } + node --check config/scripts/ssh-relay-runtime-aggregate-input.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime aggregate input test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-draft-readback.mjs + 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' } pnpm exec vitest run --config config/vitest.config.ts ` config/scripts/ssh-relay-node-release-verification.test.mjs ` config/scripts/ssh-relay-node-tar-inspection.test.mjs ` @@ -516,6 +530,8 @@ jobs: config/scripts/ssh-relay-runtime-windows-source-signature-verification.test.mjs ` config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs ` 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-closure.test.mjs ` config/scripts/ssh-relay-runtime-pty-smoke.test.mjs ` config/scripts/ssh-relay-runtime-reproducibility.test.mjs ` diff --git a/config/scripts/ssh-relay-runtime-aggregate-input.mjs b/config/scripts/ssh-relay-runtime-aggregate-input.mjs new file mode 100644 index 00000000000..f1410553d15 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-aggregate-input.mjs @@ -0,0 +1,150 @@ +import { createHash } from 'node:crypto' +import { createReadStream } from 'node:fs' +import { lstat, readdir } from 'node:fs/promises' +import { basename, join, resolve } from 'node:path' + +import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' + +const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u +const ASSET_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,239}$/u +const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024 +const MAX_TUPLES = 8 +const AGGREGATE_TIMEOUT_MS = 15 * 60_000 +const ASSET_FIELDS = ['tupleId', 'name', 'contentId', 'sha256', 'size'] + +function assertObject(value, label) { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`SSH relay runtime aggregate ${label} must be an object`) + } +} + +function assertExactFields(value, fields, label) { + assertObject(value, label) + const actual = Object.keys(value).sort() + const expected = [...fields].sort() + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + throw new Error(`SSH relay runtime aggregate ${label} has unexpected or missing fields`) + } +} + +function expectedArchiveName(tupleId, contentId) { + const digest = contentId.slice('sha256:'.length) + const extension = tupleId.startsWith('win32-') ? 'zip' : 'tar.xz' + return `orca-ssh-relay-runtime-v1-${tupleId}-${digest}.${extension}` +} + +function normalizeAssets(assets) { + if (!Array.isArray(assets) || assets.length === 0 || assets.length > MAX_TUPLES) { + throw new Error('SSH relay runtime aggregate assets must be a bounded non-empty array') + } + const tupleIds = new Set() + const names = new Set() + return assets.map((asset, index) => { + assertExactFields(asset, ASSET_FIELDS, `asset ${index}`) + if (!Object.hasOwn(sshRelayRuntimeCompatibility, asset.tupleId)) { + throw new Error(`SSH relay runtime aggregate has an unsupported tuple: ${asset.tupleId}`) + } + if (tupleIds.has(asset.tupleId)) { + throw new Error(`SSH relay runtime aggregate has a duplicate tuple: ${asset.tupleId}`) + } + tupleIds.add(asset.tupleId) + if (typeof asset.name !== 'string' || !ASSET_NAME_PATTERN.test(asset.name)) { + throw new Error('SSH relay runtime aggregate asset has an invalid name') + } + if (names.has(asset.name)) { + throw new Error(`SSH relay runtime aggregate has a duplicate asset: ${asset.name}`) + } + names.add(asset.name) + if (!DIGEST_PATTERN.test(asset.contentId)) { + throw new Error('SSH relay runtime aggregate asset has an invalid content identity') + } + if (!DIGEST_PATTERN.test(asset.sha256)) { + throw new Error('SSH relay runtime aggregate asset has an invalid SHA-256') + } + if (!Number.isSafeInteger(asset.size) || asset.size <= 0 || asset.size > MAX_ARCHIVE_BYTES) { + throw new Error('SSH relay runtime aggregate asset has an invalid size') + } + if (asset.name !== expectedArchiveName(asset.tupleId, asset.contentId)) { + throw new Error(`SSH relay runtime aggregate archive name is inconsistent: ${asset.name}`) + } + return { ...asset } + }) +} + +function sameFileState(before, after) { + return ( + before.dev === after.dev && + before.ino === after.ino && + before.size === after.size && + before.mtimeNs === after.mtimeNs && + before.ctimeNs === after.ctimeNs + ) +} + +async function hashArchive(path, asset, signal) { + signal?.throwIfAborted() + const before = await lstat(path, { bigint: true }) + if (!before.isFile() || before.isSymbolicLink()) { + throw new Error(`SSH relay runtime aggregate input is not a regular file: ${asset.name}`) + } + if (before.size !== BigInt(asset.size)) { + throw new Error(`SSH relay runtime aggregate input size mismatch: ${asset.name}`) + } + const hash = createHash('sha256') + let bytes = 0 + for await (const chunk of createReadStream(path, { signal })) { + signal?.throwIfAborted() + bytes += chunk.length + if (bytes > asset.size) { + throw new Error(`SSH relay runtime aggregate input exceeded its size: ${asset.name}`) + } + hash.update(chunk) + } + const after = await lstat(path, { bigint: true }) + // Why: aggregate identity must describe one stable file, not bytes swapped during hashing. + if (!sameFileState(before, after)) { + throw new Error(`SSH relay runtime aggregate input changed while hashing: ${asset.name}`) + } + if (bytes !== asset.size) { + throw new Error(`SSH relay runtime aggregate input size mismatch: ${asset.name}`) + } + const digest = `sha256:${hash.digest('hex')}` + if (digest !== asset.sha256) { + throw new Error(`SSH relay runtime aggregate input SHA-256 mismatch: ${asset.name}`) + } +} + +export async function verifySshRelayRuntimeAggregateInputs({ inputDirectory, assets, signal }) { + const effectiveSignal = signal + ? AbortSignal.any([signal, AbortSignal.timeout(AGGREGATE_TIMEOUT_MS)]) + : AbortSignal.timeout(AGGREGATE_TIMEOUT_MS) + effectiveSignal.throwIfAborted() + if (typeof inputDirectory !== 'string' || inputDirectory.length === 0) { + throw new Error('SSH relay runtime aggregate input directory is required') + } + const normalized = normalizeAssets(assets) + const root = resolve(inputDirectory) + const rootMetadata = await lstat(root) + if (!rootMetadata.isDirectory() || rootMetadata.isSymbolicLink()) { + throw new Error('SSH relay runtime aggregate input root must be a real directory') + } + const entries = await readdir(root, { withFileTypes: true }) + const actualNames = entries.map((entry) => entry.name).sort() + const expectedNames = normalized.map((asset) => asset.name).sort() + if (JSON.stringify(actualNames) !== JSON.stringify(expectedNames)) { + throw new Error('SSH relay runtime aggregate input directory has missing or unexpected files') + } + for (const entry of entries) { + if (!entry.isFile() || entry.isSymbolicLink()) { + throw new Error(`SSH relay runtime aggregate input is not a regular file: ${entry.name}`) + } + } + for (const asset of normalized) { + const path = join(root, asset.name) + if (basename(path) !== asset.name) { + throw new Error(`SSH relay runtime aggregate asset path is unsafe: ${asset.name}`) + } + await hashArchive(path, asset, effectiveSignal) + } + return normalized +} diff --git a/config/scripts/ssh-relay-runtime-aggregate-input.test.mjs b/config/scripts/ssh-relay-runtime-aggregate-input.test.mjs new file mode 100644 index 00000000000..7dded7d750d --- /dev/null +++ b/config/scripts/ssh-relay-runtime-aggregate-input.test.mjs @@ -0,0 +1,114 @@ +import { mkdtemp, rm, symlink, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { verifySshRelayRuntimeAggregateInputs } from './ssh-relay-runtime-aggregate-input.mjs' + +const temporaryDirectories = [] + +async function fixture() { + const inputDirectory = await mkdtemp(join(tmpdir(), 'orca-runtime-aggregate-')) + temporaryDirectories.push(inputDirectory) + const contentId = `sha256:${'a'.repeat(64)}` + const bytes = Buffer.from('verified runtime bytes') + const name = `orca-ssh-relay-runtime-v1-linux-x64-glibc-${contentId.slice(7)}.tar.xz` + await writeFile(join(inputDirectory, name), bytes) + return { + inputDirectory, + assets: [ + { + tupleId: 'linux-x64-glibc', + name, + contentId, + sha256: 'sha256:d4c28c942ea1505c048b33251b3afb8b7f1ce9c54d629c9e4c8923afd93d9f45', + size: bytes.length + } + ] + } +} + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })) + ) +}) + +describe('SSH relay runtime aggregate inputs', () => { + it('accepts only exact immutable runtime archive bytes', async () => { + const input = await fixture() + + await expect(verifySshRelayRuntimeAggregateInputs(input)).resolves.toEqual(input.assets) + }) + + it('rejects a hash or size mismatch', async () => { + const hash = await fixture() + hash.assets[0].sha256 = `sha256:${'f'.repeat(64)}` + await expect(verifySshRelayRuntimeAggregateInputs(hash)).rejects.toThrow(/sha-?256/i) + + const size = await fixture() + size.assets[0].size += 1 + await expect(verifySshRelayRuntimeAggregateInputs(size)).rejects.toThrow(/size/i) + }) + + it('rejects missing or extra inputs', async () => { + const missing = await fixture() + await rm(join(missing.inputDirectory, missing.assets[0].name)) + await expect(verifySshRelayRuntimeAggregateInputs(missing)).rejects.toThrow(/missing|exact/i) + + const extra = await fixture() + await writeFile(join(extra.inputDirectory, 'unexpected.bin'), 'extra') + await expect(verifySshRelayRuntimeAggregateInputs(extra)).rejects.toThrow(/unexpected|exact/i) + }) + + it.skipIf(process.platform === 'win32')('rejects a linked archive input', async () => { + const input = await fixture() + const path = join(input.inputDirectory, input.assets[0].name) + const target = join(input.inputDirectory, 'target') + await writeFile(target, 'target') + await rm(path) + await symlink(target, path) + + await expect(verifySshRelayRuntimeAggregateInputs(input)).rejects.toThrow(/unexpected|regular/i) + }) + + it('rejects duplicate tuples, names, and unsupported identities', async () => { + const duplicateTuple = await fixture() + duplicateTuple.assets.push({ ...duplicateTuple.assets[0], name: 'different.tar.xz' }) + await expect(verifySshRelayRuntimeAggregateInputs(duplicateTuple)).rejects.toThrow( + /duplicate tuple/i + ) + + const duplicateName = await fixture() + duplicateName.assets.push({ ...duplicateName.assets[0], tupleId: 'linux-arm64-glibc' }) + await expect(verifySshRelayRuntimeAggregateInputs(duplicateName)).rejects.toThrow( + /duplicate asset/i + ) + + const unsupported = await fixture() + unsupported.assets[0].tupleId = 'linux-riscv64-glibc' + await expect(verifySshRelayRuntimeAggregateInputs(unsupported)).rejects.toThrow( + /unsupported tuple/i + ) + }) + + it('derives the archive name from tuple and content identity', async () => { + const input = await fixture() + input.assets[0].name = 'latest.tar.xz' + + await expect(verifySshRelayRuntimeAggregateInputs(input)).rejects.toThrow(/archive name/i) + }) + + it('honors cancellation before reading an archive', async () => { + const input = await fixture() + const controller = new AbortController() + controller.abort(new Error('cancel aggregate')) + + await expect( + verifySshRelayRuntimeAggregateInputs({ ...input, signal: controller.signal }) + ).rejects.toThrow(/cancel aggregate/i) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-draft-readback.mjs b/config/scripts/ssh-relay-runtime-draft-readback.mjs new file mode 100644 index 00000000000..47b14ed2b9d --- /dev/null +++ b/config/scripts/ssh-relay-runtime-draft-readback.mjs @@ -0,0 +1,256 @@ +import { createHash } from 'node:crypto' + +const API_VERSION = '2022-11-28' +const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u +const TAG_PATTERN = /^v\d+\.\d+\.\d+(?:-rc\.\d+(?:\.[0-9A-Za-z]+)?)?$/u +const REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u +const ASSET_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,239}$/u +const MANAGED_ASSET_PREFIX = 'orca-ssh-relay-runtime-' +const RELEASE_ASSET_HOST = 'release-assets.githubusercontent.com' +const MAX_ASSET_BYTES = 100 * 1024 * 1024 +const MAX_TOTAL_BYTES = 1024 * 1024 * 1024 +const MAX_ASSETS = 26 +const READBACK_TIMEOUT_MS = 15 * 60_000 +const ASSET_FIELDS = ['name', 'sha256', 'size'] + +function assertObject(value, label) { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`SSH relay runtime draft read-back ${label} must be an object`) + } +} + +function assertExactFields(value, fields, label) { + assertObject(value, label) + const actual = Object.keys(value).sort() + const expected = [...fields].sort() + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + throw new Error(`SSH relay runtime draft read-back ${label} has unexpected or missing fields`) + } +} + +function normalizeExpectedAssets(assets) { + if (!Array.isArray(assets) || assets.length === 0 || assets.length > MAX_ASSETS) { + throw new Error('SSH relay runtime draft read-back assets must be a bounded non-empty array') + } + const names = new Set() + let totalBytes = 0 + const normalized = assets.map((asset, index) => { + assertExactFields(asset, ASSET_FIELDS, `expected asset ${index}`) + if ( + typeof asset.name !== 'string' || + !ASSET_NAME_PATTERN.test(asset.name) || + !asset.name.startsWith(MANAGED_ASSET_PREFIX) + ) { + throw new Error('SSH relay runtime draft read-back expected an invalid managed asset') + } + if (names.has(asset.name)) { + throw new Error(`SSH relay runtime draft read-back has a duplicate asset: ${asset.name}`) + } + names.add(asset.name) + if (typeof asset.sha256 !== 'string' || !DIGEST_PATTERN.test(asset.sha256)) { + throw new Error('SSH relay runtime draft read-back expected an invalid SHA-256') + } + if (!Number.isSafeInteger(asset.size) || asset.size <= 0 || asset.size > MAX_ASSET_BYTES) { + throw new Error('SSH relay runtime draft read-back expected an invalid size') + } + totalBytes += asset.size + return { ...asset } + }) + if (!Number.isSafeInteger(totalBytes) || totalBytes > MAX_TOTAL_BYTES) { + throw new Error('SSH relay runtime draft read-back expected assets exceed the total size limit') + } + return normalized +} + +function authenticatedHeaders(token, accept = 'application/vnd.github+json') { + return { + Accept: accept, + Authorization: `Bearer ${token}`, + 'X-GitHub-Api-Version': API_VERSION + } +} + +async function responseError(response) { + const body = await response.text().catch(() => '') + return new Error( + `SSH relay runtime GitHub request failed ${response.status} ${response.statusText}: ${body.slice(0, 300)}` + ) +} + +async function fetchRelease({ repo, releaseId, token, fetchImpl, signal }) { + const response = await fetchImpl(`https://api.github.com/repos/${repo}/releases/${releaseId}`, { + headers: authenticatedHeaders(token), + redirect: 'error', + signal + }) + if (!response.ok) { + throw await responseError(response) + } + return response.json() +} + +function validateRelease(release, releaseId, tag, expectedAssets) { + assertObject(release, 'release') + if (release.id !== releaseId || release.draft !== true) { + throw new Error('SSH relay runtime release must remain the requested draft during read-back') + } + if (release.tag_name !== tag) { + throw new Error('SSH relay runtime draft read-back tag does not match') + } + if (!Array.isArray(release.assets)) { + throw new Error('SSH relay runtime draft read-back release assets must be an array') + } + const expectedByName = new Map(expectedAssets.map((asset) => [asset.name, asset])) + const releaseByName = new Map() + for (const asset of release.assets) { + assertObject(asset, 'release asset') + if (typeof asset.name !== 'string' || releaseByName.has(asset.name)) { + throw new Error('SSH relay runtime draft read-back has a duplicate or invalid release asset') + } + releaseByName.set(asset.name, asset) + if (asset.name.startsWith(MANAGED_ASSET_PREFIX) && !expectedByName.has(asset.name)) { + throw new Error(`SSH relay runtime draft has an unexpected managed asset: ${asset.name}`) + } + } + for (const expected of expectedAssets) { + const actual = releaseByName.get(expected.name) + if (!actual) { + throw new Error(`SSH relay runtime draft is missing managed asset: ${expected.name}`) + } + if (!Number.isSafeInteger(actual.id) || actual.id <= 0) { + throw new Error(`SSH relay runtime draft asset has an invalid ID: ${expected.name}`) + } + if (actual.state !== 'uploaded') { + throw new Error(`SSH relay runtime draft asset is not uploaded: ${expected.name}`) + } + if (actual.size !== expected.size) { + throw new Error(`SSH relay runtime draft asset size disagrees: ${expected.name}`) + } + } + return releaseByName +} + +function redirectedAssetUrl(response) { + const location = response.headers.get('location') + if (!location) { + throw new Error('SSH relay runtime draft read-back redirect is missing Location') + } + const url = new URL(location) + if (url.protocol !== 'https:') { + throw new Error('SSH relay runtime draft read-back redirect must use HTTPS') + } + if (url.hostname !== RELEASE_ASSET_HOST) { + throw new Error('SSH relay runtime draft read-back redirect has an unapproved origin') + } + return url.href +} + +async function fetchAsset({ repo, assetId, token, fetchImpl, signal }) { + const response = await fetchImpl( + `https://api.github.com/repos/${repo}/releases/assets/${assetId}`, + { + headers: authenticatedHeaders(token, 'application/octet-stream'), + redirect: 'manual', + signal + } + ) + if (response.status !== 302) { + // Why: partial or otherwise unusual successful responses cannot prove the exact release asset. + if (response.status !== 200) { + throw await responseError(response) + } + return response + } + const location = redirectedAssetUrl(response) + // Why: the API request is authenticated, but its signed CDN redirect must never receive the token. + const redirected = await fetchImpl(location, { + headers: { Accept: 'application/octet-stream' }, + redirect: 'error', + signal + }) + if (redirected.status !== 200) { + throw await responseError(redirected) + } + return redirected +} + +async function hashResponse(response, expected, signal) { + const contentLength = response.headers.get('content-length') + if (contentLength !== null) { + const parsed = Number(contentLength) + if (!Number.isSafeInteger(parsed) || parsed !== expected.size) { + throw new Error( + `SSH relay runtime draft read-back Content-Length disagrees: ${expected.name}` + ) + } + } + if (!response.body) { + throw new Error(`SSH relay runtime draft read-back body is missing: ${expected.name}`) + } + 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}`) + } + hash.update(buffer) + } + if (bytes !== expected.size) { + throw new Error(`SSH relay runtime draft read-back size mismatch: ${expected.name}`) + } + const digest = `sha256:${hash.digest('hex')}` + if (digest !== expected.sha256) { + throw new Error(`SSH relay runtime draft read-back SHA-256 mismatch: ${expected.name}`) + } +} + +export async function verifySshRelayRuntimeDraftReadback({ + repo, + releaseId, + tag, + token, + expectedAssets, + fetchImpl = fetch, + signal +}) { + const effectiveSignal = signal + ? AbortSignal.any([signal, AbortSignal.timeout(READBACK_TIMEOUT_MS)]) + : AbortSignal.timeout(READBACK_TIMEOUT_MS) + effectiveSignal.throwIfAborted() + if (typeof repo !== 'string' || !REPOSITORY_PATTERN.test(repo)) { + throw new Error('SSH relay runtime draft read-back repository is invalid') + } + if (!Number.isSafeInteger(releaseId) || releaseId <= 0) { + throw new Error('SSH relay runtime draft read-back release ID is invalid') + } + if (typeof tag !== 'string' || !TAG_PATTERN.test(tag)) { + throw new Error('SSH relay runtime draft read-back tag is invalid') + } + 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({ + repo, + releaseId, + token, + fetchImpl, + signal: effectiveSignal + }) + const releaseAssets = validateRelease(release, releaseId, tag, expected) + for (const asset of expected) { + effectiveSignal.throwIfAborted() + const response = await fetchAsset({ + repo, + assetId: releaseAssets.get(asset.name).id, + token, + fetchImpl, + signal: effectiveSignal + }) + await hashResponse(response, asset, effectiveSignal) + } + return { releaseId, tag, downloadedAssets: expected } +} diff --git a/config/scripts/ssh-relay-runtime-draft-readback.test.mjs b/config/scripts/ssh-relay-runtime-draft-readback.test.mjs new file mode 100644 index 00000000000..5903acd5ef2 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-draft-readback.test.mjs @@ -0,0 +1,212 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { verifySshRelayRuntimeDraftReadback } from './ssh-relay-runtime-draft-readback.mjs' + +const TAG = 'v1.4.140-rc.1' +const REPO = 'stablyai/orca' +const TOKEN = 'secret-token' +const bytes = Buffer.from('immutable release bytes') +const expected = { + name: 'orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.xz', + sha256: 'sha256:5396515749878bd28c5dae110040b4fbae1c33f59318b9c23f446319d68e236a', + size: bytes.length +} + +function release(overrides = {}) { + return { + id: 42, + tag_name: TAG, + draft: true, + assets: [{ id: 101, name: expected.name, state: 'uploaded', size: expected.size }], + ...overrides + } +} + +function fetchFixture(assetResponse = new Response(bytes)) { + return vi + .fn() + .mockResolvedValueOnce(Response.json(release())) + .mockResolvedValueOnce(assetResponse) +} + +afterEach(() => vi.restoreAllMocks()) + +describe('SSH relay runtime draft read-back', () => { + it('downloads and hashes every exact managed asset from the same draft', async () => { + const fetchImpl = fetchFixture() + + await expect( + verifySshRelayRuntimeDraftReadback({ + repo: REPO, + releaseId: 42, + tag: TAG, + token: TOKEN, + expectedAssets: [expected], + fetchImpl + }) + ).resolves.toEqual({ releaseId: 42, tag: TAG, downloadedAssets: [expected] }) + }) + + it('follows only the approved asset redirect without forwarding authorization', async () => { + const location = 'https://release-assets.githubusercontent.com/example/runtime?sig=signed' + const fetchImpl = vi + .fn() + .mockResolvedValueOnce(Response.json(release())) + .mockResolvedValueOnce(new Response(null, { status: 302, headers: { location } })) + .mockResolvedValueOnce(new Response(bytes)) + + await verifySshRelayRuntimeDraftReadback({ + repo: REPO, + releaseId: 42, + tag: TAG, + token: TOKEN, + expectedAssets: [expected], + fetchImpl + }) + + expect(fetchImpl.mock.calls[1][1].headers.Authorization).toBe(`Bearer ${TOKEN}`) + expect(fetchImpl.mock.calls[2][0]).toBe(location) + expect(fetchImpl.mock.calls[2][1].headers).not.toHaveProperty('Authorization') + }) + + it.each([ + ['http://release-assets.githubusercontent.com/runtime', 'HTTPS'], + ['https://example.com/runtime', 'origin'] + ])('rejects an unsafe asset redirect %s', async (location, message) => { + const fetchImpl = vi + .fn() + .mockResolvedValueOnce(Response.json(release())) + .mockResolvedValueOnce(new Response(null, { status: 302, headers: { location } })) + + await expect( + verifySshRelayRuntimeDraftReadback({ + repo: REPO, + releaseId: 42, + tag: TAG, + token: TOKEN, + expectedAssets: [expected], + fetchImpl + }) + ).rejects.toThrow(new RegExp(message, 'i')) + }) + + it.each(['direct API response', 'redirected CDN response'])( + 'rejects an unexpected successful status from the %s', + async (responseKind) => { + const partialResponse = new Response(bytes, { + status: 206, + headers: { 'content-range': `bytes 0-${bytes.length - 1}/${bytes.length}` } + }) + const fetchImpl = + responseKind === 'direct API response' + ? fetchFixture(partialResponse) + : vi + .fn() + .mockResolvedValueOnce(Response.json(release())) + .mockResolvedValueOnce( + new Response(null, { + status: 302, + headers: { + location: 'https://release-assets.githubusercontent.com/example/runtime' + } + }) + ) + .mockResolvedValueOnce(partialResponse) + + await expect( + verifySshRelayRuntimeDraftReadback({ + repo: REPO, + releaseId: 42, + tag: TAG, + token: TOKEN, + expectedAssets: [expected], + fetchImpl + }) + ).rejects.toThrow(/failed 206/i) + } + ) + + it('rejects changed, truncated, or oversized returned bytes', async () => { + for (const body of [ + Buffer.from('changed release bytes'), + bytes.subarray(1), + Buffer.concat([bytes, bytes]) + ]) { + await expect( + verifySshRelayRuntimeDraftReadback({ + repo: REPO, + releaseId: 42, + tag: TAG, + token: TOKEN, + expectedAssets: [expected], + fetchImpl: fetchFixture(new Response(body)) + }) + ).rejects.toThrow(/size|sha-?256/i) + } + }) + + it('rejects a published, cross-tag, incomplete, or unexpected managed draft', async () => { + for (const changedRelease of [ + release({ draft: false }), + release({ tag_name: 'v1.4.140-rc.2' }), + release({ assets: [] }), + release({ + assets: [ + ...release().assets, + { id: 102, name: 'orca-ssh-relay-runtime-extra.zip', state: 'uploaded', size: 1 } + ] + }) + ]) { + const fetchImpl = vi.fn().mockResolvedValueOnce(Response.json(changedRelease)) + await expect( + verifySshRelayRuntimeDraftReadback({ + repo: REPO, + releaseId: 42, + tag: TAG, + token: TOKEN, + expectedAssets: [expected], + fetchImpl + }) + ).rejects.toThrow(/draft|tag|missing|unexpected/i) + } + }) + + it('rejects non-uploaded metadata or a mismatched declared size before download', async () => { + for (const asset of [ + { ...release().assets[0], state: 'new' }, + { ...release().assets[0], size: expected.size + 1 } + ]) { + const fetchImpl = vi.fn().mockResolvedValueOnce(Response.json(release({ assets: [asset] }))) + await expect( + verifySshRelayRuntimeDraftReadback({ + repo: REPO, + releaseId: 42, + tag: TAG, + token: TOKEN, + expectedAssets: [expected], + fetchImpl + }) + ).rejects.toThrow(/uploaded|size/i) + expect(fetchImpl).toHaveBeenCalledTimes(1) + } + }) + + it('honors cancellation before any GitHub request', async () => { + const controller = new AbortController() + controller.abort(new Error('cancel read-back')) + const fetchImpl = vi.fn() + + await expect( + verifySshRelayRuntimeDraftReadback({ + repo: REPO, + releaseId: 42, + tag: TAG, + token: TOKEN, + expectedAssets: [expected], + fetchImpl, + signal: controller.signal + }) + ).rejects.toThrow(/cancel read-back/i) + expect(fetchImpl).not.toHaveBeenCalled() + }) +}) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 93a773995d5..3eecea74467 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -98,7 +98,9 @@ describe('SSH relay runtime artifact workflow', () => { for (const moduleName of [ 'windows-source-signature-verification', 'release-stage-gate', - 'draft-recovery' + 'draft-recovery', + 'aggregate-input', + 'draft-readback' ]) { const script = `ssh-relay-runtime-${moduleName}` expect(source.split(`${script}.test.mjs`)).toHaveLength(5) 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 d1f5aa262f8..1cfa41ea32e 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 @@ -60,9 +60,12 @@ complete a box. remain separately gated. - Next external proof: kernel 4.18, macOS 13.5, Windows arm64 build 26100, and native signing/trust. - No tuple is enabled; every SSH transfer/runtime and rollout cell remains open. -- [ ] **WP3 active implementation — Prove disconnected release/signing DAG behavior in exact-head - CI.** Local contracts pass at `6f13e94f8`, which is one commit ahead of PR #8741; no production - workflow, publication, desktop consumer, or tuple is connected. +- [ ] **WP3 active implementation — Add disconnected aggregate-input and authenticated draft + read-back verification.** Release-DAG contracts pass locally and on all six native build jobs in + exact-head run [29390079639](https://github.com/stablyai/orca/actions/runs/29390079639) under + `E-M4-RELEASE-DAG-CI-001`. Aggregate/read-back RED/GREEN, workflow, full SSH-relay, and static + gates pass locally under `E-M4-AGGREGATE-READBACK-LOCAL-001`; exact-head CI is next. No + production workflow, publication, desktop consumer, signing credential, or tuple is connected. ## Work packages, in required order @@ -97,11 +100,12 @@ complete a box. ### WP3 — Release build and signing -- [ ] **In progress — 2026-07-15, Codex implementation owner:** add disconnected credential-free - fail-closed release/signing DAG contracts before production workflow wiring. Local red/green, - full SSH-relay, type, lint, formatting, and native-job wiring gates pass under - `E-M4-RELEASE-DAG-LOCAL-001` at local commit `6f13e94f8`; the commit is one ahead of the PR and - exact-head CI is next. +- [x] Release/signing DAG contracts pass locally and on all six native build jobs under + `E-M4-RELEASE-DAG-LOCAL-001` and `E-M4-RELEASE-DAG-CI-001`. +- [ ] **In progress — 2026-07-15, Codex implementation owner:** implement disconnected + aggregate-input and authenticated draft read-back verification before any production workflow, + publication, desktop consumer, or signing credential is connected. Local implementation and + verification pass under `E-M4-AGGREGATE-READBACK-LOCAL-001`; exact-head CI is next. - [ ] Add target-native runtime jobs as desktop release prerequisites. - [ ] Add native signing jobs; hash only the returned signed bytes. - [ ] Add a fail-closed aggregate and immutable manifest-signing job. 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 1cf08da951c..72bce889119 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 credential-free release/signing DAG failure contracts — **In progress — 2026-07-15, Codex implementation owner**. Work Package 2's target-native Windows official-Node and preserved-upstream source-signature gate is closed under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001. Exact-floor gaps and real native signing/trust remain open external gates, so the active package is limited to disconnected fail-closed release-DAG contracts and tests; it may not publish, connect desktop consumers, or enable a tuple. Production/default behavior is unchanged, no bundled-runtime path is enabled, and no artifact is published.
-Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — local implementation commit `6f13e94f8a7a315ac3da6bb2067b99da8e02a803` is one commit ahead of the PR head and closes the disconnected release-stage and draft-recovery contracts locally under E-M4-RELEASE-DAG-LOCAL-001. Exact-head CI for that commit is the active gate; push and GitHub-runner evidence collection are now authorized. The current PR head remains `be32653a7`: artifact jobs 87267322867 and 87267322870 in run [29388734922](https://github.com/stablyai/orca/actions/runs/29388734922) passed target-native Windows source-signature verification under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001, while PR Checks [29388734935](https://github.com/stablyai/orca/actions/runs/29388734935) and Golden E2E [29388734914](https://github.com/stablyai/orca/actions/runs/29388734914) are green. The artifact run's only failure is the retained Windows arm64 build-26200-versus-26100 floor gate. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, and native trust remain separately gated. Nothing is published or enabled, and legacy remains the production default.
+Current phase: Milestone 4 / Work Package 3 credential-free aggregate-input and draft read-back verification — **In progress — 2026-07-15, Codex implementation owner**. The disconnected release/signing DAG failure contracts are closed locally and on all six native build jobs under E-M4-RELEASE-DAG-LOCAL-001 and E-M4-RELEASE-DAG-CI-001. Exact-floor gaps and real native signing/trust remain open external gates, so the active package may validate only immutable aggregate inputs and authenticated draft bytes; it may not publish, connect desktop consumers, use signing credentials, or enable a tuple. Production/default behavior is unchanged, no bundled-runtime path is enabled, and no artifact is published.
+Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact-head artifact run [29390079639](https://github.com/stablyai/orca/actions/runs/29390079639) executes the two release-DAG suites under Node 24 on all six target-native build jobs at `97164470c`; every build and supplemental userland job passes. The run remains red only because Windows arm64 runner build 26200 is not the required build 26100, after archive/tree/runtime smoke passes in 7,721.7682 ms with 48,504,832-byte RSS. Golden E2E [29390079632](https://github.com/stablyai/orca/actions/runs/29390079632) is green. PR Checks [29390079665](https://github.com/stablyai/orca/actions/runs/29390079665) attempt 1 passed lint, typecheck, Git compatibility, and tests, then hit a TCP reset downloading Electron; authorized attempt 2 job 87272898091 passes the complete verify/package/CLI-smoke job. The disconnected aggregate-input and authenticated draft read-back implementation now passes local RED/GREEN, workflow-wiring, full SSH-relay, type, lint, line-budget, formatting, and diff gates under E-M4-AGGREGATE-READBACK-LOCAL-RED-001 and E-M4-AGGREGATE-READBACK-LOCAL-001; exact-head native CI is next. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, and native trust remain separately gated. Nothing is published or enabled, and legacy remains the production default.
Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -65,10 +65,11 @@ same change as the work it records. [#8728](https://github.com/stablyai/orca/pull/8728), which is CI-green at current PR head `0c299fe18`; implementation evidence remains anchored at `b9d80a4cb`. No deploy/resolver call site is connected and no tuple is enabled. -- Active package: Work Package 3 credential-free release/signing DAG failure contracts. The package - must model fail-closed build, approval, signing, aggregate, upload, read-back, and recovered-draft - outcomes without credentials, production workflow wiring, publication, desktop consumers, or - tuple enablement. No Milestone 4 box is checked until an implemented contract and exact tests exist. +- Active package: Work Package 3 credential-free aggregate-input and authenticated draft read-back + verification. It must bind every input and returned byte to exact name, size, SHA-256, tuple, + content identity, and source release without credentials, publication, production workflow wiring, + desktop consumers, or tuple enablement. No Milestone 4 box is checked until the corresponding + implementation and exact tests exist. - 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 @@ -1459,7 +1460,7 @@ Update status and evidence as work begins. Do not combine these into one large b | 0. #8450 legacy fix | Coherent Node/npm selection and live repro | Fixes legacy selection only | Complete and CI-green in draft PR #8724 | E-M0-UNIT-002, E-M0-LIVE-002, E-M0-STATIC-002, E-M0-PR-001, E-M0-CI-001 | | 1. Contract and selectors | Manifest schema, identity, platform/libc selection, hostile inputs | None | Complete and CI-green in draft PR #8728 | `b9d80a4cb`; E-M2-RED-001, E-M2-CONTRACT-001, E-M2-CI-001 | | 2. Runtime builds | Per-tuple assembly, native smoke, SBOM/provenance/signing | None | Draft PR #8741; native build/source trust green; real signing/trust pending | `be32653a7`; E-M3-METADATA-CI-001, E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001 | -| 3. Release publication | Prerequisite DAG, embedded manifest, draft upload/read-back gates | Asset-only | **In progress — 2026-07-15, Codex implementation owner** | E-M4-RELEASE-DAG-LOCAL-001; exact-head CI pending | +| 3. Release publication | Prerequisite DAG, embedded manifest, draft upload/read-back gates | Asset-only | **In progress — 2026-07-15, Codex implementation owner** | E-M4-RELEASE-DAG-LOCAL-001; E-M4-RELEASE-DAG-CI-001 | | 4. Desktop resolver/cache | Verified download, extraction, cache, offline behavior | None/forced mode only | Not started | — | | 5. Transfer/install | Bounded transports, structured sentinel, bundled launch behind per-target Beta/forced mode | Per-target opt-in only | Not started | — | | 6. Fallback/diagnostics | Abort-and-join state machine, mode isolation, reason codes, target-mode configuration/UI | Per-target Beta only | Not started | — | @@ -1527,6 +1528,8 @@ focused commands as their scripts/tests are introduced. - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs config/scripts/ssh-relay-runtime-draft-recovery.test.mjs` (E-M4-RELEASE-DAG-LOCAL-RED-001, E-M4-RELEASE-DAG-LOCAL-001; disconnected credential-free stage/recovery contract only) - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs config/scripts/ssh-relay-runtime-draft-recovery.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` (E-M4-RELEASE-DAG-LOCAL-001; static native-job wiring only, no real release/signing execution) +- [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-aggregate-input.test.mjs config/scripts/ssh-relay-runtime-draft-readback.test.mjs` (E-M4-AGGREGATE-READBACK-LOCAL-RED-001, E-M4-AGGREGATE-READBACK-LOCAL-001; local filesystem plus mocked GitHub responses only) +- [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-aggregate-input.test.mjs config/scripts/ssh-relay-runtime-draft-readback.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` (E-M4-AGGREGATE-READBACK-LOCAL-001; static native-job wiring only) ### Commands/scripts that must be added or formally identified @@ -8846,6 +8849,149 @@ diff --check`. credential-free aggregate-input/read-back implementation without connecting publication or a desktop consumer. +### E-M4-RELEASE-DAG-CI-001 — Release-DAG contracts pass all six native build jobs + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact PR head `97164470cb9abad670634b9d4f04424bcc8662b5`; implementation commit + `6f13e94f8a7a315ac3da6bb2067b99da8e02a803`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). +- Runner/network: GitHub-hosted native runners with normal artifact-build egress and no SSH remote, + signing service, release credential, approval environment, draft upload, publication, or desktop + consumer. Exact jobs and resolved images: + - linux-x64-glibc job 87271365779: `ubuntu-24.04`, `ubuntu24` image `20260705.232.1`, X64, + 04:53:59Z–04:58:13Z. + - linux-arm64-glibc job 87271365815: `ubuntu-24.04-arm`, `ubuntu24-arm64` image + `20260714.61.1`, ARM64, 04:54:03Z–04:59:11Z. + - darwin-x64 job 87271365782: `macos-15-intel`, `macos15` image `20260629.0276.1`, X64, + 04:53:59Z–04:59:21Z. + - darwin-arm64 job 87271365773: `macos-15`, `macos15` image `20260706.0213.1`, ARM64, + 04:53:59Z–04:57:05Z. + - win32-x64 job 87271365831: `windows-2022`, `win22` image `20260706.237.1`, X64, + 04:53:59Z–04:59:34Z. + - win32-arm64 job 87271365776: `windows-11-arm`, `win11-arm64` image `20260706.102.1`, ARM64, + 04:53:59Z–05:03:25Z. +- Command: the workflow's `Run runtime artifact contract tests` step syntax-checks both new modules + and tests, then includes + `ssh-relay-runtime-release-stage-gate.test.mjs` and + `ssh-relay-runtime-draft-recovery.test.mjs` in the purpose-named native Vitest command. Inspection + used `gh run view 29390079639 --repo stablyai/orca --job --log` for every job. +- Result: PASS for all six target-native build jobs. Each runner passes the release-stage suite's 15 + tests and draft-recovery suite's 6 tests under Node v24.18.0. The four POSIX jobs report 32 passing + contract files; the two Windows jobs report 33, including the Windows-only PE diagnostic suite. + Both Linux oldest-userland jobs and Windows x64 floor job also pass. Overall workflow conclusion is + the expected `failure` only because downstream Windows arm64 floor job 87272581856 observes build + 26200 instead of required 26100 after the exact runtime passes archive/tree verification, bundled + Node, PTY, watcher, and resource settlement in 7,721.7682 ms with 48,504,832-byte RSS. +- Concurrent regression: Golden E2E run + [29390079632](https://github.com/stablyai/orca/actions/runs/29390079632) passes both macOS job + 87271365717 and Linux job 87271365749. PR Checks run 29390079665 attempt 1 job 87271365846 passes + lint, typecheck, Git compatibility, and tests, then flakes during `Build unpacked app` when a + 125-MB Electron GitHub asset download receives `read: connection reset by peer`. Authorized + attempt 2 job 87272898091 runs from 05:05:54Z to 05:21:00Z and passes the complete verify job, + including unpacked packaging and packaged CLI smoke. +- Oracle proved: the disconnected build → signing → aggregate → upload → read-back failure contract + and same-draft immutable recovery contract parse and execute identically on all six native runner + families, including Windows PowerShell workflow wiring. No platform can omit its required signing + stage, invent a Linux signing stage, exceed the bounded retry/time policy, or accept missing, + duplicate, drifted, failed, cancelled, approval-rejected, or recovered cross-release state. +- Does not prove: real Apple/SignPath signing or approval, target-native trust of returned Orca bytes, + aggregate/manifest generation, manifest signing, GitHub draft upload/read-back, packaged manifest + embedding, SSH transfer/install, fallback, performance, or an enabled tuple. +- Follow-up: implement disconnected aggregate-input and authenticated draft read-back verification, + then execute it on all six native job families. Retain every production/default and tuple state. + +### E-M4-AGGREGATE-READBACK-LOCAL-RED-001 — Aggregate and draft byte verifiers are absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact implementation base `97164470cb9abad670634b9d4f04424bcc8662b5` plus the two new + uncommitted purpose-named test files. +- Command: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \ + config/scripts/ssh-relay-runtime-aggregate-input.test.mjs \ + config/scripts/ssh-relay-runtime-draft-readback.test.mjs + ``` + +- Result: expected RED in 284 ms. Both suites fail before collecting tests with + `ERR_MODULE_NOT_FOUND` for `ssh-relay-runtime-aggregate-input.mjs` and + `ssh-relay-runtime-draft-readback.mjs`. +- Oracle proved: the exact head had only declarative stage/recovery contracts; it had no filesystem + implementation binding aggregate input declarations to stable archive bytes and no authenticated, + bounded draft asset read-back implementation. +- Does not prove: implementation, GitHub API behavior, actual release assets, native runners, + manifest generation/signing, publication, desktop embedding, SSH behavior, or an enabled tuple. +- Correction: implement both byte boundaries as disconnected modules with bounded streaming, + cancellation, exact identity/asset checks, and credential-safe redirect handling. + +### E-M4-AGGREGATE-READBACK-LOCAL-001 — Immutable aggregate and draft read-back bytes pass locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted implementation based on exact head + `97164470cb9abad670634b9d4f04424bcc8662b5`. +- Runner/network: local macOS 26.2 build 25C56 arm64, Node v26.0.0 and pnpm 10.24.0. Aggregate tests + use exclusive local temporary directories. Draft tests use injected WHATWG `Response` objects and + make no network, GitHub API, release, credential, signing-service, SSH, publication, or production + call. The repository emits its expected Node-24 engine warning. +- Commands: + + ```sh + node --check config/scripts/ssh-relay-runtime-aggregate-input.mjs + 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 + /usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \ + config/scripts/ssh-relay-runtime-aggregate-input.test.mjs \ + config/scripts/ssh-relay-runtime-draft-readback.test.mjs + pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \ + config/scripts/ssh-relay-runtime-aggregate-input.test.mjs \ + config/scripts/ssh-relay-runtime-draft-readback.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \ + config/scripts/ssh-relay-*.test.mjs + pnpm run typecheck + pnpm run lint + pnpm run check:max-lines-ratchet + pnpm exec oxfmt --check + git diff --check + ``` + +- Result: PASS. Four syntax checks pass. After exact-HTTP-200 hardening, the two purpose-named suites + pass 2 files/17 tests in 295 ms; `/usr/bin/time -l` records 0.89 seconds wall time, + 132,104,192-byte maximum RSS, zero swaps, and 96,277,880-byte peak memory footprint. + Workflow-inclusive focus passes 3 files/23 tests in 410 ms. The complete SSH-relay suite passes 35 + files/201 tests in 6.63 seconds. Typecheck, full + lint/reliability/localization/bundled-skill gates, max-lines (355 grandfathered suppressions and no + new bypass), focused formatting, and `git diff --check` all pass; lint emits only existing unrelated + warnings. +- Aggregate oracle: accepts at most eight declared supported tuples and derives each immutable + archive name from its tuple/content identity. The exclusive input directory must contain exactly + those regular files, with no extras or links; each file is streamed under a 100-MiB/file and + 15-minute bound, and exact size, SHA-256, and stable pre/post file metadata must match before the + declared asset chain is returned. Duplicate tuple/name, unsupported tuple, malformed identity, + cancellation, missing/extra input, or byte drift fails closed. +- Draft read-back oracle: requires one exact draft ID and stable/RC/perf tag, exact managed asset + names, uploaded state, nonzero declared size, at most 100 MiB per asset/1 GiB total, and a 15-minute + bound. It manually handles the authenticated GitHub asset API redirect, accepts only HTTPS + `release-assets.githubusercontent.com`, never forwards authorization to the CDN, requires exact + HTTP 200 from either direct or redirected downloads, streams every body, and requires exact size + and SHA-256. Published/cross-tag/incomplete/unexpected drafts, unsafe redirects, unexpected 2xx + statuses, changed/truncated/oversized bytes, and cancellation fail closed. +- Workflow oracle: both POSIX and Windows artifact jobs syntax-check and execute the two new suites + under the same Node 24 native job families. The workflow retains read-only contents permission and + has no release, signing credential, or publication authority. +- Does not prove: a real GitHub API/CDN redirect, a real draft or full-size asset, aggregate manifest + generation, Ed25519 signing, native signing/trust, upload/recovery execution, desktop + prerequisites/embedding, SSH transfer/install, fallback, performance, or an enabled tuple. The + local symlink fixture is skipped on Windows, although the shared implementation rejects all + non-regular directory entries and exact-head Windows execution remains required. +- Follow-up: execute both suites on all six native jobs at the exact implementation head, then add + credential-free canonical manifest assembly/signature handoff without production credentials, + publication, or a desktop consumer. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -8903,12 +9049,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Run the disconnected credential-free release/signing DAG and draft-recovery contracts on all six -native jobs at the exact implementation head. If green, implement the next artifact-only -aggregate-input/read-back capability without release publication or a desktop consumer. In parallel, -provision qualifying exact-floor execution for Linux kernel 4.18, macOS 13.5, and Windows arm64 build -26100 and real macOS/Windows signing and trust. Keep every tuple disabled until the applicable -baseline and trust cells pass. +Commit and run the disconnected aggregate-input and authenticated draft read-back verifiers on all +six native jobs at the exact implementation head. If green, record exact job evidence and implement +the next credential-free canonical-manifest assembly/signature-handoff slice without release +publication, production signing credentials, or a desktop consumer. In parallel, provision +qualifying exact-floor execution for Linux kernel 4.18, macOS 13.5, and Windows arm64 build 26100 and +real macOS/Windows signing and trust. Keep every tuple disabled until the applicable baseline and +trust cells pass. Cross-family Layer B targets, the protected manifest-signing environment, oldest-baseline/native- trust cells, and the paired legacy performance baseline remain release/default-path blockers. No