mirror of
https://github.com/stablyai/orca.git
synced 2026-10-28 08:02:54 +00:00
build(ssh): gate runtime release stages
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -143,6 +143,10 @@ jobs:
|
||||
node --check config/scripts/ssh-relay-runtime-windows-signature-verification.test.mjs
|
||||
node --check config/scripts/ssh-relay-runtime-windows-source-signature-verification.mjs
|
||||
node --check config/scripts/ssh-relay-runtime-windows-source-signature-verification.test.mjs
|
||||
node --check config/scripts/ssh-relay-runtime-release-stage-gate.mjs
|
||||
node --check config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs
|
||||
node --check config/scripts/ssh-relay-runtime-draft-recovery.mjs
|
||||
node --check config/scripts/ssh-relay-runtime-draft-recovery.test.mjs
|
||||
pnpm exec vitest run --config config/vitest.config.ts \
|
||||
config/scripts/ssh-relay-node-release-verification.test.mjs \
|
||||
config/scripts/ssh-relay-node-tar-inspection.test.mjs \
|
||||
@@ -164,6 +168,8 @@ jobs:
|
||||
config/scripts/ssh-relay-runtime-macos-signature-verification.test.mjs \
|
||||
config/scripts/ssh-relay-runtime-windows-signature-verification.test.mjs \
|
||||
config/scripts/ssh-relay-runtime-windows-source-signature-verification.test.mjs \
|
||||
config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs \
|
||||
config/scripts/ssh-relay-runtime-draft-recovery.test.mjs \
|
||||
config/scripts/ssh-relay-runtime-closure.test.mjs \
|
||||
config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \
|
||||
config/scripts/ssh-relay-runtime-reproducibility.test.mjs \
|
||||
@@ -479,6 +485,14 @@ jobs:
|
||||
if ($LASTEXITCODE -ne 0) { throw 'runtime Windows source signature verification script syntax check failed' }
|
||||
node --check config/scripts/ssh-relay-runtime-windows-source-signature-verification.test.mjs
|
||||
if ($LASTEXITCODE -ne 0) { throw 'runtime Windows source signature verification test syntax check failed' }
|
||||
node --check config/scripts/ssh-relay-runtime-release-stage-gate.mjs
|
||||
if ($LASTEXITCODE -ne 0) { throw 'runtime release stage gate syntax check failed' }
|
||||
node --check config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs
|
||||
if ($LASTEXITCODE -ne 0) { throw 'runtime release stage gate test syntax check failed' }
|
||||
node --check config/scripts/ssh-relay-runtime-draft-recovery.mjs
|
||||
if ($LASTEXITCODE -ne 0) { throw 'runtime draft recovery syntax check failed' }
|
||||
node --check config/scripts/ssh-relay-runtime-draft-recovery.test.mjs
|
||||
if ($LASTEXITCODE -ne 0) { throw 'runtime draft recovery test syntax check failed' }
|
||||
pnpm exec vitest run --config config/vitest.config.ts `
|
||||
config/scripts/ssh-relay-node-release-verification.test.mjs `
|
||||
config/scripts/ssh-relay-node-tar-inspection.test.mjs `
|
||||
@@ -500,6 +514,8 @@ jobs:
|
||||
config/scripts/ssh-relay-runtime-macos-signature-verification.test.mjs `
|
||||
config/scripts/ssh-relay-runtime-windows-signature-verification.test.mjs `
|
||||
config/scripts/ssh-relay-runtime-windows-source-signature-verification.test.mjs `
|
||||
config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs `
|
||||
config/scripts/ssh-relay-runtime-draft-recovery.test.mjs `
|
||||
config/scripts/ssh-relay-runtime-closure.test.mjs `
|
||||
config/scripts/ssh-relay-runtime-pty-smoke.test.mjs `
|
||||
config/scripts/ssh-relay-runtime-reproducibility.test.mjs `
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/u
|
||||
const TAG_PATTERN = /^v\d+\.\d+\.\d+(?:-rc\.\d+(?:\.[0-9A-Za-z]+)?)?$/u
|
||||
const COMMIT_PATTERN = /^[0-9a-f]{40}$/u
|
||||
const MANAGED_ASSET_PREFIX = 'orca-ssh-relay-runtime-'
|
||||
|
||||
function assertObject(value, label) {
|
||||
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new Error(`SSH relay runtime draft recovery ${label} must be an object`)
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAsset(value, label) {
|
||||
assertObject(value, label)
|
||||
if (typeof value.name !== 'string' || value.name.length === 0 || value.name.length > 240) {
|
||||
throw new Error(`SSH relay runtime draft recovery ${label} has an invalid name`)
|
||||
}
|
||||
if (typeof value.sha256 !== 'string' || !SHA256_PATTERN.test(value.sha256)) {
|
||||
throw new Error(`SSH relay runtime draft recovery ${label} has an invalid sha256`)
|
||||
}
|
||||
if (!Number.isSafeInteger(value.size) || value.size <= 0) {
|
||||
throw new Error(`SSH relay runtime draft recovery ${label} has an invalid size`)
|
||||
}
|
||||
return { name: value.name, sha256: value.sha256, size: value.size }
|
||||
}
|
||||
|
||||
function uniqueAssets(values, label) {
|
||||
if (!Array.isArray(values)) {
|
||||
throw new Error(`SSH relay runtime draft recovery ${label} must be an array`)
|
||||
}
|
||||
const names = new Set()
|
||||
return values.map((value) => {
|
||||
const asset = normalizeAsset(value, label)
|
||||
if (names.has(asset.name)) {
|
||||
throw new Error(
|
||||
`SSH relay runtime draft recovery ${label} has a duplicate asset: ${asset.name}`
|
||||
)
|
||||
}
|
||||
names.add(asset.name)
|
||||
return asset
|
||||
})
|
||||
}
|
||||
|
||||
function sameAsset(left, right) {
|
||||
return left.name === right.name && left.sha256 === right.sha256 && left.size === right.size
|
||||
}
|
||||
|
||||
export function planSshRelayRuntimeDraftRecovery({ tag, sourceCommit, expectedAssets, draft }) {
|
||||
if (typeof tag !== 'string' || !TAG_PATTERN.test(tag)) {
|
||||
throw new Error('SSH relay runtime draft recovery tag is invalid')
|
||||
}
|
||||
if (typeof sourceCommit !== 'string' || !COMMIT_PATTERN.test(sourceCommit)) {
|
||||
throw new Error('SSH relay runtime draft recovery source commit is invalid')
|
||||
}
|
||||
assertObject(draft, 'draft')
|
||||
if (draft.state !== 'draft') {
|
||||
// Why: recovery may fill an existing private draft, never alter already published bytes.
|
||||
throw new Error('SSH relay runtime release must remain draft during recovery')
|
||||
}
|
||||
if (draft.tag !== tag) {
|
||||
throw new Error('SSH relay runtime recovered draft tag does not match the requested release')
|
||||
}
|
||||
if (draft.sourceCommit !== sourceCommit) {
|
||||
throw new Error('SSH relay runtime recovered draft source commit does not match')
|
||||
}
|
||||
const expected = uniqueAssets(expectedAssets, 'expected assets')
|
||||
if (expected.some((asset) => !asset.name.startsWith(MANAGED_ASSET_PREFIX))) {
|
||||
throw new Error('SSH relay runtime draft recovery expected an unmanaged asset')
|
||||
}
|
||||
const expectedByName = new Map(expected.map((asset) => [asset.name, asset]))
|
||||
const existing = uniqueAssets(draft.assets, 'draft assets')
|
||||
const reusableNames = new Set()
|
||||
for (const asset of existing) {
|
||||
const expectedAsset = expectedByName.get(asset.name)
|
||||
if (!expectedAsset) {
|
||||
if (asset.name.startsWith(MANAGED_ASSET_PREFIX)) {
|
||||
throw new Error(`SSH relay runtime draft has an unexpected managed asset: ${asset.name}`)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (!sameAsset(asset, expectedAsset)) {
|
||||
throw new Error(`SSH relay runtime draft immutable bytes disagree: ${asset.name}`)
|
||||
}
|
||||
reusableNames.add(asset.name)
|
||||
}
|
||||
return {
|
||||
reusableAssets: expected.filter((asset) => reusableNames.has(asset.name)),
|
||||
uploadAssets: expected.filter((asset) => !reusableNames.has(asset.name))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { planSshRelayRuntimeDraftRecovery } from './ssh-relay-runtime-draft-recovery.mjs'
|
||||
|
||||
const TAG = 'v1.5.0-rc.1'
|
||||
const COMMIT = 'a'.repeat(40)
|
||||
|
||||
function asset(name, digit) {
|
||||
return { name, sha256: `sha256:${digit.repeat(64)}`, size: 1024 }
|
||||
}
|
||||
|
||||
function fixture() {
|
||||
const expectedAssets = [
|
||||
asset('orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.xz', '1'),
|
||||
asset('orca-ssh-relay-runtime-v1-win32-x64-b.zip', '2'),
|
||||
asset('orca-ssh-relay-runtime-manifest.json', '3'),
|
||||
asset('orca-ssh-relay-runtime-manifest.sig', '4')
|
||||
]
|
||||
return {
|
||||
tag: TAG,
|
||||
sourceCommit: COMMIT,
|
||||
expectedAssets,
|
||||
draft: { state: 'draft', tag: TAG, sourceCommit: COMMIT, assets: [] }
|
||||
}
|
||||
}
|
||||
|
||||
describe('SSH relay runtime draft recovery', () => {
|
||||
it('plans all immutable assets for a new empty draft', () => {
|
||||
const input = fixture()
|
||||
expect(planSshRelayRuntimeDraftRecovery(input)).toEqual({
|
||||
reusableAssets: [],
|
||||
uploadAssets: input.expectedAssets
|
||||
})
|
||||
})
|
||||
|
||||
it('reuses only exact immutable bytes from the same draft and source commit', () => {
|
||||
const input = fixture()
|
||||
input.draft.assets = [
|
||||
structuredClone(input.expectedAssets[0]),
|
||||
asset('orca-windows-setup.exe', '9')
|
||||
]
|
||||
|
||||
expect(planSshRelayRuntimeDraftRecovery(input)).toEqual({
|
||||
reusableAssets: [input.expectedAssets[0]],
|
||||
uploadAssets: input.expectedAssets.slice(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects changed, empty, duplicate, or unexpected managed assets', () => {
|
||||
const changed = fixture()
|
||||
changed.draft.assets = [{ ...changed.expectedAssets[0], sha256: `sha256:${'f'.repeat(64)}` }]
|
||||
expect(() => planSshRelayRuntimeDraftRecovery(changed)).toThrow(/immutable bytes disagree/i)
|
||||
|
||||
const empty = fixture()
|
||||
empty.draft.assets = [{ ...empty.expectedAssets[0], size: 0 }]
|
||||
expect(() => planSshRelayRuntimeDraftRecovery(empty)).toThrow(/invalid size/i)
|
||||
|
||||
const duplicate = fixture()
|
||||
duplicate.draft.assets = [inputAsset(duplicate), inputAsset(duplicate)]
|
||||
expect(() => planSshRelayRuntimeDraftRecovery(duplicate)).toThrow(/duplicate/i)
|
||||
|
||||
const unexpected = fixture()
|
||||
unexpected.draft.assets = [asset('orca-ssh-relay-runtime-v1-unknown.zip', '8')]
|
||||
expect(() => planSshRelayRuntimeDraftRecovery(unexpected)).toThrow(/unexpected managed asset/i)
|
||||
})
|
||||
|
||||
it.each([
|
||||
[{ state: 'published' }, 'must remain draft'],
|
||||
[{ tag: 'v1.5.0-rc.2' }, 'tag'],
|
||||
[{ sourceCommit: 'b'.repeat(40) }, 'source commit']
|
||||
])('rejects unsafe recovered draft state %o', (override, message) => {
|
||||
const input = fixture()
|
||||
Object.assign(input.draft, override)
|
||||
expect(() => planSshRelayRuntimeDraftRecovery(input)).toThrow(message)
|
||||
})
|
||||
})
|
||||
|
||||
function inputAsset(input) {
|
||||
return structuredClone(input.expectedAssets[0])
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs'
|
||||
|
||||
const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/u
|
||||
const ASSET_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,239}$/u
|
||||
const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024
|
||||
const SUCCESS_FIELDS = ['id', 'outcome', 'attempts', 'elapsedMs', 'output']
|
||||
const OUTCOMES = new Set(['success', 'failed', 'timed-out', 'cancelled', 'retry-exhausted'])
|
||||
|
||||
// Why: these mirror current release ceilings, but signing is single-attempt so approval cannot
|
||||
// accidentally authorize a second set of bytes after a timeout or service failure.
|
||||
export const SSH_RELAY_RUNTIME_RELEASE_LIMITS = Object.freeze({
|
||||
build: Object.freeze({ maxAttempts: 3, timeoutMs: 30 * 60_000 }),
|
||||
sign: Object.freeze({ maxAttempts: 1, timeoutMs: 4 * 60 * 60_000 }),
|
||||
aggregate: Object.freeze({ maxAttempts: 1, timeoutMs: 15 * 60_000 }),
|
||||
upload: Object.freeze({ maxAttempts: 3, timeoutMs: 15 * 60_000 }),
|
||||
readback: Object.freeze({ maxAttempts: 3, timeoutMs: 15 * 60_000 })
|
||||
})
|
||||
|
||||
function assertObject(value, label) {
|
||||
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new Error(`SSH relay runtime release ${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 ${label} has unexpected or missing fields`)
|
||||
}
|
||||
}
|
||||
|
||||
function assertSha256(value, label) {
|
||||
if (typeof value !== 'string' || !SHA256_PATTERN.test(value)) {
|
||||
throw new Error(`SSH relay runtime release ${label} must be a lowercase sha256 identity`)
|
||||
}
|
||||
}
|
||||
|
||||
function assertAssetName(value, label) {
|
||||
if (typeof value !== 'string' || !ASSET_NAME_PATTERN.test(value)) {
|
||||
throw new Error(`SSH relay runtime release ${label} has an invalid asset name`)
|
||||
}
|
||||
}
|
||||
|
||||
function assertSize(value, label, maximum = MAX_ARCHIVE_BYTES) {
|
||||
if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) {
|
||||
throw new Error(`SSH relay runtime release ${label} has an invalid size`)
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeRuntimeAsset(value, tupleId, label) {
|
||||
assertExactFields(value, ['tupleId', 'name', 'sha256', 'contentId', 'size'], label)
|
||||
if (value.tupleId !== tupleId) {
|
||||
throw new Error(`SSH relay runtime release ${label} has the wrong tuple`)
|
||||
}
|
||||
assertAssetName(value.name, label)
|
||||
assertSha256(value.sha256, `${label} sha256`)
|
||||
assertSha256(value.contentId, `${label} content ID`)
|
||||
assertSize(value.size, label)
|
||||
return { ...value }
|
||||
}
|
||||
|
||||
function normalizeReleaseFile(value, expectedName, label, maximum) {
|
||||
assertExactFields(value, ['name', 'sha256', 'size'], label)
|
||||
if (value.name !== expectedName) {
|
||||
throw new Error(`SSH relay runtime release ${label} has an unexpected name`)
|
||||
}
|
||||
assertSha256(value.sha256, `${label} sha256`)
|
||||
assertSize(value.size, label, maximum)
|
||||
return { ...value }
|
||||
}
|
||||
|
||||
function stageKind(id) {
|
||||
if (id.startsWith('build:')) {
|
||||
return 'build'
|
||||
}
|
||||
if (id.startsWith('sign:')) {
|
||||
return 'sign'
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
function failureMessage(stage) {
|
||||
if (stage.outcome === 'timed-out') {
|
||||
return `${stage.id} timed out`
|
||||
}
|
||||
if (stage.outcome === 'retry-exhausted') {
|
||||
return `${stage.id} reached retry exhaustion`
|
||||
}
|
||||
return `${stage.id} ${stage.outcome}`
|
||||
}
|
||||
|
||||
function normalizeSuccessfulStage(stage) {
|
||||
assertExactFields(stage, SUCCESS_FIELDS, 'stage result')
|
||||
if (typeof stage.id !== 'string' || stage.id.length === 0) {
|
||||
throw new Error('SSH relay runtime release stage ID is invalid')
|
||||
}
|
||||
if (!OUTCOMES.has(stage.outcome)) {
|
||||
throw new Error(`SSH relay runtime release ${stage.id} has an unknown outcome`)
|
||||
}
|
||||
const limits = SSH_RELAY_RUNTIME_RELEASE_LIMITS[stageKind(stage.id)]
|
||||
if (!limits) {
|
||||
throw new Error(`SSH relay runtime release has an unexpected stage: ${stage.id}`)
|
||||
}
|
||||
if (
|
||||
!Number.isSafeInteger(stage.attempts) ||
|
||||
stage.attempts < 1 ||
|
||||
stage.attempts > limits.maxAttempts
|
||||
) {
|
||||
throw new Error(`SSH relay runtime release ${stage.id} exceeds its attempt budget`)
|
||||
}
|
||||
if (
|
||||
!Number.isSafeInteger(stage.elapsedMs) ||
|
||||
stage.elapsedMs < 0 ||
|
||||
stage.elapsedMs > limits.timeoutMs
|
||||
) {
|
||||
throw new Error(`SSH relay runtime release ${stage.id} exceeds its time budget`)
|
||||
}
|
||||
if (stage.outcome !== 'success') {
|
||||
throw new Error(`SSH relay runtime release ${failureMessage(stage)}`)
|
||||
}
|
||||
return stage
|
||||
}
|
||||
|
||||
function normalizeTupleIds(tupleIds, label, { allowEmpty = false } = {}) {
|
||||
if (!Array.isArray(tupleIds) || (!allowEmpty && tupleIds.length === 0)) {
|
||||
throw new Error(
|
||||
`SSH relay runtime release ${label} must be ${allowEmpty ? 'an array' : 'a non-empty array'}`
|
||||
)
|
||||
}
|
||||
const seen = new Set()
|
||||
return tupleIds.map((tupleId) => {
|
||||
if (!Object.hasOwn(sshRelayRuntimeCompatibility, tupleId)) {
|
||||
throw new Error(`SSH relay runtime release ${label} contains an unknown tuple: ${tupleId}`)
|
||||
}
|
||||
if (seen.has(tupleId)) {
|
||||
throw new Error(`SSH relay runtime release ${label} contains a duplicate tuple: ${tupleId}`)
|
||||
}
|
||||
seen.add(tupleId)
|
||||
return tupleId
|
||||
})
|
||||
}
|
||||
|
||||
function sameAssets(actual, expected) {
|
||||
return JSON.stringify(actual) === JSON.stringify(expected)
|
||||
}
|
||||
|
||||
function assertAssetChain(actual, expected, label) {
|
||||
if (!Array.isArray(actual) || !sameAssets(actual, expected)) {
|
||||
throw new Error(
|
||||
`SSH relay runtime release ${label} assets disagree with prior immutable output`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function collectStages(stages, expectedIds) {
|
||||
if (!Array.isArray(stages)) {
|
||||
throw new Error('SSH relay runtime release stages must be an array')
|
||||
}
|
||||
const expected = new Set(expectedIds)
|
||||
const byId = new Map()
|
||||
for (const stage of stages) {
|
||||
assertObject(stage, 'stage result')
|
||||
if (typeof stage.id !== 'string' || !expected.has(stage.id)) {
|
||||
throw new Error(`SSH relay runtime release has an unexpected stage: ${String(stage.id)}`)
|
||||
}
|
||||
if (byId.has(stage.id)) {
|
||||
throw new Error(`SSH relay runtime release has a duplicate stage: ${stage.id}`)
|
||||
}
|
||||
byId.set(stage.id, normalizeSuccessfulStage(stage))
|
||||
}
|
||||
for (const id of expectedIds) {
|
||||
if (!byId.has(id)) {
|
||||
throw new Error(`SSH relay runtime release is missing required stage: ${id}`)
|
||||
}
|
||||
}
|
||||
return byId
|
||||
}
|
||||
|
||||
function expectedSigningTuples(candidateTupleIds) {
|
||||
return candidateTupleIds.filter(
|
||||
(tupleId) => tupleId.startsWith('darwin-') || tupleId.startsWith('win32-')
|
||||
)
|
||||
}
|
||||
|
||||
function assertUniqueAssetNames(assets) {
|
||||
const names = new Set()
|
||||
for (const asset of assets) {
|
||||
if (names.has(asset.name)) {
|
||||
throw new Error(`SSH relay runtime release has a duplicate asset name: ${asset.name}`)
|
||||
}
|
||||
names.add(asset.name)
|
||||
}
|
||||
}
|
||||
|
||||
export function evaluateSshRelayRuntimeReleaseStages({
|
||||
candidateTupleIds,
|
||||
signingTupleIds,
|
||||
stages
|
||||
}) {
|
||||
const candidates = normalizeTupleIds(candidateTupleIds, 'candidate tuples')
|
||||
const signing = normalizeTupleIds(signingTupleIds, 'signing tuples', { allowEmpty: true })
|
||||
const requiredSigning = expectedSigningTuples(candidates)
|
||||
if (!sameAssets(signing, requiredSigning)) {
|
||||
// Why: macOS and Windows candidates cannot bypass the returned native-signing boundary.
|
||||
throw new Error('SSH relay runtime release signing tuples do not match platform policy')
|
||||
}
|
||||
const expectedStageIds = [
|
||||
...candidates.map((tupleId) => `build:${tupleId}`),
|
||||
...signing.map((tupleId) => `sign:${tupleId}`),
|
||||
'aggregate',
|
||||
'upload',
|
||||
'readback'
|
||||
]
|
||||
const stageResults = collectStages(stages, expectedStageIds)
|
||||
const builds = new Map()
|
||||
for (const tupleId of candidates) {
|
||||
builds.set(
|
||||
tupleId,
|
||||
normalizeRuntimeAsset(
|
||||
stageResults.get(`build:${tupleId}`).output,
|
||||
tupleId,
|
||||
`build:${tupleId} output`
|
||||
)
|
||||
)
|
||||
}
|
||||
const finalByTuple = new Map(builds)
|
||||
for (const tupleId of signing) {
|
||||
const output = stageResults.get(`sign:${tupleId}`).output
|
||||
assertExactFields(output, ['approval', 'inputSha256', 'asset'], `sign:${tupleId} output`)
|
||||
if (output.approval !== 'approved') {
|
||||
const reason =
|
||||
output.approval === 'denied'
|
||||
? 'approval was denied'
|
||||
: output.approval === 'timed-out'
|
||||
? 'approval timed out'
|
||||
: 'approval is absent'
|
||||
throw new Error(`SSH relay runtime release sign:${tupleId} ${reason}`)
|
||||
}
|
||||
if (output.inputSha256 !== builds.get(tupleId).sha256) {
|
||||
throw new Error(
|
||||
`SSH relay runtime release sign:${tupleId} unsigned input disagrees with build output`
|
||||
)
|
||||
}
|
||||
const signed = normalizeRuntimeAsset(output.asset, tupleId, `sign:${tupleId} returned asset`)
|
||||
if (
|
||||
signed.sha256 === output.inputSha256 ||
|
||||
signed.contentId === builds.get(tupleId).contentId
|
||||
) {
|
||||
throw new Error(
|
||||
`SSH relay runtime release sign:${tupleId} did not produce a new immutable identity`
|
||||
)
|
||||
}
|
||||
finalByTuple.set(tupleId, signed)
|
||||
}
|
||||
const finalRuntimeAssets = candidates.map((tupleId) => finalByTuple.get(tupleId))
|
||||
assertUniqueAssetNames(finalRuntimeAssets)
|
||||
|
||||
const aggregate = stageResults.get('aggregate').output
|
||||
assertExactFields(aggregate, ['inputAssets', 'manifest', 'signature'], 'aggregate output')
|
||||
assertAssetChain(aggregate.inputAssets, finalRuntimeAssets, 'aggregate input')
|
||||
const manifest = normalizeReleaseFile(
|
||||
aggregate.manifest,
|
||||
'orca-ssh-relay-runtime-manifest.json',
|
||||
'aggregate manifest',
|
||||
1024 * 1024
|
||||
)
|
||||
const signature = normalizeReleaseFile(
|
||||
aggregate.signature,
|
||||
'orca-ssh-relay-runtime-manifest.sig',
|
||||
'aggregate manifest signature',
|
||||
4096
|
||||
)
|
||||
const releaseAssets = [...finalRuntimeAssets, manifest, signature]
|
||||
assertUniqueAssetNames(releaseAssets)
|
||||
|
||||
const upload = stageResults.get('upload').output
|
||||
assertExactFields(upload, ['inputAssets', 'uploadedAssets'], 'upload output')
|
||||
assertAssetChain(upload.inputAssets, releaseAssets, 'upload input')
|
||||
assertAssetChain(upload.uploadedAssets, releaseAssets, 'uploaded')
|
||||
const readback = stageResults.get('readback').output
|
||||
assertExactFields(readback, ['inputAssets', 'downloadedAssets'], 'readback output')
|
||||
assertAssetChain(readback.inputAssets, releaseAssets, 'readback input')
|
||||
assertAssetChain(readback.downloadedAssets, releaseAssets, 'readback downloaded')
|
||||
|
||||
return { candidateTupleIds: candidates, finalRuntimeAssets, releaseAssets, publishable: true }
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
SSH_RELAY_RUNTIME_RELEASE_LIMITS,
|
||||
evaluateSshRelayRuntimeReleaseStages
|
||||
} from './ssh-relay-runtime-release-stage-gate.mjs'
|
||||
|
||||
const LINUX = 'linux-x64-glibc'
|
||||
const WINDOWS = 'win32-x64'
|
||||
const SHA = {
|
||||
linux: `sha256:${'1'.repeat(64)}`,
|
||||
windowsUnsigned: `sha256:${'2'.repeat(64)}`,
|
||||
windowsSigned: `sha256:${'3'.repeat(64)}`,
|
||||
manifest: `sha256:${'4'.repeat(64)}`,
|
||||
signature: `sha256:${'5'.repeat(64)}`
|
||||
}
|
||||
|
||||
function asset(tupleId, name, sha256, contentDigit) {
|
||||
return {
|
||||
tupleId,
|
||||
name,
|
||||
sha256,
|
||||
contentId: `sha256:${contentDigit.repeat(64)}`,
|
||||
size: 1024
|
||||
}
|
||||
}
|
||||
|
||||
function stage(id, output, overrides = {}) {
|
||||
return {
|
||||
id,
|
||||
outcome: 'success',
|
||||
attempts: 1,
|
||||
elapsedMs: 1_000,
|
||||
output,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function successfulFixture() {
|
||||
const linux = asset(LINUX, 'orca-ssh-relay-runtime-linux.tar.xz', SHA.linux, 'a')
|
||||
const windowsUnsigned = asset(
|
||||
WINDOWS,
|
||||
'orca-ssh-relay-runtime-windows-unsigned.zip',
|
||||
SHA.windowsUnsigned,
|
||||
'b'
|
||||
)
|
||||
const windowsSigned = asset(
|
||||
WINDOWS,
|
||||
'orca-ssh-relay-runtime-windows-signed.zip',
|
||||
SHA.windowsSigned,
|
||||
'c'
|
||||
)
|
||||
const manifest = {
|
||||
name: 'orca-ssh-relay-runtime-manifest.json',
|
||||
sha256: SHA.manifest,
|
||||
size: 2048
|
||||
}
|
||||
const signature = {
|
||||
name: 'orca-ssh-relay-runtime-manifest.sig',
|
||||
sha256: SHA.signature,
|
||||
size: 64
|
||||
}
|
||||
const finalRuntimeAssets = [linux, windowsSigned]
|
||||
const releaseAssets = [...finalRuntimeAssets, manifest, signature]
|
||||
return {
|
||||
candidateTupleIds: [LINUX, WINDOWS],
|
||||
signingTupleIds: [WINDOWS],
|
||||
stages: [
|
||||
stage(`build:${LINUX}`, linux),
|
||||
stage(`build:${WINDOWS}`, windowsUnsigned),
|
||||
stage(`sign:${WINDOWS}`, {
|
||||
approval: 'approved',
|
||||
inputSha256: windowsUnsigned.sha256,
|
||||
asset: windowsSigned
|
||||
}),
|
||||
stage('aggregate', {
|
||||
inputAssets: structuredClone(finalRuntimeAssets),
|
||||
manifest: structuredClone(manifest),
|
||||
signature: structuredClone(signature)
|
||||
}),
|
||||
stage('upload', {
|
||||
inputAssets: structuredClone(releaseAssets),
|
||||
uploadedAssets: structuredClone(releaseAssets)
|
||||
}),
|
||||
stage('readback', {
|
||||
inputAssets: structuredClone(releaseAssets),
|
||||
downloadedAssets: structuredClone(releaseAssets)
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function stageById(fixture, id) {
|
||||
return fixture.stages.find((entry) => entry.id === id)
|
||||
}
|
||||
|
||||
describe('SSH relay runtime release stage gate', () => {
|
||||
it('pins finite release-stage retry and timeout ceilings', () => {
|
||||
expect(SSH_RELAY_RUNTIME_RELEASE_LIMITS).toEqual({
|
||||
build: { maxAttempts: 3, timeoutMs: 30 * 60_000 },
|
||||
sign: { maxAttempts: 1, timeoutMs: 4 * 60 * 60_000 },
|
||||
aggregate: { maxAttempts: 1, timeoutMs: 15 * 60_000 },
|
||||
upload: { maxAttempts: 3, timeoutMs: 15 * 60_000 },
|
||||
readback: { maxAttempts: 3, timeoutMs: 15 * 60_000 }
|
||||
})
|
||||
})
|
||||
|
||||
it('accepts only a complete immutable build-sign-aggregate-upload-readback chain', () => {
|
||||
const result = evaluateSshRelayRuntimeReleaseStages(successfulFixture())
|
||||
|
||||
expect(result.candidateTupleIds).toEqual([LINUX, WINDOWS])
|
||||
expect(result.finalRuntimeAssets.map((entry) => entry.tupleId)).toEqual([LINUX, WINDOWS])
|
||||
expect(result.releaseAssets.map((entry) => entry.name)).toEqual([
|
||||
'orca-ssh-relay-runtime-linux.tar.xz',
|
||||
'orca-ssh-relay-runtime-windows-signed.zip',
|
||||
'orca-ssh-relay-runtime-manifest.json',
|
||||
'orca-ssh-relay-runtime-manifest.sig'
|
||||
])
|
||||
expect(result.publishable).toBe(true)
|
||||
})
|
||||
|
||||
it('does not invent a signing stage for a Linux-only candidate set', () => {
|
||||
const fixture = successfulFixture()
|
||||
fixture.candidateTupleIds = [LINUX]
|
||||
fixture.signingTupleIds = []
|
||||
fixture.stages = fixture.stages.filter(
|
||||
(entry) => entry.id !== `build:${WINDOWS}` && entry.id !== `sign:${WINDOWS}`
|
||||
)
|
||||
const linux = structuredClone(stageById(fixture, `build:${LINUX}`).output)
|
||||
const aggregate = stageById(fixture, 'aggregate').output
|
||||
aggregate.inputAssets = [structuredClone(linux)]
|
||||
const releaseAssets = [linux, aggregate.manifest, aggregate.signature]
|
||||
Object.assign(stageById(fixture, 'upload').output, {
|
||||
inputAssets: structuredClone(releaseAssets),
|
||||
uploadedAssets: structuredClone(releaseAssets)
|
||||
})
|
||||
Object.assign(stageById(fixture, 'readback').output, {
|
||||
inputAssets: structuredClone(releaseAssets),
|
||||
downloadedAssets: structuredClone(releaseAssets)
|
||||
})
|
||||
|
||||
expect(evaluateSshRelayRuntimeReleaseStages(fixture).finalRuntimeAssets).toEqual([linux])
|
||||
})
|
||||
|
||||
it('requires signing for every macOS and Windows candidate and forbids it for Linux', () => {
|
||||
const missing = successfulFixture()
|
||||
missing.signingTupleIds = []
|
||||
expect(() => evaluateSshRelayRuntimeReleaseStages(missing)).toThrow(/platform policy/i)
|
||||
|
||||
const extra = successfulFixture()
|
||||
extra.signingTupleIds = [WINDOWS, LINUX]
|
||||
expect(() => evaluateSshRelayRuntimeReleaseStages(extra)).toThrow(/platform policy/i)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['timed-out', 'timed out'],
|
||||
['retry-exhausted', 'retry exhaustion'],
|
||||
['failed', 'failed'],
|
||||
['cancelled', 'cancelled']
|
||||
])('fails closed when a build is %s', (outcome, message) => {
|
||||
const fixture = successfulFixture()
|
||||
Object.assign(stageById(fixture, `build:${LINUX}`), { outcome, output: undefined })
|
||||
|
||||
expect(() => evaluateSshRelayRuntimeReleaseStages(fixture)).toThrow(message)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['pending', 'approval is absent'],
|
||||
['denied', 'approval was denied'],
|
||||
['timed-out', 'approval timed out']
|
||||
])('fails closed when signing approval is %s', (approval, message) => {
|
||||
const fixture = successfulFixture()
|
||||
stageById(fixture, `sign:${WINDOWS}`).output.approval = approval
|
||||
|
||||
expect(() => evaluateSshRelayRuntimeReleaseStages(fixture)).toThrow(message)
|
||||
})
|
||||
|
||||
it('rejects signing failure, unsigned input drift, and incomplete returned output', () => {
|
||||
const failed = successfulFixture()
|
||||
Object.assign(stageById(failed, `sign:${WINDOWS}`), { outcome: 'failed', output: undefined })
|
||||
expect(() => evaluateSshRelayRuntimeReleaseStages(failed)).toThrow(/sign:win32-x64 failed/i)
|
||||
|
||||
const drifted = successfulFixture()
|
||||
stageById(drifted, `sign:${WINDOWS}`).output.inputSha256 = SHA.linux
|
||||
expect(() => evaluateSshRelayRuntimeReleaseStages(drifted)).toThrow(/unsigned input/i)
|
||||
|
||||
const incomplete = successfulFixture()
|
||||
delete stageById(incomplete, `sign:${WINDOWS}`).output.asset.sha256
|
||||
expect(() => evaluateSshRelayRuntimeReleaseStages(incomplete)).toThrow(/missing fields|sha256/i)
|
||||
})
|
||||
|
||||
it('enforces bounded attempts and elapsed time even for success results', () => {
|
||||
const retried = successfulFixture()
|
||||
stageById(retried, 'upload').attempts = SSH_RELAY_RUNTIME_RELEASE_LIMITS.upload.maxAttempts + 1
|
||||
expect(() => evaluateSshRelayRuntimeReleaseStages(retried)).toThrow(/attempt budget/i)
|
||||
|
||||
const slow = successfulFixture()
|
||||
stageById(slow, 'aggregate').elapsedMs =
|
||||
SSH_RELAY_RUNTIME_RELEASE_LIMITS.aggregate.timeoutMs + 1
|
||||
expect(() => evaluateSshRelayRuntimeReleaseStages(slow)).toThrow(/time budget/i)
|
||||
})
|
||||
|
||||
it('rejects missing, duplicate, and unexpected stage results', () => {
|
||||
const missing = successfulFixture()
|
||||
missing.stages = missing.stages.filter((entry) => entry.id !== 'readback')
|
||||
expect(() => evaluateSshRelayRuntimeReleaseStages(missing)).toThrow(/missing.*readback/i)
|
||||
|
||||
const duplicate = successfulFixture()
|
||||
duplicate.stages.push(structuredClone(duplicate.stages[0]))
|
||||
expect(() => evaluateSshRelayRuntimeReleaseStages(duplicate)).toThrow(/duplicate/i)
|
||||
|
||||
const unexpected = successfulFixture()
|
||||
unexpected.stages.push(stage('sign:linux-x64-glibc', {}))
|
||||
expect(() => evaluateSshRelayRuntimeReleaseStages(unexpected)).toThrow(/unexpected/i)
|
||||
})
|
||||
|
||||
it('rejects aggregate, upload, or read-back byte drift', () => {
|
||||
for (const [id, field] of [
|
||||
['aggregate', 'inputAssets'],
|
||||
['upload', 'uploadedAssets'],
|
||||
['readback', 'downloadedAssets']
|
||||
]) {
|
||||
const fixture = successfulFixture()
|
||||
stageById(fixture, id).output[field][0].sha256 = SHA.windowsUnsigned
|
||||
expect(() => evaluateSshRelayRuntimeReleaseStages(fixture)).toThrow(/asset.*disagree/i)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -95,9 +95,15 @@ describe('SSH relay runtime artifact workflow', () => {
|
||||
expect(
|
||||
source.match(/ssh-relay-runtime-windows-signature-verification\.test\.mjs/g)
|
||||
).toHaveLength(4)
|
||||
expect(
|
||||
source.match(/ssh-relay-runtime-windows-source-signature-verification\.test\.mjs/g)
|
||||
).toHaveLength(4)
|
||||
for (const moduleName of [
|
||||
'windows-source-signature-verification',
|
||||
'release-stage-gate',
|
||||
'draft-recovery'
|
||||
]) {
|
||||
const script = `ssh-relay-runtime-${moduleName}`
|
||||
expect(source.split(`${script}.test.mjs`)).toHaveLength(5)
|
||||
expect(source.split(`node --check config/scripts/${script}.mjs`)).toHaveLength(3)
|
||||
}
|
||||
expect(
|
||||
source.match(/node --check config\/scripts\/ssh-relay-runtime-native-signing-plan\.mjs/g)
|
||||
).toHaveLength(2)
|
||||
@@ -128,11 +134,6 @@ describe('SSH relay runtime artifact workflow', () => {
|
||||
/node --check config\/scripts\/ssh-relay-runtime-windows-signature-verification\.mjs/g
|
||||
)
|
||||
).toHaveLength(2)
|
||||
expect(
|
||||
source.match(
|
||||
/node --check config\/scripts\/ssh-relay-runtime-windows-source-signature-verification\.mjs/g
|
||||
)
|
||||
).toHaveLength(2)
|
||||
expect(
|
||||
source.match(/node --check config\/scripts\/ssh-relay-runtime-closure\.mjs/g)
|
||||
).toHaveLength(2)
|
||||
|
||||
+15
-4
@@ -1,6 +1,6 @@
|
||||
# SSH Relay Runtime Distribution — Short Implementation Checklist
|
||||
|
||||
Last updated: 2026-07-14
|
||||
Last updated: 2026-07-15
|
||||
|
||||
Use this file to track the project. The
|
||||
[detailed evidence ledger](./2026-07-14-ssh-relay-github-release-implementation-checklist.md)
|
||||
@@ -48,9 +48,16 @@ complete a box.
|
||||
[29387668264](https://github.com/stablyai/orca/actions/runs/29387668264): credential-free Windows
|
||||
final-tree-first Authenticode policy contracts execute under Node 24 on all six native jobs.
|
||||
This is contract proof, not proof of real SignPath returns or native trust.
|
||||
- Proven locally: target-native official-Node and preserved-upstream Windows source-signature
|
||||
verification contracts. Exact x64/arm64 `Valid` signer reports are the active CI gate; real
|
||||
SignPath returns and missing oldest-floor snapshots remain separately gated.
|
||||
- Proven locally and on native x64/arm64 Windows jobs in exact-head run
|
||||
[29388734922](https://github.com/stablyai/orca/actions/runs/29388734922): official Node and both
|
||||
preserved Microsoft ConPTY files report `Valid`; downloaded reports match the exact identity,
|
||||
hashes, subjects, thumbprints, and signing-stage selection
|
||||
(`E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001`). PR Checks
|
||||
[29388734935](https://github.com/stablyai/orca/actions/runs/29388734935) and Golden E2E
|
||||
[29388734914](https://github.com/stablyai/orca/actions/runs/29388734914) are green at `be32653a7`.
|
||||
The artifact workflow remains red only for the declared Windows arm64 floor mismatch (hosted
|
||||
build 26200 versus required 26100). Real SignPath returns and missing oldest-floor snapshots
|
||||
remain separately gated.
|
||||
- Next external proof: kernel 4.18, macOS 13.5, Windows arm64 build 26100, and native signing/trust.
|
||||
- No tuple is enabled; every SSH transfer/runtime and rollout cell remains open.
|
||||
|
||||
@@ -87,6 +94,10 @@ complete a box.
|
||||
|
||||
### WP3 — Release build and signing
|
||||
|
||||
- [ ] **In progress — 2026-07-15, Codex implementation owner:** add disconnected credential-free
|
||||
fail-closed release/signing DAG contracts before production workflow wiring. Local red/green,
|
||||
full SSH-relay, type, lint, formatting, and native-job wiring gates pass under
|
||||
`E-M4-RELEASE-DAG-LOCAL-001`; exact-head CI is next.
|
||||
- [ ] Add target-native runtime jobs as desktop release prerequisites.
|
||||
- [ ] Add native signing jobs; hash only the returned signed bytes.
|
||||
- [ ] Add a fail-closed aggregate and immutable manifest-signing job.
|
||||
|
||||
+212
-27
@@ -7,9 +7,9 @@ This file is the detailed evidence ledger. Use the tracker above for project sta
|
||||
work; keep exact commands, runner identities, hashes, metrics, and residual gaps here.
|
||||
|
||||
Date created: 2026-07-14<br>
|
||||
Last updated: 2026-07-14<br>
|
||||
Current phase: Milestone 3 / Work Package 2 oldest-supported-baseline and native-trust proof — **In progress — 2026-07-14, Codex implementation owner**; exact-head run [29373507297](https://github.com/stablyai/orca/actions/runs/29373507297) passes all six target-native build, smoke, exact clean-build equality, upload, SBOM, license, provenance, runner/toolchain, and prohibited-content cells, and direct inspection of every downloaded payload passes exact archive/subject hashes, archive-scoped SPDX identity, one-owner-per-file, dependency, commit/run/builder/runner, tool-version/hash, and closure assertions (E-M3-METADATA-CI-001); Windows x64/arm64 record strict `MSVC 14.44.35207` identities and distinct exact linker SHA-256 values despite the Git-for-Windows PATH collision; the all-six metadata/provenance gate is closed, while oldest-baseline execution, native signing/trust, cross-family remotes, and measured legacy baselines remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled and no artifact is published<br>
|
||||
Session checkpoint: **In progress — 2026-07-14, Codex implementation owner** — credential-free macOS post-sign strict-codesign and signer-policy verification contracts are locally green under E-M3-MACOS-SIGNATURE-LOCAL-001 and pass under Node 24 on all six target-native jobs in exact-head run 29386372366 under E-M3-MACOS-SIGNATURE-CI-001. E-M3-WINDOWS-SIGNATURE-LOCAL-RED-001 proves the final Windows Authenticode policy verifier was absent; its final-tree-first implementation and POSIX/PowerShell workflow contracts pass locally under E-M3-WINDOWS-SIGNATURE-LOCAL-001 and under Node 24 on all six target-native jobs in exact-head run 29387668264 under E-M3-WINDOWS-SIGNATURE-CI-001. Target-native official-Node and preserved-upstream source-signature verification is implemented at `6ef2943de` and locally green under E-M3-WINDOWS-SOURCE-SIGNATURE-LOCAL-001; exact x64/arm64 `Valid` reports are the active gate. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, and native trust remain separately gated. Nothing is published or enabled, and legacy remains the production default.<br>
|
||||
Last updated: 2026-07-15<br>
|
||||
Current phase: Milestone 4 / Work Package 3 credential-free release/signing DAG failure contracts — **In progress — 2026-07-15, Codex implementation owner**. Work Package 2's target-native Windows official-Node and preserved-upstream source-signature gate is closed under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001. Exact-floor gaps and real native signing/trust remain open external gates, so the active package is limited to disconnected fail-closed release-DAG contracts and tests; it may not publish, connect desktop consumers, or enable a tuple. Production/default behavior is unchanged, no bundled-runtime path is enabled, and no artifact is published.<br>
|
||||
Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact-head artifact jobs 87267322867 and 87267322870 in run [29388734922](https://github.com/stablyai/orca/actions/runs/29388734922) passed target-native Windows source-signature verification and uploaded independently inspected reports under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001. PR Checks [29388734935](https://github.com/stablyai/orca/actions/runs/29388734935) and Golden E2E [29388734914](https://github.com/stablyai/orca/actions/runs/29388734914) are green at exact head `be32653a7`; the artifact run's only failure is the retained Windows arm64 build-26200-versus-26100 floor gate. E-M4-RELEASE-DAG-LOCAL-RED-001 proves the disconnected release-stage and draft-recovery modules were absent; their implementation and native-workflow test wiring now pass locally under E-M4-RELEASE-DAG-LOCAL-001. Exact-head CI for the new package is the active gate. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, and native trust remain separately gated. Nothing is published or enabled, and legacy remains the production default.<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)
|
||||
|
||||
@@ -64,7 +64,16 @@ same change as the work it records.
|
||||
E-M2-CONTRACT-001, and E-M2-CI-001. They remain isolated in stacked draft PR
|
||||
[#8728](https://github.com/stablyai/orca/pull/8728) at implementation commit `b9d80a4cb`; no
|
||||
deploy/resolver call site is connected and no tuple is enabled.
|
||||
- Active package: Work Package 2 target-native runtime assembly, archive inspection, executable
|
||||
- Active package: Work Package 3 credential-free release/signing DAG failure contracts. The package
|
||||
must model fail-closed build, approval, signing, aggregate, upload, read-back, and recovered-draft
|
||||
outcomes without credentials, production workflow wiring, publication, desktop consumers, or
|
||||
tuple enablement. No Milestone 4 box is checked until an implemented contract and exact tests exist.
|
||||
- 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
|
||||
29388734935 and Golden E2E 29388734914 are green. Exact floors and real native signing/trust remain
|
||||
open, so Work Package 2 as a whole is not complete and no tuple is enabled.
|
||||
- Historical Work Package 2 progression: target-native runtime assembly, archive inspection, executable
|
||||
smoke, SBOM, and provenance only. The Windows x64/arm64 artifact candidate and portability
|
||||
corrections are implemented through `b6903b220`, with bounded smoke diagnostics in `3aab5aff8`,
|
||||
explicit PTY smoke settlement in `ddf28eb8d`, and bounded active-resource diagnostics in
|
||||
@@ -86,7 +95,7 @@ same change as the work it records.
|
||||
window, exited normally, and uploaded unpublished evidence (E-M3-WINDOWS-CI-001). This package
|
||||
may produce test artifacts but must not publish, resolve, transfer, install, launch, or enable
|
||||
them.
|
||||
- Active evidence gate: the immutable Node v24.18.0 contract, pinned release key, bounded verifier,
|
||||
- Historical evidence progression: the immutable Node v24.18.0 contract, pinned release key, bounded verifier,
|
||||
and artifact-only CLI are locally green under E-M3-NODE-RED-001 and E-M3-NODE-PROVENANCE-001.
|
||||
E-M3-RUNTIME-LOCAL-001 additionally proves one unpublished Linux arm64 glibc assembly, exact-tree
|
||||
archive inspection, deterministic repack, bundled Node 24.18.0 execution, real patched PTY, and
|
||||
@@ -176,12 +185,11 @@ same change as the work it records.
|
||||
per-target opt-in selects bundled-preferred behavior, and implementing the setting does not
|
||||
authorize default-on rollout or legacy removal (E-M1-ROLLOUT-DECISION-001).
|
||||
- Legacy fallback removal: not authorized.
|
||||
- Next required action: implement purpose-named oldest-supported-baseline verification and its
|
||||
artifact-only workflow cells. Use GitHub-hosted native runners where they satisfy the declared
|
||||
floor, add static libc/libstdc++/kernel eligibility oracles where execution alone is insufficient,
|
||||
and keep unavailable macOS/Windows snapshot and native-trust cells explicitly open rather than
|
||||
substituting newer runners. Preserve the artifact-only boundary, legacy/default path, and every
|
||||
tuple's disabled state.
|
||||
- Next required action: implement and prove the disconnected credential-free release/signing DAG
|
||||
failure contract, including timeout, retry exhaustion, absent/denied approval, signing failure,
|
||||
incomplete output, and recovered-draft cases. Keep real native-signing credentials, endpoint trust,
|
||||
missing exact-floor snapshots, production workflow wiring, publication, desktop consumers, and
|
||||
every tuple's enabled state outside this slice.
|
||||
|
||||
## Non-Negotiable Invariants
|
||||
|
||||
@@ -1445,16 +1453,16 @@ Baseline measurements must be captured before product behavior changes.
|
||||
|
||||
Update status and evidence as work begins. Do not combine these into one large behavior switch.
|
||||
|
||||
| Work package | Scope | Default behavior change | Status | PR/evidence |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------ | --------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| 0. #8450 legacy fix | Coherent Node/npm selection and live repro | Fixes legacy selection only | Complete and CI-green in draft PR #8724 | E-M0-UNIT-002, E-M0-LIVE-002, E-M0-STATIC-002, E-M0-PR-001, E-M0-CI-001 |
|
||||
| 1. Contract and selectors | Manifest schema, identity, platform/libc selection, hostile inputs | None | Complete and CI-green in draft PR #8728 | `b9d80a4cb`; E-M2-RED-001, E-M2-CONTRACT-001, E-M2-CI-001 |
|
||||
| 2. Runtime builds | Per-tuple assembly, native smoke, SBOM/provenance/signing | None | Draft PR #8741; native closure green; metadata local; trust pending | `26bbd7b67`; E-M3-RUNTIME-CLOSURE-CI-001, E-M3-METADATA-LOCAL-001 |
|
||||
| 3. Release publication | Prerequisite DAG, embedded manifest, draft upload/read-back gates | Asset-only | Not started | — |
|
||||
| 4. Desktop resolver/cache | Verified download, extraction, cache, offline behavior | None/forced mode only | Not started | — |
|
||||
| 5. Transfer/install | Bounded transports, structured sentinel, bundled launch behind per-target Beta/forced mode | Per-target opt-in only | Not started | — |
|
||||
| 6. Fallback/diagnostics | Abort-and-join state machine, mode isolation, reason codes, target-mode configuration/UI | Per-target Beta only | Not started | — |
|
||||
| 7. Live gates/rollout | Matrix, security, performance, release promotion | Per-tuple staged | Not started | — |
|
||||
| Work package | Scope | Default behavior change | Status | PR/evidence |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------ | --------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| 0. #8450 legacy fix | Coherent Node/npm selection and live repro | Fixes legacy selection only | Complete and CI-green in draft PR #8724 | E-M0-UNIT-002, E-M0-LIVE-002, E-M0-STATIC-002, E-M0-PR-001, E-M0-CI-001 |
|
||||
| 1. Contract and selectors | Manifest schema, identity, platform/libc selection, hostile inputs | None | Complete and CI-green in draft PR #8728 | `b9d80a4cb`; E-M2-RED-001, E-M2-CONTRACT-001, E-M2-CI-001 |
|
||||
| 2. Runtime builds | Per-tuple assembly, native smoke, SBOM/provenance/signing | None | Draft PR #8741; native build/source trust green; real signing/trust pending | `be32653a7`; E-M3-METADATA-CI-001, E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001 |
|
||||
| 3. Release publication | Prerequisite DAG, embedded manifest, draft upload/read-back gates | Asset-only | **In progress — 2026-07-15, Codex implementation owner** | E-M4-RELEASE-DAG-LOCAL-001; exact-head CI pending |
|
||||
| 4. Desktop resolver/cache | Verified download, extraction, cache, offline behavior | None/forced mode only | Not started | — |
|
||||
| 5. Transfer/install | Bounded transports, structured sentinel, bundled launch behind per-target Beta/forced mode | Per-target opt-in only | Not started | — |
|
||||
| 6. Fallback/diagnostics | Abort-and-join state machine, mode isolation, reason codes, target-mode configuration/UI | Per-target Beta only | Not started | — |
|
||||
| 7. Live gates/rollout | Matrix, security, performance, release promotion | Per-tuple staged | Not started | — |
|
||||
|
||||
Every PR must document:
|
||||
|
||||
@@ -1514,6 +1522,11 @@ focused commands as their scripts/tests are introduced.
|
||||
- [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-*.test.mjs` (E-M3-RUNTIME-CLOSURE-LOCAL-001; 68 local artifact-contract tests plus static reuse of six prior candidate archives; new target-native builder execution remains pending)
|
||||
- [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-*.test.mjs && pnpm run typecheck && pnpm run lint && pnpm run check:max-lines-ratchet` (E-M3-METADATA-LOCAL-001; 76 local artifact-contract tests, typecheck, full lint, reliability gates, localization gates, and max-lines ratchet; native metadata/toolchain proof remains pending)
|
||||
|
||||
### Milestone 4 commands added
|
||||
|
||||
- [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs config/scripts/ssh-relay-runtime-draft-recovery.test.mjs` (E-M4-RELEASE-DAG-LOCAL-RED-001, E-M4-RELEASE-DAG-LOCAL-001; disconnected credential-free stage/recovery contract only)
|
||||
- [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs config/scripts/ssh-relay-runtime-draft-recovery.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` (E-M4-RELEASE-DAG-LOCAL-001; static native-job wiring only, no real release/signing execution)
|
||||
|
||||
### Commands/scripts that must be added or formally identified
|
||||
|
||||
- [x] Manifest/schema/identity/signature/selector unit-test command. (E-M2-CONTRACT-001)
|
||||
@@ -1521,8 +1534,9 @@ focused commands as their scripts/tests are introduced.
|
||||
- [x] Per-tuple POSIX archive inspection and native smoke command. (E-M3-RUNTIME-LOCAL-001; Windows command remains open)
|
||||
- [ ] Embedded-manifest extraction/comparison command for every packaged app.
|
||||
- [ ] Draft release relay-asset completeness/read-back command.
|
||||
- [ ] Release-DAG failure rehearsal covering native signing, aggregate, timeout/retry/manual approval,
|
||||
and recovered draft behavior.
|
||||
- [ ] Live release-DAG failure rehearsal covering native signing, aggregate, timeout/retry/manual
|
||||
approval, and recovered draft behavior. The disconnected contract is implemented under
|
||||
E-M4-RELEASE-DAG-LOCAL-001; real workflow/service evidence remains open.
|
||||
- [ ] Hostile-archive and cache fault suite.
|
||||
- [ ] Full-size transfer memory/cancellation suite for SFTP, POSIX system SSH, and Windows system SSH.
|
||||
- [ ] Remote primitive/binary-fidelity suite for missing commands, BusyBox variants, no-tar POSIX,
|
||||
@@ -8662,6 +8676,175 @@ diff --check`.
|
||||
runner logs, and keep returned-byte/native-trust boxes unchecked until the separately gated real
|
||||
SignPath and endpoint checks pass.
|
||||
|
||||
### E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001 — Native Windows source signatures match retained identity
|
||||
|
||||
- Date: 2026-07-15
|
||||
- Owner: Codex implementation owner
|
||||
- Source: exact head `be32653a7b77eeaed168d91b790fc2e006b438db`; artifact run
|
||||
[29388734922](https://github.com/stablyai/orca/actions/runs/29388734922), PR Checks
|
||||
[29388734935](https://github.com/stablyai/orca/actions/runs/29388734935), and Golden E2E
|
||||
[29388734914](https://github.com/stablyai/orca/actions/runs/29388734914).
|
||||
- Native jobs:
|
||||
- `win32-x64`: job
|
||||
[87267322870](https://github.com/stablyai/orca/actions/runs/29388734922/job/87267322870),
|
||||
GitHub-hosted `windows-2022`, resolved `win22` image `20260706.237.1`, native `X64`,
|
||||
runner `GitHub Actions 1000055156`; PASS in 5 minutes 23 seconds. Its combined two-build,
|
||||
verify, smoke, compare, signing-stage, and source-signature step ran for 3 minutes 45 seconds.
|
||||
- `win32-arm64`: job
|
||||
[87267322867](https://github.com/stablyai/orca/actions/runs/29388734922/job/87267322867),
|
||||
GitHub-hosted `windows-11-arm`, resolved `win11-arm64` image `20260706.102.1`, native `ARM64`,
|
||||
runner `GitHub Actions 1000055154`; PASS in 11 minutes 7 seconds. Its combined two-build,
|
||||
verify, smoke, compare, signing-stage, and source-signature step ran for 5 minutes 30 seconds.
|
||||
- The workflow does not time the three source-signature probes separately. Each probe retains the
|
||||
implemented 30-second process timeout and 64-KiB output bound; the combined step timings above
|
||||
are the narrowest exact native timing available from this run.
|
||||
- Artifact retrieval and identity command:
|
||||
|
||||
```sh
|
||||
gh run download 29388734922 --repo stablyai/orca \
|
||||
--name ssh-relay-runtime-win32-x64 --dir <exclusive-x64-directory>
|
||||
gh run download 29388734922 --repo stablyai/orca \
|
||||
--name ssh-relay-runtime-win32-arm64 --dir <exclusive-arm64-directory>
|
||||
node --input-type=module -e '<bounded identity/signing-report comparison>' <artifact-directory>
|
||||
shasum -a 256 <artifact-directory>/*.source-signatures.json
|
||||
wc -c <artifact-directory>/*.source-signatures.json
|
||||
```
|
||||
|
||||
- Result: PASS. Each artifact contains exactly one identity, archive, SPDX statement, provenance
|
||||
statement, signing-stage report, and source-signature report. The bounded comparison requires one
|
||||
report, exact tuple/content identity, exactly three verified files, one `official-node`, two
|
||||
`preserved-upstream`, identity-entry hash equality for every verified file, and exact preserved
|
||||
subject/thumbprint equality with the authenticated signing-stage report.
|
||||
- Retained report identities:
|
||||
- x64 report: 1,401 bytes, SHA-256
|
||||
`9ceb6b3dadf85ea7d338dbd11e1397e58b83455842378f3465ccb945b5ec0805`, source content ID
|
||||
`sha256:7ddad668780ce5b2592d86afcebf9d897172bdf07c618ae238b5d51eebfe1596`.
|
||||
- arm64 report: 1,403 bytes, SHA-256
|
||||
`5a63a7f31d64cc6a8b825461e9729f59f0176d8d03293fa0ce2c05df39734d22`, source content ID
|
||||
`sha256:2955cec7f22447a1303dad548df6c2fa2470b62ba1d70924b8b169e718a7701e`.
|
||||
- Exact native signer evidence:
|
||||
- Both official Node files report `Valid` with subject
|
||||
`CN=OpenJS Foundation, O=OpenJS Foundation, L=San Francisco, S=California, C=US` and thumbprint
|
||||
`CECD9673E955CA766047DD43706D31E48A6BD3B5`. The x64 Node hash is
|
||||
`sha256:9a4eb5f1c29c6a2e93852ead46b999e284a6a5ca8bab4d4e241d587d025a52de`; arm64 is
|
||||
`sha256:c7225670c3f477778e18c43a55867f7a0d76468221245e5981ab80eb953c8102`.
|
||||
- Every preserved file reports `Valid` with subject
|
||||
`CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US`. x64
|
||||
`OpenConsole.exe` and `conpty.dll` use thumbprint
|
||||
`3F56A45111684D454E231CFDC4DA5C8D370F9816`. Arm64 `OpenConsole.exe` uses
|
||||
`F5877012FBD62FABCBDC8D8CEE9C9585BA30DF79`; arm64 `conpty.dll` uses
|
||||
`3F56A45111684D454E231CFDC4DA5C8D370F9816`.
|
||||
- Concurrent regression result: PR Checks and both Golden E2E jobs completed successfully at the
|
||||
exact head. The artifact run concluded `failure` solely because its separate Windows arm64
|
||||
oldest-baseline job observed `10.0.26200` rather than the exact required build `26100`, after the
|
||||
runtime itself passed bundled Node, PTY, watcher, and settlement smoke in 8,275.5554 ms with a
|
||||
49,852,416-byte RSS measurement. This is the retained
|
||||
E-M3-WINDOWS-ARM64-BASELINE-CI-RED-001 gap, not a source-signature failure.
|
||||
- Does not prove: real SignPath returned Orca signatures, final-tree Authenticode trust, Defender or
|
||||
WDAC policy, Windows arm64 build 26100, macOS trust, draft release aggregation/read-back, SSH
|
||||
transfer/install, fallback/performance, or an enabled tuple. Nothing was published or connected to
|
||||
production behavior.
|
||||
|
||||
### E-M4-RELEASE-DAG-LOCAL-RED-001 — Missing release-stage and draft-recovery gates fail imports
|
||||
|
||||
- Date: 2026-07-15
|
||||
- Owner: Codex implementation owner
|
||||
- Source: exact implementation base `be32653a7b77eeaed168d91b790fc2e006b438db` plus the two new
|
||||
uncommitted purpose-named test files.
|
||||
- Command:
|
||||
|
||||
```sh
|
||||
pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \
|
||||
config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs \
|
||||
config/scripts/ssh-relay-runtime-draft-recovery.test.mjs
|
||||
```
|
||||
|
||||
- Result: expected RED in 346 ms. Both suites fail before collecting tests with
|
||||
`ERR_MODULE_NOT_FOUND` for `ssh-relay-runtime-release-stage-gate.mjs` and
|
||||
`ssh-relay-runtime-draft-recovery.mjs`.
|
||||
- Oracle proved: the current worktree has no reusable contract that requires a complete immutable
|
||||
build → native-signing approval/return → aggregate/manifest-signature → upload → authenticated
|
||||
read-back chain, and no contract that permits same-draft recovery only for exact already-uploaded
|
||||
runtime bytes.
|
||||
- Does not prove: implementation, workflow wiring, GitHub release behavior, signing-service behavior,
|
||||
a real draft recovery, publication, desktop embedding, SSH behavior, or an enabled tuple.
|
||||
- Correction: implement bounded exact-stage validation and hash/size-bound same-draft reuse as
|
||||
disconnected credential-free modules; retain every production/default and tuple state unchanged.
|
||||
|
||||
### E-M4-RELEASE-DAG-LOCAL-001 — Fail-closed release stages and same-draft recovery pass locally
|
||||
|
||||
- Date: 2026-07-15
|
||||
- Owner: Codex implementation owner
|
||||
- Source: uncommitted implementation based on exact head
|
||||
`be32653a7b77eeaed168d91b790fc2e006b438db`.
|
||||
- Runner/remote/network: local macOS 26.2 build 25C56 arm64, Node v26.0.0 and pnpm 10.24.0. No
|
||||
GitHub release, signing service, credential, approval environment, SSH remote, publication, or
|
||||
production consumer was used. The repository emits its expected Node-24 engine warning.
|
||||
- Commands:
|
||||
|
||||
```sh
|
||||
node --check config/scripts/ssh-relay-runtime-release-stage-gate.mjs
|
||||
node --check config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs
|
||||
node --check config/scripts/ssh-relay-runtime-draft-recovery.mjs
|
||||
node --check config/scripts/ssh-relay-runtime-draft-recovery.test.mjs
|
||||
/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \
|
||||
config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs \
|
||||
config/scripts/ssh-relay-runtime-draft-recovery.test.mjs
|
||||
pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \
|
||||
config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs \
|
||||
config/scripts/ssh-relay-runtime-draft-recovery.test.mjs \
|
||||
config/scripts/ssh-relay-runtime-workflow.test.mjs
|
||||
pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \
|
||||
config/scripts/ssh-relay-*.test.mjs
|
||||
pnpm run typecheck
|
||||
pnpm run lint
|
||||
pnpm run check:max-lines-ratchet
|
||||
pnpm exec oxfmt --check \
|
||||
.github/workflows/ssh-relay-runtime-artifacts.yml \
|
||||
config/scripts/ssh-relay-runtime-release-stage-gate.mjs \
|
||||
config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs \
|
||||
config/scripts/ssh-relay-runtime-draft-recovery.mjs \
|
||||
config/scripts/ssh-relay-runtime-draft-recovery.test.mjs \
|
||||
config/scripts/ssh-relay-runtime-workflow.test.mjs \
|
||||
docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md \
|
||||
docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md
|
||||
git diff --check
|
||||
```
|
||||
|
||||
- Result: PASS. The two purpose-named suites pass 2 files/21 tests in 245 ms; `/usr/bin/time -l`
|
||||
records 0.91 seconds wall time, 131,825,664-byte maximum RSS, zero swaps, and 95,933,720-byte peak
|
||||
memory footprint for the Vitest process tree. The workflow-inclusive focus passes 3 files/27
|
||||
tests; the final complete SSH-relay suite passes 33 files/184 tests in 8.52 seconds. All four syntax
|
||||
checks, typecheck, full lint/reliability/localization/bundled-skill gates, max-lines (355
|
||||
grandfathered suppressions and no new bypass), focused formatting, and `git diff --check` pass.
|
||||
Lint emits only existing unrelated warnings.
|
||||
- Stage oracle: candidate tuples must use exact supported identities; every candidate requires one
|
||||
bounded successful build, every macOS/Windows candidate requires one single-attempt approved
|
||||
native-signing return that changes immutable identity, and Linux cannot invent a signing stage.
|
||||
Aggregate inputs must exactly equal final per-tuple outputs and produce bounded manifest/signature
|
||||
assets. Upload and authenticated read-back must reproduce the complete ordered hash/size chain.
|
||||
Missing, duplicate, unexpected, failed, cancelled, timed-out, retry-exhausted, over-budget,
|
||||
approval-absent, approval-denied, approval-timed-out, input-drifted, incomplete, or byte-drifted
|
||||
results fail closed.
|
||||
- Bound policy: build is at most 30 minutes and three attempts; native signing is one attempt and at
|
||||
most four hours; aggregate is one attempt and 15 minutes; upload and read-back are each at most
|
||||
three attempts and 15 minutes. Signing is deliberately single-attempt so one approval cannot
|
||||
silently authorize a second set of bytes after timeout or service failure.
|
||||
- Recovery oracle: only the same draft tag and exact 40-hex source commit may be resumed. Existing
|
||||
managed assets are reusable only when name, SHA-256, and non-zero size exactly match expected
|
||||
immutable output. Changed, empty, duplicate, unexpected managed, published, cross-tag, and
|
||||
cross-commit states fail closed; unrelated desktop assets are ignored rather than overwritten.
|
||||
- Workflow oracle: both POSIX and Windows artifact jobs syntax-check and execute the new suites under
|
||||
the same Node 24 native job families. The workflow retains read-only contents permission and has
|
||||
no release, signing credential, or publication authority.
|
||||
- Does not prove: a real native-signing approval/timeout, signing-service retry behavior, a real
|
||||
aggregate or signed manifest, upload/read-back through GitHub's draft release API, recovered-draft
|
||||
execution, desktop prerequisites/embedding, native trust, SSH transfer/install, fallback,
|
||||
performance, or an enabled tuple. Production release workflow wiring remains absent.
|
||||
- Follow-up: run the contract on all six native jobs at the exact implementation head, then add the
|
||||
credential-free aggregate-input/read-back implementation without connecting publication or a
|
||||
desktop consumer.
|
||||
|
||||
## Accepted Gaps
|
||||
|
||||
No product gap is accepted merely because it appears in this list. Each entry requires explicit
|
||||
@@ -8719,10 +8902,12 @@ The project is not complete until every applicable item below is checked with ev
|
||||
|
||||
## Next Required Action
|
||||
|
||||
Provision or select qualifying exact-floor execution for Linux kernel 4.18, macOS 13.5, and Windows
|
||||
arm64 build 26100 without weakening their oracles; in parallel, implement and prove target-native
|
||||
macOS/Windows signing and trust as the next independent artifact-only capability. Keep every tuple
|
||||
disabled and do not connect an artifact consumer until the applicable baseline and trust cells pass.
|
||||
Run the disconnected credential-free release/signing DAG and draft-recovery contracts on all six
|
||||
native jobs at the exact implementation head. If green, implement the next artifact-only
|
||||
aggregate-input/read-back capability without release publication or a desktop consumer. In parallel,
|
||||
provision qualifying exact-floor execution for Linux kernel 4.18, macOS 13.5, and Windows arm64 build
|
||||
26100 and real macOS/Windows signing and trust. Keep every tuple disabled until the applicable
|
||||
baseline and trust cells pass.
|
||||
|
||||
Cross-family Layer B targets, the protected manifest-signing environment, oldest-baseline/native-
|
||||
trust cells, and the paired legacy performance baseline remain release/default-path blockers. No
|
||||
|
||||
Reference in New Issue
Block a user