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.