mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 00:27:39 +00:00
build(ssh): add relay release asset gate
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -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 `
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
+9
-3
@@ -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
|
||||
|
||||
+95
-11
@@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps
|
||||
|
||||
Date created: 2026-07-14<br>
|
||||
Last updated: 2026-07-15<br>
|
||||
Current phase: Milestone 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.<br>
|
||||
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.<br>
|
||||
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.<br>
|
||||
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.<br>
|
||||
Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)<br>
|
||||
Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user