From 5460563ff48846cbf54218860fdca1cc3c7b8614 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 12:46:20 -0400 Subject: [PATCH] build(ssh): add relay release asset gate Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 8 + .../ssh-relay-runtime-release-assets.mjs | 289 ++++++++++++++++++ .../ssh-relay-runtime-release-assets.test.mjs | 193 ++++++++++++ .../ssh-relay-runtime-workflow.test.mjs | 3 +- ...elease-implementation-checklist-summary.md | 12 +- ...github-release-implementation-checklist.md | 106 ++++++- 6 files changed, 596 insertions(+), 15 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-release-assets.mjs create mode 100644 config/scripts/ssh-relay-runtime-release-assets.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 37d2c741edd..bab4b0d3517 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -220,6 +220,8 @@ jobs: node --check config/scripts/ssh-relay-runtime-draft-readback.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 + node --check config/scripts/ssh-relay-runtime-release-assets.test.mjs node --check config/scripts/ssh-relay-runtime-manifest-validation.mjs node --check config/scripts/ssh-relay-runtime-manifest-assembly.mjs node --check config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs @@ -276,6 +278,7 @@ jobs: config/scripts/ssh-relay-runtime-aggregate-input.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-release-assets.test.mjs \ config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs \ config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs \ config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs \ @@ -641,6 +644,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime draft upload syntax check failed' } node --check config/scripts/ssh-relay-runtime-draft-upload.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime draft upload test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-release-assets.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime release assets syntax check failed' } + node --check config/scripts/ssh-relay-runtime-release-assets.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime release assets test syntax check failed' } node --check config/scripts/ssh-relay-runtime-manifest-validation.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime manifest validation syntax check failed' } node --check config/scripts/ssh-relay-runtime-manifest-assembly.mjs @@ -726,6 +733,7 @@ jobs: config/scripts/ssh-relay-runtime-aggregate-input.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-release-assets.test.mjs ` config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs ` config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs ` config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs ` diff --git a/config/scripts/ssh-relay-runtime-release-assets.mjs b/config/scripts/ssh-relay-runtime-release-assets.mjs new file mode 100644 index 00000000000..d3b22b55a2d --- /dev/null +++ b/config/scripts/ssh-relay-runtime-release-assets.mjs @@ -0,0 +1,289 @@ +import { parseSshRelayRuntimeUnsignedManifest } from './ssh-relay-runtime-manifest-validation.mjs' + +const ASSET_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,239}$/u +const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u +const SIGNATURE_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/u +const MANAGED_ASSET_PREFIX = 'orca-ssh-relay-runtime-' +const MANIFEST_NAME = 'orca-ssh-relay-runtime-manifest.json' +const SIGNATURE_NAME = 'orca-ssh-relay-runtime-manifest.sig' +const MAX_ASSET_BYTES = 100 * 1024 * 1024 +const MAX_TOTAL_BYTES = 1024 * 1024 * 1024 +const MAX_MANIFEST_BYTES = 1024 * 1024 +const MAX_SIGNATURE_BYTES = 4096 +const MAX_MANAGED_ASSETS = 26 +const MAX_RELEASE_ASSETS = 1000 +const MAX_SIGNATURES = 4 + +function assertObject(value, label) { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`SSH relay runtime release assets ${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 release assets ${label} has unexpected or missing fields`) + } +} + +function parseReleaseTag(tag) { + if (typeof tag !== 'string') { + throw new Error('SSH relay runtime release assets tag is invalid') + } + for (const [pattern, channel] of [ + [/^v(\d+\.\d+\.\d+)$/u, 'stable'], + [/^v(\d+\.\d+\.\d+-rc\.\d+)$/u, 'rc'], + [/^v(\d+\.\d+\.\d+-rc\.\d+\.perf)$/u, 'perf'] + ]) { + const match = pattern.exec(tag) + if (match) { + return { tag, channel, version: match[1] } + } + } + throw new Error('SSH relay runtime release assets tag is invalid') +} + +function normalizeSignatures(signatures) { + if (!Array.isArray(signatures) || signatures.length === 0 || signatures.length > MAX_SIGNATURES) { + throw new Error('SSH relay runtime release assets verified manifest signatures are invalid') + } + const keyIds = new Set() + for (const [index, signature] of signatures.entries()) { + assertExactFields(signature, ['algorithm', 'keyId', 'signature'], `manifest signature ${index}`) + if (signature.algorithm !== 'ed25519-v1' || !DIGEST_PATTERN.test(signature.keyId)) { + throw new Error('SSH relay runtime release assets verified manifest signature is invalid') + } + if (typeof signature.signature !== 'string' || !SIGNATURE_PATTERN.test(signature.signature)) { + throw new Error('SSH relay runtime release assets verified manifest signature is invalid') + } + const signatureBytes = Buffer.from(signature.signature, 'base64') + if ( + signatureBytes.length !== 64 || + signatureBytes.toString('base64') !== signature.signature || + keyIds.has(signature.keyId) + ) { + throw new Error('SSH relay runtime release assets verified manifest signature is invalid') + } + keyIds.add(signature.keyId) + } + return [...keyIds].sort() +} + +function normalizeAssetDescriptor(asset, expectedName, label, maximum = MAX_ASSET_BYTES) { + assertExactFields(asset, ['name', 'sha256', 'size'], label) + if (asset.name !== expectedName || !ASSET_NAME_PATTERN.test(asset.name)) { + throw new Error(`SSH relay runtime release assets ${label} has an invalid name`) + } + if (!DIGEST_PATTERN.test(asset.sha256)) { + throw new Error(`SSH relay runtime release assets ${label} has an invalid SHA-256`) + } + if (!Number.isSafeInteger(asset.size) || asset.size <= 0 || asset.size > maximum) { + throw new Error(`SSH relay runtime release assets ${label} has an invalid size`) + } + return { ...asset } +} + +function normalizeSignatureAsset(asset, manifestAsset, manifestKeyIds) { + assertExactFields( + asset, + ['keyIds', 'manifestSha256', 'name', 'sha256', 'size'], + 'signature asset' + ) + const descriptor = normalizeAssetDescriptor( + { name: asset.name, sha256: asset.sha256, size: asset.size }, + SIGNATURE_NAME, + 'signature asset descriptor', + MAX_SIGNATURE_BYTES + ) + if ( + !Array.isArray(asset.keyIds) || + asset.keyIds.length === 0 || + asset.keyIds.length > MAX_SIGNATURES || + asset.keyIds.some((keyId) => typeof keyId !== 'string' || !DIGEST_PATTERN.test(keyId)) + ) { + throw new Error('SSH relay runtime release assets signature keys are invalid') + } + if (!DIGEST_PATTERN.test(asset.manifestSha256)) { + throw new Error('SSH relay runtime release assets signature manifest identity is invalid') + } + if (asset.manifestSha256 !== manifestAsset.sha256) { + throw new Error( + 'SSH relay runtime release assets signature manifest identity disagrees with the manifest' + ) + } + const keyIds = [...new Set(asset.keyIds)].sort() + if ( + keyIds.length !== asset.keyIds.length || + JSON.stringify(keyIds) !== JSON.stringify(manifestKeyIds) + ) { + // Why: the detached encoding stays separately gated, but it cannot claim different signers. + throw new Error('SSH relay runtime release assets signature keys disagree with the manifest') + } + return descriptor +} + +function normalizeVerifiedManifest(envelope) { + assertExactFields( + envelope, + ['manifest', 'manifestAsset', 'signatureAsset'], + 'verified manifest envelope' + ) + assertObject(envelope.manifest, 'verified manifest') + const { signatures, ...unsignedManifest } = envelope.manifest + const manifest = parseSshRelayRuntimeUnsignedManifest(unsignedManifest) + const manifestKeyIds = normalizeSignatures(signatures) + const manifestAsset = normalizeAssetDescriptor( + envelope.manifestAsset, + MANIFEST_NAME, + 'manifest asset', + MAX_MANIFEST_BYTES + ) + const signatureAsset = normalizeSignatureAsset( + envelope.signatureAsset, + manifestAsset, + manifestKeyIds + ) + return { manifest, manifestAsset, signatureAsset } +} + +function coveredManifestAssets(manifest) { + const assets = [] + for (const tuple of manifest.tuples) { + assets.push( + normalizeAssetDescriptor( + { + name: tuple.archive.name, + sha256: tuple.archive.sha256, + size: tuple.archive.size + }, + tuple.archive.name, + `${tuple.tupleId} archive` + ), + normalizeAssetDescriptor( + tuple.metadataAssets.sbom, + tuple.metadataAssets.sbom.name, + `${tuple.tupleId} SBOM` + ), + normalizeAssetDescriptor( + tuple.metadataAssets.provenance, + tuple.metadataAssets.provenance.name, + `${tuple.tupleId} provenance` + ) + ) + } + return assets +} + +function exactManagedAssets(verified) { + const assets = [ + ...coveredManifestAssets(verified.manifest), + verified.manifestAsset, + verified.signatureAsset + ] + if (assets.length === 0 || assets.length > MAX_MANAGED_ASSETS) { + throw new Error('SSH relay runtime release assets coverage must be a bounded non-empty set') + } + const names = new Set() + let totalBytes = 0 + for (const asset of assets) { + if (!asset.name.startsWith(MANAGED_ASSET_PREFIX) || names.has(asset.name)) { + throw new Error( + `SSH relay runtime release assets has duplicate or invalid coverage: ${asset.name}` + ) + } + names.add(asset.name) + totalBytes += asset.size + } + if (!Number.isSafeInteger(totalBytes) || totalBytes > MAX_TOTAL_BYTES) { + throw new Error('SSH relay runtime release assets coverage exceeds the total size limit') + } + return assets +} + +function validateBuildIdentity(manifest, expected) { + if (manifest.build.tag !== expected.tag) { + throw new Error('SSH relay runtime release assets manifest tag does not match the release') + } + if (manifest.build.channel !== expected.channel || manifest.build.version !== expected.version) { + throw new Error('SSH relay runtime release assets manifest build identity does not match') + } +} + +function validateRelease(release, releaseId, expectedIdentity, expectedAssets) { + assertObject(release, 'authenticated release metadata') + if (!Number.isSafeInteger(releaseId) || releaseId <= 0 || release.id !== releaseId) { + throw new Error('SSH relay runtime release assets release ID does not match') + } + if (release.draft !== true) { + throw new Error('SSH relay runtime release assets requires an exact unpublished draft') + } + if (release.tag_name !== expectedIdentity.tag) { + throw new Error('SSH relay runtime release assets release tag does not match') + } + const expectedPrerelease = expectedIdentity.channel !== 'stable' + if (release.prerelease !== expectedPrerelease) { + throw new Error('SSH relay runtime release assets prerelease state disagrees with its channel') + } + if (!Array.isArray(release.assets) || release.assets.length > MAX_RELEASE_ASSETS) { + throw new Error('SSH relay runtime release assets metadata must contain a bounded asset 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' || !ASSET_NAME_PATTERN.test(asset.name)) { + throw new Error('SSH relay runtime release assets has an invalid release asset') + } + if (releaseByName.has(asset.name)) { + throw new Error( + `SSH relay runtime release assets has a duplicate release asset: ${asset.name}` + ) + } + releaseByName.set(asset.name, asset) + if (asset.name.startsWith(MANAGED_ASSET_PREFIX) && !expectedByName.has(asset.name)) { + throw new Error( + `SSH relay runtime release assets 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 release assets is missing managed asset: ${expected.name}`) + } + if (!Number.isSafeInteger(actual.id) || actual.id <= 0) { + throw new Error(`SSH relay runtime release asset ID is invalid: ${expected.name}`) + } + if (actual.state !== 'uploaded') { + throw new Error(`SSH relay runtime release asset is not uploaded: ${expected.name}`) + } + if (actual.size === 0) { + throw new Error(`SSH relay runtime release asset is empty: ${expected.name}`) + } + if (actual.size !== expected.size) { + throw new Error(`SSH relay runtime release asset size disagrees: ${expected.name}`) + } + } +} + +export function verifySshRelayRuntimeReleaseAssets({ releaseId, tag, release, verifiedManifest }) { + const expectedIdentity = parseReleaseTag(tag) + const verified = normalizeVerifiedManifest(verifiedManifest) + validateBuildIdentity(verified.manifest, expectedIdentity) + const expectedAssets = exactManagedAssets(verified) + // Why: this pure boundary can consume authenticated metadata without gaining release authority. + validateRelease(release, releaseId, expectedIdentity, expectedAssets) + return { + releaseId: release.id, + tag, + channel: expectedIdentity.channel, + draft: true, + prerelease: release.prerelease, + checked: expectedAssets.map((asset) => asset.name).sort() + } +} diff --git a/config/scripts/ssh-relay-runtime-release-assets.test.mjs b/config/scripts/ssh-relay-runtime-release-assets.test.mjs new file mode 100644 index 00000000000..cef9cffb4c7 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-release-assets.test.mjs @@ -0,0 +1,193 @@ +import { describe, expect, it, vi } from 'vitest' + +import { createSshRelayArtifactTestManifest } from '../../src/main/ssh/ssh-relay-artifact-test-manifest' +import { verifySshRelayRuntimeReleaseAssets } from './ssh-relay-runtime-release-assets.mjs' + +const MANIFEST_NAME = 'orca-ssh-relay-runtime-manifest.json' +const SIGNATURE_NAME = 'orca-ssh-relay-runtime-manifest.sig' +const digest = (character) => `sha256:${character.repeat(64)}` + +function buildIdentity(tag) { + for (const [pattern, channel] of [ + [/^v(\d+\.\d+\.\d+)$/u, 'stable'], + [/^v(\d+\.\d+\.\d+-rc\.\d+)$/u, 'rc'], + [/^v(\d+\.\d+\.\d+-rc\.\d+\.perf)$/u, 'perf'] + ]) { + const match = pattern.exec(tag) + if (match) { + return { tag, channel, version: match[1] } + } + } + throw new Error(`Unsupported test tag: ${tag}`) +} + +function fixture(tag = 'v1.4.140-rc.1') { + const manifest = createSshRelayArtifactTestManifest() + manifest.build = { ...manifest.build, ...buildIdentity(tag) } + const manifestAsset = { name: MANIFEST_NAME, size: 2_048, sha256: digest('e') } + const signatureAsset = { + name: SIGNATURE_NAME, + size: 256, + sha256: digest('f'), + manifestSha256: manifestAsset.sha256, + keyIds: manifest.signatures.map((signature) => signature.keyId) + } + const expectedAssets = [ + manifest.tuples[0].archive, + manifest.tuples[0].metadataAssets.sbom, + manifest.tuples[0].metadataAssets.provenance, + manifestAsset, + signatureAsset + ] + const release = { + id: 42, + tag_name: tag, + draft: true, + prerelease: manifest.build.channel !== 'stable', + assets: [ + ...expectedAssets.map((asset, index) => ({ + id: index + 1, + name: asset.name, + state: 'uploaded', + size: asset.size + })), + { id: 99, name: 'orca-windows-setup.exe', state: 'uploaded', size: 3_000 } + ] + } + return { + releaseId: release.id, + tag, + release, + verifiedManifest: { manifest, manifestAsset, signatureAsset } + } +} + +function managedAsset(input, name) { + return input.release.assets.find((asset) => asset.name === name) +} + +describe('SSH relay runtime release required assets', () => { + it.each([ + ['v1.4.140', 'stable', false], + ['v1.4.140-rc.2', 'rc', true], + ['v1.4.140-rc.2.perf', 'perf', true] + ])( + 'accepts exact %s manifest coverage and authenticated draft metadata', + (tag, channel, prerelease) => { + const input = fixture(tag) + + expect(verifySshRelayRuntimeReleaseAssets(input)).toEqual({ + releaseId: 42, + tag, + channel, + draft: true, + prerelease, + checked: expect.arrayContaining([ + input.verifiedManifest.manifest.tuples[0].archive.name, + input.verifiedManifest.manifest.tuples[0].metadataAssets.sbom.name, + input.verifiedManifest.manifest.tuples[0].metadataAssets.provenance.name, + MANIFEST_NAME, + SIGNATURE_NAME + ]) + }) + } + ) + + it.each([ + ['missing', (input) => input.release.assets.splice(0, 1), /missing managed asset/i], + [ + 'extra managed', + (input) => + input.release.assets.push({ + id: 100, + name: 'orca-ssh-relay-runtime-uncovered.zip', + state: 'uploaded', + size: 1 + }), + /unexpected managed asset/i + ], + [ + 'duplicate', + (input) => input.release.assets.push(structuredClone(input.release.assets[0])), + /duplicate release asset/i + ] + ])('rejects a %s managed asset set', (_label, mutate, message) => { + const input = fixture() + mutate(input) + expect(() => verifySshRelayRuntimeReleaseAssets(input)).toThrow(message) + }) + + it.each([ + ['non-uploaded', (asset) => (asset.state = 'new'), /not uploaded/i], + ['empty', (asset) => (asset.size = 0), /empty/i], + ['wrong-size', (asset) => (asset.size += 1), /size disagrees/i] + ])('rejects a %s covered asset', (_label, mutate, message) => { + const input = fixture() + mutate(managedAsset(input, MANIFEST_NAME)) + expect(() => verifySshRelayRuntimeReleaseAssets(input)).toThrow(message) + }) + + it('rejects release, manifest, or prerelease identity crossing tags and channels', () => { + const crossRelease = fixture() + crossRelease.release.tag_name = 'v1.4.140-rc.2' + expect(() => verifySshRelayRuntimeReleaseAssets(crossRelease)).toThrow(/release tag/i) + + const crossDraft = fixture() + crossDraft.release.id += 1 + expect(() => verifySshRelayRuntimeReleaseAssets(crossDraft)).toThrow(/release ID/i) + + const crossManifest = fixture() + Object.assign(crossManifest.verifiedManifest.manifest.build, buildIdentity('v1.4.140-rc.2')) + expect(() => verifySshRelayRuntimeReleaseAssets(crossManifest)).toThrow(/manifest tag/i) + + const crossChannel = fixture() + crossChannel.verifiedManifest.manifest.build.channel = 'stable' + expect(() => verifySshRelayRuntimeReleaseAssets(crossChannel)).toThrow(/manifest.*identity/i) + + const wrongPrerelease = fixture() + wrongPrerelease.release.prerelease = false + expect(() => verifySshRelayRuntimeReleaseAssets(wrongPrerelease)).toThrow(/prerelease/i) + }) + + it('rejects a detached signature asset that disagrees with verified manifest signatures', () => { + const input = fixture() + input.verifiedManifest.signatureAsset.keyIds = [digest('0')] + + expect(() => verifySshRelayRuntimeReleaseAssets(input)).toThrow(/signature keys disagree/i) + + const crossManifest = fixture() + crossManifest.verifiedManifest.signatureAsset.manifestSha256 = digest('0') + expect(() => verifySshRelayRuntimeReleaseAssets(crossManifest)).toThrow( + /signature manifest identity disagrees/i + ) + }) + + it('rejects malformed or incomplete verified-manifest asset bindings', () => { + const malformedDigest = fixture() + malformedDigest.verifiedManifest.manifestAsset.sha256 = digest('A') + expect(() => verifySshRelayRuntimeReleaseAssets(malformedDigest)).toThrow(/sha-256/i) + + const missingSignature = fixture() + missingSignature.verifiedManifest.manifest.signatures = [] + expect(() => verifySshRelayRuntimeReleaseAssets(missingSignature)).toThrow(/signature/i) + + const malformedSignature = fixture() + malformedSignature.verifiedManifest.manifest.signatures[0].signature = 42 + expect(() => verifySshRelayRuntimeReleaseAssets(malformedSignature)).toThrow(/signature/i) + + const unexpectedField = fixture() + unexpectedField.verifiedManifest.manifestAsset.path = '/tmp/manifest' + expect(() => verifySshRelayRuntimeReleaseAssets(unexpectedField)).toThrow(/fields/i) + }) + + it('is a disconnected metadata gate with no network request', () => { + const fetchMock = vi.fn(() => { + throw new Error('network must remain disconnected') + }) + vi.stubGlobal('fetch', fetchMock) + + expect(() => verifySshRelayRuntimeReleaseAssets(fixture())).not.toThrow() + expect(fetchMock).not.toHaveBeenCalled() + vi.unstubAllGlobals() + }) +}) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 1e997d16d96..fb28af32c32 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -134,7 +134,8 @@ describe('SSH relay runtime artifact workflow', () => { 'draft-recovery', 'aggregate-input', 'draft-readback', - 'draft-upload' + 'draft-upload', + 'release-assets' ]) { 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 c1a3d733041..9da4b6a3256 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 @@ -15,9 +15,11 @@ in progress, 2026-07-15, Codex implementation owner.** Exact-head run target-native artifact jobs and the uploader's eight-test suite on every Node 24 runner under `E-M4-DRAFT-UPLOAD-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 the disconnected relay-specific release required-asset gate for exact manifest coverage, -names, sizes, signature, upload state, and channel identity. Production keys/environment/seed, -publication, desktop/default consumers, tuple enablement, and merge to `main` remain disconnected. +The disconnected relay-specific release required-asset gate is locally green under +`E-M4-RELEASE-ASSETS-LOCAL-RED-001` and `E-M4-RELEASE-ASSETS-LOCAL-001`: exact manifest coverage, +names, sizes, signature binding, upload state, and stable/RC/perf identity. Exact-head all-six native +CI is required. Production keys/environment/seed, publication, desktop/default consumers, tuple +enablement, and merge to `main` remain disconnected. ## Safety status @@ -261,6 +263,10 @@ publication, desktop/default consumers, tuple enablement, and merge to `main` re `E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-001`, and exact-head execution passes on all six native jobs under `E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001`. Live protected signing remains open; no accepted production key, environment, or seed is provisioned. +- [ ] Add the relay-specific required-asset gate to the release composition. The disconnected pure + gate is locally green under `E-M4-RELEASE-ASSETS-LOCAL-RED-001` and + `E-M4-RELEASE-ASSETS-LOCAL-001`; exact-head all-six native CI is required before closing the + capability, and release/default workflow composition remains separately gated. - [ ] Embed the exact signed manifest and accepted keys in each desktop build. - [ ] Upload to a draft release, read back, re-hash, and execute the downloaded archives. A disconnected bounded upload/recovery implementation is locally green under 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 8efefa5e629..bfee6a13e56 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 relay required-asset gate — **In progress — 2026-07-15, Codex implementation owner**. The bounded draft upload/recovery capability is closed locally and on all six exact-head native jobs under E-M4-DRAFT-UPLOAD-LOCAL-RED-001, E-M4-DRAFT-UPLOAD-LOCAL-001, and E-M4-DRAFT-UPLOAD-CI-001. The active package must add a disconnected relay-specific release gate that consumes only an already verified signed manifest plus authenticated draft metadata and rejects missing, extra managed, non-uploaded, empty, wrong-size, uncovered, cross-tag/channel, or signature-disagreeing assets. Keep the existing desktop required-asset gate unchanged until composition is explicitly reviewed. Release-cut, desktop builds, publication, and every tuple stay disconnected; production/default behavior is unchanged.
-Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `b37cacecf8f1c865f09c118f274ab62302e951ad` passes all six target-native artifact jobs and the eight-test uploader suite in run 29431510990 under E-M4-DRAFT-UPLOAD-CI-001. Both Linux oldest-userland supplements and Windows x64 floor pass. Windows arm64 runtime smoke passes in 7,752.6288ms with 49,684,480-byte RSS, then its floor job retains the expected fail-closed hosted-build 26200 versus required 26100 mismatch. PR Checks 29431510764 and Golden E2E 29431510883 are green. 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 relay required-asset gate — **In progress — 2026-07-15, Codex implementation owner**. The bounded draft upload/recovery capability is closed locally and on all six exact-head native jobs under E-M4-DRAFT-UPLOAD-LOCAL-RED-001, E-M4-DRAFT-UPLOAD-LOCAL-001, and E-M4-DRAFT-UPLOAD-CI-001. The active package's purpose-named missing-module RED and locally green implementation are recorded under E-M4-RELEASE-ASSETS-LOCAL-RED-001 and E-M4-RELEASE-ASSETS-LOCAL-001. Exact-head execution on all six native Node 24 jobs is required before closing this package. The gate remains a disconnected pure metadata boundary; detached-signature byte encoding, the existing desktop required-asset gate, and workflow composition are unchanged. Release-cut, desktop builds, publication, and every tuple stay disconnected; production/default behavior is unchanged.
+Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — the locally green release-asset gate pins exact release ID/tag/channel/prerelease, derives the complete managed set from a validated already-verified signed-manifest envelope, binds opaque manifest/signature descriptors by hash and signer IDs, and rejects missing, extra managed, duplicate, non-uploaded, empty, wrong-size, uncovered, or cross-identity metadata under E-M4-RELEASE-ASSETS-LOCAL-001. The final local run passes 13 focused tests, 64 adjacent release tests, all 283 SSH-relay script tests, typecheck, lint, formatting, and diff checks. Only syntax/test execution is added to the six native artifact jobs; there is no command-line entrypoint, release API call, upload/publication caller, desktop consumer, or tuple enablement. 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.
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) @@ -113,8 +113,11 @@ same change as the work it records. is no CLI entrypoint or workflow caller, and no real release write occurred. - Active package: Work Package 3 disconnected relay required-asset gate. Begin with a purpose-named missing-module RED for exact verified-manifest coverage, signature, name, size, upload state, - managed-asset closure, tag/channel identity, and consumer disconnection. Keep the existing desktop - required-asset gate unchanged in this slice. + managed-asset closure, tag/channel identity, and consumer disconnection. The missing-module RED is + recorded under E-M4-RELEASE-ASSETS-LOCAL-RED-001. The locally green pure gate and test-only native + workflow wiring are recorded under E-M4-RELEASE-ASSETS-LOCAL-001; require exact-head all-six CI. + Keep the existing desktop required-asset gate and detached-signature byte encoding unchanged 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 @@ -11355,6 +11358,88 @@ lint` passes all 41 reliability gates, the 355-entry max-lines ratchet, bundled- - Checklist items satisfied: exact-head all-six CI gate for the disconnected draft upload/recovery capability. The broader draft upload/read-back/execution and Work Package 3 boxes remain open. +### E-M4-RELEASE-ASSETS-LOCAL-RED-001 — Relay-specific release required-asset gate is absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted purpose-named test + `config/scripts/ssh-relay-runtime-release-assets.test.mjs`; draft PR #8741. +- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. No network request, + credential, release write, signer, protected environment, SSH remote, desktop consumer, + publication, or enabled tuple. +- Command: + - `/usr/bin/time -l pnpm exec vitest run config/scripts/ssh-relay-runtime-release-assets.test.mjs` +- Result: expected FAIL, one suite / zero imported tests, 200ms Vitest and 0.97s wall. Maximum RSS + 132,022,272 bytes; peak memory footprint 95,950,176 bytes. +- Failure oracle: Vitest reports only `Cannot find module './ssh-relay-runtime-release-assets.mjs'` + at the purpose-named import. No production module existed that derived exact archive/SBOM/ + provenance/manifest/signature coverage from an already verified signed manifest and compared it + with authenticated draft metadata. +- Does not prove: implementation correctness, exact tag/channel closure, managed-asset closure, + upload state/size enforcement, signature-asset binding, native runner behavior, real GitHub + metadata, read-back byte identity, publication, desktop embedding, SSH behavior, or an enabled + tuple. +- Follow-up: implement only the disconnected metadata gate, retain the detached-signature encoding + decision and existing desktop release gate unchanged, then run the focused GREEN and broader + local regressions before adding native test-job integration. + +### E-M4-RELEASE-ASSETS-LOCAL-001 — Exact signed-manifest release coverage passes locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted `config/scripts/ssh-relay-runtime-release-assets.mjs`, its purpose-named test, + and POSIX/Windows native test-job wiring atop exact pushed head + `88a77eee91a34f8936882d096e568f499e9160e6`; draft PR #8741. +- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. In-memory manifest and + authenticated-release-metadata fixtures only. No network request, credential, release write, + signer, protected environment, SSH remote, desktop consumer, publication, or enabled tuple. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 config/scripts/ssh-relay-runtime-release-assets.test.mjs` — PASS, one file / 13 + tests in 203ms Vitest and 0.92s wall; maximum RSS 152,403,968 bytes, peak footprint 96,114,064 + 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-release-assets.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-draft-readback.test.mjs +config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs +config/scripts/ssh-relay-runtime-workflow.test.mjs +config/scripts/verify-release-required-assets.test.mjs` — PASS, 7 files / 64 tests in 988ms + Vitest and 1.72s wall; maximum RSS 135,806,976 bytes, peak footprint 95,802,744 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, 53 files / 283 tests in 9.97s Vitest and 10.94s wall; maximum RSS + 189,071,360 bytes and zero swaps. +- Static gates: `node --check` passes both new files. `pnpm run typecheck` passes. `pnpm run lint` + passes all 41 reliability gates, the 355-entry max-lines ratchet, bundled-skill verification, + localization catalog/parity, and localization coverage; all 26 warnings remain in untouched + existing files. Focused `oxfmt --check` and `git diff --check` pass. The protected Node/npm + resolver files and existing `verify-release-required-assets.mjs` plus its tests have zero diff. +- Oracle proved: the pure gate accepts only an exact release ID and stable/RC/perf tag whose + authenticated metadata remains a draft with channel-correct prerelease state. It revalidates the + already-verified signed-manifest schema, derives every tuple archive/SBOM/provenance plus exact + manifest/signature descriptors, bounds count/per-file/aggregate sizes, and requires unique + managed names. The opaque detached-signature descriptor must bind the exact manifest SHA-256 and + exact embedded signer key IDs without deciding its byte encoding. Missing, unexpected managed, + duplicate, non-uploaded, empty, wrong-size, malformed, signature-disagreeing, cross-release, + cross-tag, and cross-channel states fail closed; unrelated desktop assets remain untouched. +- Consumer-disconnection oracle: repository search finds the new production module only in its own + focused test and POSIX/Windows artifact-job syntax checks; only the test file is executed. There is + no CLI entrypoint, API fetch, release-cut/default workflow caller, upload, publication, desktop + consumer, tuple enablement, or change to the legacy SSH path. +- Does not prove: Node 24/native-runner behavior, real authenticated GitHub metadata, cryptographic + verification before envelope construction, final detached-signature byte encoding, byte + read-back, downloaded archive execution, a real release draft/write, native signing/trust, + publication, desktop embedding, SSH transfer/install, performance against legacy, or any enabled + tuple. +- Checklist items satisfied: local implementation gate for the disconnected relay-specific + required-asset capability. The broader Milestone 4 release composition/publication box remains + open. +- Follow-up: commit and push the exact package, then require syntax and 13-test execution on all six + exact-head native Node 24 jobs plus PR Checks and Golden E2E. Keep every production/default + consumer disconnected. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -11413,13 +11498,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Checkpoint and push E-M4-DRAFT-UPLOAD-CI-001. Then begin a purpose-named disconnected RED for the -relay-specific release required-asset gate: consume only an already verified signed manifest and -authenticated draft metadata; require exact manifest/signature coverage, asset names, sizes, upload -state, managed-asset closure, and stable/RC/perf tag identity. Keep the existing desktop -`verify-release-required-assets.mjs` behavior unchanged until composition is separately reviewed. -Do not add a workflow caller or perform a real release write. The protected environment, accepted -production keys, and secret remain unprovisioned, so live signing evidence stays BLOCKED. +Checkpoint and push E-M4-RELEASE-ASSETS-LOCAL-RED-001 and E-M4-RELEASE-ASSETS-LOCAL-001. Require +exact-head syntax and 13-test execution on all six native artifact jobs plus PR Checks and Golden +E2E. Do not add a workflow caller or perform a real release write; keep the existing desktop +`verify-release-required-assets.mjs` behavior and detached-signature byte encoding unchanged until +composition is separately reviewed. 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.