diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml
index 5b4b8d2682b..05f9979d6c8 100644
--- a/.github/workflows/ssh-relay-runtime-artifacts.yml
+++ b/.github/workflows/ssh-relay-runtime-artifacts.yml
@@ -179,6 +179,8 @@ jobs:
node --check config/scripts/ssh-relay-runtime-macos-signing.test.mjs
node --check config/scripts/ssh-relay-runtime-native-signing-finalization.mjs
node --check config/scripts/ssh-relay-runtime-native-signing-finalization.test.mjs
+ node --check config/scripts/ssh-relay-runtime-linux-finalization.mjs
+ node --check config/scripts/ssh-relay-runtime-linux-finalization.test.mjs
node --check config/scripts/ssh-relay-runtime-native-signing-workflow.test.mjs
node --check config/scripts/ssh-relay-runtime-native-signing-rehearsal-workflow.test.mjs
node --check config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs
@@ -216,6 +218,7 @@ jobs:
config/scripts/ssh-relay-runtime-archive-extraction.test.mjs \
config/scripts/ssh-relay-runtime-macos-signing.test.mjs \
config/scripts/ssh-relay-runtime-native-signing-finalization.test.mjs \
+ config/scripts/ssh-relay-runtime-linux-finalization.test.mjs \
config/scripts/ssh-relay-runtime-native-signing-workflow.test.mjs \
config/scripts/ssh-relay-runtime-native-signing-rehearsal-workflow.test.mjs \
config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs \
@@ -316,6 +319,17 @@ jobs:
' "$signing_report"
test ! -e "$signing_stage"
cp "$signing_report" "$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}/"
+ verified_at=$(node -e 'process.stdout.write(new Date().toISOString())')
+ final_output="$output_root/linux-final"
+ node config/scripts/ssh-relay-runtime-linux-finalization.mjs \
+ --source-output-directory "$first_output" \
+ --identity "$identity" \
+ --output-directory "$final_output" \
+ --verified-at "$verified_at" \
+ --native-verification-tool-version "$(node -p 'process.versions.node')"
+ cp "$final_output/assets/orca-ssh-relay-runtime-${{ matrix.tuple }}.manifest-tuple.json" \
+ "$final_output/evidence/${{ matrix.tuple }}.linux-finalization.json" \
+ "$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}/"
exit 0
fi
mkdir -p "$output_root"
@@ -590,6 +604,10 @@ jobs:
if ($LASTEXITCODE -ne 0) { throw 'runtime signing finalization syntax check failed' }
node --check config/scripts/ssh-relay-runtime-native-signing-finalization.test.mjs
if ($LASTEXITCODE -ne 0) { throw 'runtime signing finalization test syntax check failed' }
+ node --check config/scripts/ssh-relay-runtime-linux-finalization.mjs
+ if ($LASTEXITCODE -ne 0) { throw 'runtime Linux finalization syntax check failed' }
+ node --check config/scripts/ssh-relay-runtime-linux-finalization.test.mjs
+ if ($LASTEXITCODE -ne 0) { throw 'runtime Linux finalization test syntax check failed' }
node --check config/scripts/ssh-relay-runtime-native-signing-workflow.test.mjs
if ($LASTEXITCODE -ne 0) { throw 'runtime native signing workflow test syntax check failed' }
node --check config/scripts/ssh-relay-runtime-native-signing-rehearsal-workflow.test.mjs
@@ -630,6 +648,7 @@ jobs:
config/scripts/ssh-relay-runtime-archive-extraction.test.mjs `
config/scripts/ssh-relay-runtime-macos-signing.test.mjs `
config/scripts/ssh-relay-runtime-native-signing-finalization.test.mjs `
+ config/scripts/ssh-relay-runtime-linux-finalization.test.mjs `
config/scripts/ssh-relay-runtime-native-signing-workflow.test.mjs `
config/scripts/ssh-relay-runtime-native-signing-rehearsal-workflow.test.mjs `
config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs `
diff --git a/config/scripts/ssh-relay-runtime-linux-finalization.mjs b/config/scripts/ssh-relay-runtime-linux-finalization.mjs
new file mode 100644
index 00000000000..93a3237cbb5
--- /dev/null
+++ b/config/scripts/ssh-relay-runtime-linux-finalization.mjs
@@ -0,0 +1,226 @@
+import { constants } from 'node:fs'
+import { copyFile, lstat, mkdir, realpath, rm, writeFile } from 'node:fs/promises'
+import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
+import { pathToFileURL } from 'node:url'
+
+import { writeSshRelayRuntimeManifestTupleDescriptor } from './ssh-relay-runtime-manifest-tuple.mjs'
+import {
+ buildSshRelayRuntimeNativeSigningPlan,
+ readSshRelayRuntimeNativeSigningIdentity
+} from './ssh-relay-runtime-native-signing-plan.mjs'
+import { verifySshRelayRuntime } from './verify-ssh-relay-runtime.mjs'
+
+const MAX_RECEIPT_BYTES = 32 * 1024 * 1024
+const PATH_ARGUMENTS = new Map([
+ ['--source-output-directory', 'sourceOutputDirectory'],
+ ['--identity', 'identityPath'],
+ ['--output-directory', 'outputDirectory']
+])
+const VALUE_ARGUMENTS = new Map([
+ ['--verified-at', 'verifiedAt'],
+ ['--native-verification-tool-version', 'nativeVerificationToolVersion']
+])
+
+function containsPath(parent, candidate) {
+ const path = relative(parent, candidate)
+ return path === '' || (path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path))
+}
+
+async function physicalDirectory(path, label) {
+ const metadata = await lstat(path)
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
+ throw new Error(`Runtime Linux finalization ${label} must be a real directory`)
+ }
+ return realpath(path)
+}
+
+async function assertAbsent(path) {
+ try {
+ await lstat(path)
+ } catch (error) {
+ if (error.code === 'ENOENT') {
+ return
+ }
+ throw error
+ }
+ throw new Error('Runtime Linux finalization requires an exclusive output directory')
+}
+
+async function copyRegularFile(source, destination, label) {
+ const before = await lstat(source, { bigint: true })
+ if (!before.isFile() || before.isSymbolicLink() || before.size <= 0n) {
+ throw new Error(`Runtime Linux finalization ${label} must be a non-empty regular file`)
+ }
+ await copyFile(source, destination, constants.COPYFILE_EXCL)
+ const after = await lstat(source, { bigint: true })
+ if (
+ before.dev !== after.dev ||
+ before.ino !== after.ino ||
+ before.size !== after.size ||
+ before.mtimeNs !== after.mtimeNs ||
+ before.ctimeNs !== after.ctimeNs
+ ) {
+ throw new Error(`Runtime Linux finalization ${label} changed while copying`)
+ }
+}
+
+function aggregateAssetNames(identity) {
+ if (
+ typeof identity.archive?.name !== 'string' ||
+ basename(identity.archive.name) !== identity.archive.name
+ ) {
+ throw new Error('Runtime Linux finalization archive identity is invalid')
+ }
+ const prefix = `orca-ssh-relay-runtime-${identity.tupleId}`
+ return {
+ archive: identity.archive.name,
+ sbom: `${prefix}.spdx.json`,
+ provenance: `${prefix}.provenance.json`
+ }
+}
+
+function linuxVerificationReport(identity, plan) {
+ return {
+ tupleId: identity.tupleId,
+ sourceContentId: identity.contentId,
+ finalContentId: identity.contentId,
+ verifiedFiles: plan.verificationFiles.map((file) => ({
+ path: file.path,
+ role: file.role,
+ sha256: file.sourceSha256
+ }))
+ }
+}
+
+async function writeReceipt(path, receipt) {
+ const bytes = Buffer.from(`${JSON.stringify(receipt, null, 2)}\n`, 'utf8')
+ if (bytes.length === 0 || bytes.length > MAX_RECEIPT_BYTES) {
+ throw new Error('Runtime Linux finalization receipt exceeds its size limit')
+ }
+ await writeFile(path, bytes, { flag: 'wx', mode: 0o600 })
+}
+
+export async function finalizeSshRelayRuntimeLinuxArtifact({
+ sourceOutputDirectory,
+ identityPath,
+ outputDirectory,
+ verifiedAt,
+ nativeVerificationTool,
+ readIdentityImpl = readSshRelayRuntimeNativeSigningIdentity,
+ buildPlanImpl = buildSshRelayRuntimeNativeSigningPlan,
+ verifyRuntimeImpl = verifySshRelayRuntime,
+ writeDescriptorImpl = writeSshRelayRuntimeManifestTupleDescriptor
+}) {
+ const source = await physicalDirectory(resolve(sourceOutputDirectory), 'source output')
+ const absoluteIdentity = resolve(identityPath)
+ const physicalIdentity = await realpath(absoluteIdentity)
+ const identity = await readIdentityImpl(physicalIdentity)
+ if (identity.os !== 'linux' || !identity.tupleId?.startsWith('linux-')) {
+ throw new Error('Runtime Linux finalization accepts only Linux identities')
+ }
+ const plan = buildPlanImpl(identity)
+ if (plan.platform !== 'linux') {
+ throw new Error('Runtime Linux finalization signing plan must be hash-only Linux')
+ }
+ const expectedIdentityPath = join(
+ source,
+ `orca-ssh-relay-runtime-${identity.tupleId}.identity.json`
+ )
+ if (physicalIdentity !== expectedIdentityPath) {
+ throw new Error('Runtime Linux finalization identity must be the exact source output identity')
+ }
+ const outputParent = await physicalDirectory(dirname(resolve(outputDirectory)), 'output parent')
+ const output = resolve(outputParent, basename(resolve(outputDirectory)))
+ if (containsPath(source, output) || containsPath(output, source)) {
+ throw new Error('Runtime Linux finalization source and output must be physically disjoint')
+ }
+ await assertAbsent(output)
+
+ const names = aggregateAssetNames(identity)
+ const runtimeDirectory = join(source, 'runtime')
+ await physicalDirectory(runtimeDirectory, 'runtime')
+ const finalIdentity = { ...identity }
+ delete finalIdentity.archive
+ let outputCreated = false
+ try {
+ await mkdir(output)
+ outputCreated = true
+ const assetsRoot = join(output, 'assets')
+ const evidenceRoot = join(output, 'evidence')
+ await Promise.all([mkdir(assetsRoot), mkdir(evidenceRoot)])
+ for (const [label, name] of Object.entries(names)) {
+ await copyRegularFile(join(source, name), join(assetsRoot, name), label)
+ }
+
+ const verification = await verifyRuntimeImpl({
+ runtimeDirectory,
+ identityPath: physicalIdentity,
+ archivePath: join(source, names.archive)
+ })
+ // Why: Linux has no signing mutation, but its complete native hash projection must still be final.
+ const verificationReport = linuxVerificationReport(identity, plan)
+ const descriptor = await writeDescriptorImpl({
+ runtimeRoot: runtimeDirectory,
+ inputDirectory: assetsRoot,
+ finalIdentity,
+ verificationReport,
+ nativeVerificationTool,
+ verifiedAt
+ })
+ const receipt = {
+ tupleId: identity.tupleId,
+ contentId: identity.contentId,
+ verification,
+ aggregateInput: descriptor.input
+ }
+ const receiptPath = join(evidenceRoot, `${identity.tupleId}.linux-finalization.json`)
+ await writeReceipt(receiptPath, receipt)
+ return {
+ ...receipt,
+ assetsRoot,
+ evidenceRoot,
+ receiptPath
+ }
+ } catch (error) {
+ if (outputCreated) {
+ // Why: an incomplete descriptor set must never become an aggregate candidate.
+ await rm(output, { recursive: true, force: true })
+ }
+ throw error
+ }
+}
+
+export function parseSshRelayRuntimeLinuxFinalizationArguments(argv) {
+ const result = {}
+ for (let index = 0; index < argv.length; index += 2) {
+ const flag = argv[index]
+ const field = PATH_ARGUMENTS.get(flag) ?? VALUE_ARGUMENTS.get(flag)
+ const value = argv[index + 1]
+ if (!field || !value || value.startsWith('--') || result[field]) {
+ throw new Error(`Invalid runtime Linux finalization argument: ${flag}`)
+ }
+ result[field] = PATH_ARGUMENTS.has(flag) ? resolve(value) : value
+ }
+ for (const field of [...PATH_ARGUMENTS.values(), ...VALUE_ARGUMENTS.values()]) {
+ if (!result[field]) {
+ throw new Error(`Missing runtime Linux finalization argument: ${field}`)
+ }
+ }
+ return result
+}
+
+async function main() {
+ const options = parseSshRelayRuntimeLinuxFinalizationArguments(process.argv.slice(2))
+ const result = await finalizeSshRelayRuntimeLinuxArtifact({
+ ...options,
+ nativeVerificationTool: { name: 'node', version: options.nativeVerificationToolVersion }
+ })
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
+}
+
+if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
+ main().catch((error) => {
+ process.stderr.write(`SSH relay runtime Linux finalization failed: ${error.stack ?? error}\n`)
+ process.exitCode = 1
+ })
+}
diff --git a/config/scripts/ssh-relay-runtime-linux-finalization.test.mjs b/config/scripts/ssh-relay-runtime-linux-finalization.test.mjs
new file mode 100644
index 00000000000..a40d469f3f0
--- /dev/null
+++ b/config/scripts/ssh-relay-runtime-linux-finalization.test.mjs
@@ -0,0 +1,251 @@
+import { mkdtemp, mkdir, readFile, realpath, rm, writeFile } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+
+import { parse } from 'yaml'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+
+import { createSshRelayArtifactTestManifest } from '../../src/main/ssh/ssh-relay-artifact-test-manifest.ts'
+import {
+ finalizeSshRelayRuntimeLinuxArtifact,
+ parseSshRelayRuntimeLinuxFinalizationArguments
+} from './ssh-relay-runtime-linux-finalization.mjs'
+
+const temporaryDirectories = []
+const VERIFIED_AT = '2026-07-15T13:30:00.000Z'
+const artifactWorkflowUrl = new URL(
+ '../../.github/workflows/ssh-relay-runtime-artifacts.yml',
+ import.meta.url
+)
+
+function linuxIdentity() {
+ const tuple = createSshRelayArtifactTestManifest().tuples[0]
+ const {
+ archive,
+ metadataAssets: _metadataAssets,
+ nativeVerification: _native,
+ ...identity
+ } = tuple
+ return { identity: { ...identity, archive }, finalIdentity: identity }
+}
+
+function linuxPlan(identity) {
+ return {
+ platform: 'linux',
+ verificationFiles: identity.entries
+ .filter(
+ (entry) =>
+ entry.type === 'file' &&
+ ['node', 'node-pty-native', 'parcel-watcher-native', 'native-runtime'].includes(
+ entry.role
+ )
+ )
+ .map((entry) => ({ path: entry.path, role: entry.role, sourceSha256: entry.sha256 }))
+ }
+}
+
+async function fixture() {
+ const root = await mkdtemp(join(tmpdir(), 'orca-runtime-linux-finalization-'))
+ temporaryDirectories.push(root)
+ const source = join(root, 'source')
+ const runtime = join(source, 'runtime')
+ const output = join(root, 'final')
+ await mkdir(runtime, { recursive: true })
+ const { identity, finalIdentity } = linuxIdentity()
+ const identityPath = join(source, `orca-ssh-relay-runtime-${identity.tupleId}.identity.json`)
+ const archivePath = join(source, identity.archive.name)
+ const sbomPath = join(source, `orca-ssh-relay-runtime-${identity.tupleId}.spdx.json`)
+ const provenancePath = join(source, `orca-ssh-relay-runtime-${identity.tupleId}.provenance.json`)
+ await Promise.all([
+ writeFile(identityPath, `${JSON.stringify(identity)}\n`),
+ writeFile(archivePath, 'archive'),
+ writeFile(sbomPath, 'sbom'),
+ writeFile(provenancePath, 'provenance')
+ ])
+ return { source, runtime, output, identity, finalIdentity, identityPath }
+}
+
+afterEach(async () => {
+ await Promise.all(
+ temporaryDirectories
+ .splice(0)
+ .map((directory) => rm(directory, { recursive: true, force: true }))
+ )
+})
+
+describe('SSH relay runtime Linux hash-only finalization', () => {
+ it('verifies before emitting an exact aggregate-ready descriptor and receipt', async () => {
+ const input = await fixture()
+ const order = []
+ const plan = linuxPlan(input.finalIdentity)
+ const physicalIdentityPath = await realpath(input.identityPath)
+ const physicalRuntime = await realpath(input.runtime)
+ const physicalSource = await realpath(input.source)
+ const verifyRuntimeImpl = vi.fn(async ({ runtimeDirectory, identityPath, archivePath }) => {
+ order.push('verify')
+ expect(runtimeDirectory).toBe(physicalRuntime)
+ expect(identityPath).toBe(physicalIdentityPath)
+ expect(archivePath).toBe(join(physicalSource, input.identity.archive.name))
+ return { smoke: { nodeVersion: 'v24.18.0' } }
+ })
+ const writeDescriptorImpl = vi.fn(async (options) => {
+ order.push('descriptor')
+ expect(options.finalIdentity).toEqual(input.finalIdentity)
+ expect(options.verificationReport).toEqual({
+ tupleId: input.identity.tupleId,
+ sourceContentId: input.identity.contentId,
+ finalContentId: input.identity.contentId,
+ verifiedFiles: plan.verificationFiles.map((file) => ({
+ path: file.path,
+ role: file.role,
+ sha256: file.sourceSha256
+ }))
+ })
+ expect(options.nativeVerificationTool).toEqual({ name: 'node', version: '24.18.0' })
+ expect(options.verifiedAt).toBe(VERIFIED_AT)
+ return {
+ tupleId: input.identity.tupleId,
+ input: {
+ tupleId: input.identity.tupleId,
+ descriptor: { name: 'descriptor.json', size: 1, sha256: `sha256:${'a'.repeat(64)}` },
+ archive: {
+ name: input.identity.archive.name,
+ size: 7,
+ sha256: `sha256:${'b'.repeat(64)}`
+ },
+ sbom: { name: 'sbom.json', size: 4, sha256: `sha256:${'c'.repeat(64)}` },
+ provenance: { name: 'provenance.json', size: 10, sha256: `sha256:${'d'.repeat(64)}` }
+ }
+ }
+ })
+
+ const result = await finalizeSshRelayRuntimeLinuxArtifact({
+ sourceOutputDirectory: input.source,
+ identityPath: input.identityPath,
+ outputDirectory: input.output,
+ verifiedAt: VERIFIED_AT,
+ nativeVerificationTool: { name: 'node', version: '24.18.0' },
+ readIdentityImpl: async () => input.identity,
+ buildPlanImpl: () => plan,
+ verifyRuntimeImpl,
+ writeDescriptorImpl
+ })
+
+ expect(order).toEqual(['verify', 'descriptor'])
+ expect(result).toMatchObject({
+ tupleId: input.identity.tupleId,
+ contentId: input.identity.contentId,
+ aggregateInput: { tupleId: input.identity.tupleId }
+ })
+ expect(JSON.parse(await readFile(result.receiptPath, 'utf8'))).toEqual({
+ tupleId: input.identity.tupleId,
+ contentId: input.identity.contentId,
+ verification: { smoke: { nodeVersion: 'v24.18.0' } },
+ aggregateInput: result.aggregateInput
+ })
+ await expect(
+ readFile(join(result.assetsRoot, input.identity.archive.name), 'utf8')
+ ).resolves.toBe('archive')
+ })
+
+ it('rejects non-Linux input and removes partial output on failure', async () => {
+ const input = await fixture()
+ const nonLinux = { ...input.identity, os: 'darwin', tupleId: 'darwin-x64' }
+ await expect(
+ finalizeSshRelayRuntimeLinuxArtifact({
+ sourceOutputDirectory: input.source,
+ identityPath: input.identityPath,
+ outputDirectory: input.output,
+ verifiedAt: VERIFIED_AT,
+ nativeVerificationTool: { name: 'node', version: '24.18.0' },
+ readIdentityImpl: async () => nonLinux,
+ verifyRuntimeImpl: vi.fn(),
+ writeDescriptorImpl: vi.fn()
+ })
+ ).rejects.toThrow(/linux/i)
+
+ await expect(
+ finalizeSshRelayRuntimeLinuxArtifact({
+ sourceOutputDirectory: input.source,
+ identityPath: input.identityPath,
+ outputDirectory: input.output,
+ verifiedAt: VERIFIED_AT,
+ nativeVerificationTool: { name: 'node', version: '24.18.0' },
+ readIdentityImpl: async () => input.identity,
+ buildPlanImpl: () => linuxPlan(input.finalIdentity),
+ verifyRuntimeImpl: async () => ({ smoke: true }),
+ writeDescriptorImpl: async () => {
+ throw new Error('descriptor failed')
+ }
+ })
+ ).rejects.toThrow(/descriptor failed/i)
+ await expect(
+ readFile(join(input.output, 'assets', input.identity.archive.name))
+ ).rejects.toMatchObject({ code: 'ENOENT' })
+ })
+
+ it('parses only the exact bounded CLI arguments', () => {
+ expect(
+ parseSshRelayRuntimeLinuxFinalizationArguments([
+ '--source-output-directory',
+ 'source',
+ '--identity',
+ 'identity.json',
+ '--output-directory',
+ 'output',
+ '--verified-at',
+ VERIFIED_AT,
+ '--native-verification-tool-version',
+ '24.18.0'
+ ])
+ ).toMatchObject({
+ verifiedAt: VERIFIED_AT,
+ nativeVerificationToolVersion: '24.18.0'
+ })
+ expect(() =>
+ parseSshRelayRuntimeLinuxFinalizationArguments(['--output-directory', 'output'])
+ ).toThrow(/missing/i)
+ expect(() =>
+ parseSshRelayRuntimeLinuxFinalizationArguments([
+ '--source-output-directory',
+ 'source',
+ '--identity',
+ 'identity.json',
+ '--output-directory',
+ 'output',
+ '--verified-at',
+ VERIFIED_AT,
+ '--native-verification-tool-version',
+ '24.18.0',
+ '--extra',
+ 'value'
+ ])
+ ).toThrow(/invalid/i)
+ })
+
+ it('emits Linux descriptors before upload and runs the contract on both native families', async () => {
+ const source = await readFile(artifactWorkflowUrl, 'utf8')
+ const workflow = parse(source)
+ const posix = workflow.jobs['build-posix-runtime']
+ const windows = workflow.jobs['build-windows-runtime']
+ const build = posix.steps.find(
+ (step) => step.name === 'Build twice, inspect, smoke, and compare exact runtime'
+ ).run
+ const finalization = build.indexOf('ssh-relay-runtime-linux-finalization.mjs')
+ const descriptorCopy = build.indexOf('.manifest-tuple.json', finalization)
+ const linuxExit = build.indexOf('exit 0', descriptorCopy)
+ expect(finalization).toBeGreaterThan(-1)
+ expect(descriptorCopy).toBeGreaterThan(finalization)
+ expect(linuxExit).toBeGreaterThan(descriptorCopy)
+ for (const job of [posix, windows]) {
+ const contract = job.steps.find(
+ (step) => step.name === 'Run runtime artifact contract tests'
+ ).run
+ expect(contract).toContain(
+ 'node --check config/scripts/ssh-relay-runtime-linux-finalization.mjs'
+ )
+ expect(contract).toContain('config/scripts/ssh-relay-runtime-linux-finalization.test.mjs')
+ }
+ expect(posix.steps.at(-1).with.path).toBe('runtime-evidence/${{ matrix.tuple }}/')
+ })
+})
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 b7e342239ad..8ad68dab3f9 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
@@ -234,6 +234,11 @@ complete a box.
`E-M4-NATIVE-SIGNING-REHEARSAL-CI-001`; PR Checks and Golden E2E are green. The artifact run is
red only for the retained Windows arm64 build-26100 floor mismatch.
- [ ] Add a fail-closed aggregate and immutable manifest-signing job.
+ First emit the missing aggregate-ready Linux hash-only tuple descriptors; macOS/Windows
+ already emit equivalent post-sign descriptors. This prerequisite is in progress and remains
+ artifact-only; its missing-module RED and local GREEN are
+ `E-M4-LINUX-FINALIZATION-LOCAL-RED-001` and `E-M4-LINUX-FINALIZATION-LOCAL-001`. Exact-head
+ Linux runner proof remains open.
- [ ] Embed the exact signed manifest and accepted keys in each desktop build.
- [ ] Upload to a draft release, read back, re-hash, and execute the downloaded archives.
- [ ] Test timeouts, retries, approval denial, signing failure, partial output, and draft recovery.
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 e643b171d43..1ad2c5ecbcc 100644
--- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md
+++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md
@@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps
Date created: 2026-07-14
Last updated: 2026-07-15
-Current phase: Milestone 4 / Work Package 3 manual platform-native signing rehearsal caller — **Closed as a capability; real dispatch blocked — 2026-07-15, Codex implementation owner**. The disconnected signing workflow and its authenticated reconstruction/finalization contracts are closed locally and on all six exact-head Node 24 native build jobs under E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-RED-001, E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-001, E-M4-NATIVE-SIGNING-WORKFLOW-CI-RED-001, and E-M4-NATIVE-SIGNING-WORKFLOW-CI-001. The manual-only caller is closed locally and on all six native build jobs under E-M4-NATIVE-SIGNING-REHEARSAL-LOCAL-RED-001, E-M4-NATIVE-SIGNING-REHEARSAL-LOCAL-001, and E-M4-NATIVE-SIGNING-REHEARSAL-CI-001. It binds typed authorization to the exact source, serializes rehearsals without cancelling approval in flight, and leaves ordinary baseline qualification default-on. Real dispatch remains blocked by GitHub's default-branch rule under E-M4-NATIVE-SIGNING-REHEARSAL-DISPATCH-BLOCKED-001; merging to `main` is prohibited. The next safe package is the disconnected fail-closed aggregate/immutable-manifest-signing job contract. Release-cut, desktop builds, publication, and every tuple remain disconnected. Production/default behavior is unchanged.
-Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact head `c2db2b62e544139176d0993b1340f9d0c706e017` passes the new rehearsal/build contracts on all six native jobs in artifact run 29417449971 under `E-M4-NATIVE-SIGNING-REHEARSAL-CI-001`; all six full runtime builds, both Linux supplements, Windows x64 baseline, Golden E2E 29417449936, and PR Checks 29417449960 pass. The artifact run fails only because the unchanged Windows arm64 gate observes hosted build 26200 instead of required build 26100 after successful runtime verification/smoke. The confirmation-bound live dispatch is still refused before run creation under `E-M4-NATIVE-SIGNING-REHEARSAL-DISPATCH-BLOCKED-001`, so no credential was used and real Apple/SignPath/native-trust evidence stays open. Release-cut, desktop builds, publication, tuple enablement, Gatekeeper/notarization, Defender/WDAC, and exact floor gates remain disconnected and open. Legacy remains the production default, and merge to `main` remains prohibited.
+Current phase: Milestone 4 / Work Package 3 Linux aggregate-ready hash-only finalization — **In progress — 2026-07-15, Codex implementation owner**. Exact-head signing-workflow and manual-caller capabilities are closed, while live credentialed dispatch remains blocked by GitHub's default-branch rule under E-M4-NATIVE-SIGNING-REHEARSAL-DISPATCH-BLOCKED-001. Aggregate design inspection found that macOS/Windows finalization emits verified four-file tuple inputs but Linux build artifacts stop before the same tuple-descriptor boundary. The active narrow package adds Linux-only full archive/tree/Node/PTY/watcher verification, derives the complete hash-only native report, re-verifies tree/assets, and emits the aggregate descriptor/receipt. It must fail closed on non-Linux, stale identity, mutation, smoke failure, or partial output. The fail-closed aggregate/manifest-signing job remains next. Release-cut, desktop builds, publication, and every tuple stay disconnected; production/default behavior is unchanged.
+Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — Linux aggregate-ready finalization is locally green under `E-M4-LINUX-FINALIZATION-LOCAL-RED-001` and `E-M4-LINUX-FINALIZATION-LOCAL-001`. The Linux-only module copies the three stable build assets into exclusive output, verifies archive/full tree and bundled Node/PTY/watcher, derives every native hash from the validated signing plan, then invokes the existing tuple producer to re-verify tree/archive/SBOM/provenance before emitting the descriptor and bound receipt. Any failure removes the entire candidate. Both Linux native build jobs invoke it before upload, and both POSIX/Windows contract families execute its tests. Focused 3 files / 18 tests, broad 48 files / 246 tests, desktop parity 3 files / 48 tests, typecheck, full lint/reliability/localization/max-lines, focused syntax/lint/format, and `git diff --check` pass. Exact-head native runner execution remains required. No aggregate workflow, credential, release consumer, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default, and merge to `main` remains prohibited.
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)
@@ -10535,6 +10535,78 @@ ssh-relay-runtime-native-signing-rehearsal.yml not found on the default branch`.
- Follow-up: checkpoint this evidence, keep the dispatch blocker open, and begin the next
disconnected aggregate/manifest-signing job contract without credentials or production consumers.
+### E-M4-LINUX-FINALIZATION-LOCAL-RED-001 — Linux aggregate-ready finalization is absent
+
+- Date: 2026-07-15
+- Owner: Codex implementation owner
+- Source: uncommitted purpose-named test atop pushed head `0be32c5cc`; draft PR #8741.
+- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. Local test import
+ only; no Actions runner, credential, signer, SSH host, publication, desktop consumer, or enabled
+ tuple.
+- Command:
+ `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-linux-finalization.test.mjs`.
+- Result: expected FAIL before collection because
+ `config/scripts/ssh-relay-runtime-linux-finalization.mjs` is absent; 157 ms Vitest / 0.89 seconds
+ wall, 131,891,200-byte maximum RSS, 96,048,480-byte peak footprint, zero swaps.
+- Oracle proved: existing Linux build output has no capability that converts its already verified
+ hash-only runtime into the same exact four-file aggregate boundary emitted after macOS/Windows
+ signing. The RED defines Linux-only validation, verify-before-descriptor ordering, complete native
+ hash projection, exclusive output, failure cleanup, receipt binding, and strict CLI arguments.
+- Does not prove: implementation, real runtime/archive execution, native CI, aggregate workflow,
+ manifest signing, publication, desktop embedding, SSH behavior, or an enabled tuple.
+- Follow-up: implement the Linux-only finalizer, wire it into both Linux native build jobs, and run
+ focused/broad local gates before exact-head runner execution.
+
+### E-M4-LINUX-FINALIZATION-LOCAL-001 — Linux aggregate-ready finalization passes locally
+
+- Date: 2026-07-15
+- Owner: Codex implementation owner
+- Source: local implementation atop pushed head `0be32c5cc`; draft PR #8741.
+- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. Local generated
+ fixtures and workflow-source contracts only; no Linux runtime execution, Actions credential,
+ signer, SSH host, publication, desktop consumer, or enabled tuple.
+- Implementation-iteration evidence: the first implementation run rejects the old abbreviated test
+ manifest through the production closure validator and rejects the non-Linux spoof before output;
+ the test is corrected to inject only the already-proven signing-plan dependency. Subsequent REDs
+ expose macOS `/var` → `/private/var` physical-path assumptions in the test; production comparison
+ is corrected to real paths before GREEN. No production closure, identity, or path validation is
+ weakened.
+- Commands and final results:
+ - Focused finalizer/tuple/workflow suite:
+ `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-linux-finalization.test.mjs config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs`
+ — PASS, 3 files / 18 tests in 896 ms test / 1.59 seconds wall, 140,427,264-byte maximum RSS,
+ 96,261,496-byte peak footprint, zero swaps. The final post-format rerun also passes 3/18 in
+ 833 ms.
+ - Broad release suite:
+ `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay*.test.mjs`
+ — PASS, 48 files / 246 tests in 8.61 seconds test / 9.56 seconds wall,
+ 189,005,824-byte maximum RSS, 96,097,680-byte peak footprint, zero swaps.
+ - Desktop manifest/signature parity:
+ `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-artifact-schema.test.ts src/main/ssh/ssh-relay-manifest-signature.test.ts src/main/ssh/ssh-relay-release-asset.test.ts`
+ — PASS, 3 files / 48 tests in 504 ms test / 1.19 seconds wall, 131,973,120-byte maximum RSS,
+ 96,097,584-byte peak footprint, zero swaps.
+ - `/usr/bin/time -l pnpm run typecheck` — PASS in 1.99 seconds wall,
+ 1,254,457,344-byte maximum RSS, 95,835,440-byte peak footprint, zero swaps.
+ - `/usr/bin/time -l pnpm run lint` — PASS in 9.42 seconds wall, 2,000,797,696-byte maximum RSS,
+ 95,720,656-byte peak footprint, zero swaps. All 41 reliability gates, 355-entry max-lines ratchet,
+ bundled-skill guides, localization catalog/parity, and localization coverage pass; all 26
+ warnings are in untouched existing files.
+ - Focused `node --check`, `oxlint`, `oxfmt --check`, `git diff --check`, and zero diff in the two
+ user-owned Node/npm resolver files — PASS.
+- Oracle proved: only exact Linux identities from the named physical source output are accepted.
+ Source and exclusive output are physically disjoint; symlinked/empty/mutating input assets fail.
+ The existing archive/tree/bundled smoke runs before descriptor generation. The complete native
+ file report is derived from the validated hash-only plan, and the existing tuple producer rechecks
+ tree/archive/metadata before the descriptor. A receipt binds tuple/content identity, verification,
+ and exact aggregate input. Any failure removes all candidate output. Linux build jobs copy only the
+ descriptor/receipt into their existing unpublished evidence after success; ordinary baseline jobs
+ remain unchanged.
+- Does not prove: execution on Linux x64/arm64, a real archive/tree/smoke through the new call,
+ aggregate workflow, manifest credential/signature, publication/read-back, desktop embedding, SSH
+ behavior, or an enabled tuple.
+- Follow-up: commit/push and require both Linux native jobs plus all-six contract families to pass at
+ the exact head. Inspect the emitted descriptor/receipt before starting aggregate job wiring.
+
## Accepted Gaps
No product gap is accepted merely because it appears in this list. Each entry requires explicit