diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml
index 4362b6ed781..2a522a034a2 100644
--- a/.github/workflows/ssh-relay-runtime-artifacts.yml
+++ b/.github/workflows/ssh-relay-runtime-artifacts.yml
@@ -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 `
diff --git a/config/scripts/ssh-relay-runtime-draft-recovery.mjs b/config/scripts/ssh-relay-runtime-draft-recovery.mjs
new file mode 100644
index 00000000000..eca860624cb
--- /dev/null
+++ b/config/scripts/ssh-relay-runtime-draft-recovery.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))
+ }
+}
diff --git a/config/scripts/ssh-relay-runtime-draft-recovery.test.mjs b/config/scripts/ssh-relay-runtime-draft-recovery.test.mjs
new file mode 100644
index 00000000000..0567062b053
--- /dev/null
+++ b/config/scripts/ssh-relay-runtime-draft-recovery.test.mjs
@@ -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])
+}
diff --git a/config/scripts/ssh-relay-runtime-release-stage-gate.mjs b/config/scripts/ssh-relay-runtime-release-stage-gate.mjs
new file mode 100644
index 00000000000..4eee9540bc3
--- /dev/null
+++ b/config/scripts/ssh-relay-runtime-release-stage-gate.mjs
@@ -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 }
+}
diff --git a/config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs b/config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs
new file mode 100644
index 00000000000..7aeddfcbe51
--- /dev/null
+++ b/config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs
@@ -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)
+ }
+ })
+})
diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs
index cd6938bf512..93a773995d5 100644
--- a/config/scripts/ssh-relay-runtime-workflow.test.mjs
+++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs
@@ -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)
diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md
index 795025792f6..9fe965f8892 100644
--- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md
+++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md
@@ -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.
diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md
index ad00982b5a4..3c6a88372f7 100644
--- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md
+++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md
@@ -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
-Last updated: 2026-07-14
-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
-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.
+Last updated: 2026-07-15
+Current phase: Milestone 4 / Work Package 3 credential-free release/signing DAG failure contracts — **In progress — 2026-07-15, Codex implementation owner**. Work Package 2's target-native Windows official-Node and preserved-upstream source-signature gate is closed under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001. Exact-floor gaps and real native signing/trust remain open external gates, so the active package is limited to disconnected fail-closed release-DAG contracts and tests; it may not publish, connect desktop consumers, or enable a tuple. Production/default behavior is unchanged, no bundled-runtime path is enabled, and no artifact is published.
+Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — 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.
Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693)
@@ -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
+ gh run download 29388734922 --repo stablyai/orca \
+ --name ssh-relay-runtime-win32-arm64 --dir
+ node --input-type=module -e ''
+ shasum -a 256 /*.source-signatures.json
+ wc -c /*.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