From f2b387b21bbe6a3863ff1a492a1a03b65e0a0477 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 09:03:25 -0400 Subject: [PATCH 001/267] build(ssh): assemble relay runtime artifacts Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 162 ++++++++ config/scripts/build-ssh-relay-runtime.mjs | 234 ++++++++++++ config/scripts/ssh-relay-node-pty-build.mjs | 141 +++++++ .../ssh-relay-node-release-contract.mjs | 215 +++++++++++ ...h-relay-node-release-file-verification.mjs | 76 ++++ .../ssh-relay-node-release-signature.mjs | 153 ++++++++ .../ssh-relay-node-release-verification.mjs | 6 + ...h-relay-node-release-verification.test.mjs | 288 +++++++++++++++ .../scripts/ssh-relay-node-tar-inspection.mjs | 296 +++++++++++++++ .../ssh-relay-node-tar-inspection.test.mjs | 160 ++++++++ config/scripts/ssh-relay-runtime-archive.mjs | 201 ++++++++++ .../ssh-relay-runtime-artifact.test.mjs | 207 +++++++++++ config/scripts/ssh-relay-runtime-identity.mjs | 64 ++++ .../scripts/ssh-relay-runtime-provenance.mjs | 159 ++++++++ .../scripts/ssh-relay-runtime-smoke-child.cjs | 158 ++++++++ config/scripts/ssh-relay-runtime-tree.mjs | 346 ++++++++++++++++++ .../ssh-relay-runtime-workflow.test.mjs | 56 +++ .../verify-ssh-relay-node-release-inputs.mjs | 135 +++++++ config/scripts/verify-ssh-relay-runtime.mjs | 154 ++++++++ ...2FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C.asc | 90 +++++ config/ssh-relay-node-release-v24.18.0.json | 49 +++ ...github-release-implementation-checklist.md | 305 +++++++++++++-- ...6-07-14-ssh-relay-github-release-plan.html | 14 +- package.json | 3 + pnpm-lock.yaml | 3 + 25 files changed, 3631 insertions(+), 44 deletions(-) create mode 100644 .github/workflows/ssh-relay-runtime-artifacts.yml create mode 100644 config/scripts/build-ssh-relay-runtime.mjs create mode 100644 config/scripts/ssh-relay-node-pty-build.mjs create mode 100644 config/scripts/ssh-relay-node-release-contract.mjs create mode 100644 config/scripts/ssh-relay-node-release-file-verification.mjs create mode 100644 config/scripts/ssh-relay-node-release-signature.mjs create mode 100644 config/scripts/ssh-relay-node-release-verification.mjs create mode 100644 config/scripts/ssh-relay-node-release-verification.test.mjs create mode 100644 config/scripts/ssh-relay-node-tar-inspection.mjs create mode 100644 config/scripts/ssh-relay-node-tar-inspection.test.mjs create mode 100644 config/scripts/ssh-relay-runtime-archive.mjs create mode 100644 config/scripts/ssh-relay-runtime-artifact.test.mjs create mode 100644 config/scripts/ssh-relay-runtime-identity.mjs create mode 100644 config/scripts/ssh-relay-runtime-provenance.mjs create mode 100644 config/scripts/ssh-relay-runtime-smoke-child.cjs create mode 100644 config/scripts/ssh-relay-runtime-tree.mjs create mode 100644 config/scripts/ssh-relay-runtime-workflow.test.mjs create mode 100644 config/scripts/verify-ssh-relay-node-release-inputs.mjs create mode 100644 config/scripts/verify-ssh-relay-runtime.mjs create mode 100644 config/ssh-relay-node-release-keys/C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C.asc create mode 100644 config/ssh-relay-node-release-v24.18.0.json diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml new file mode 100644 index 00000000000..63a7b9b1b5a --- /dev/null +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -0,0 +1,162 @@ +name: SSH Relay Runtime Artifacts + +on: + pull_request: + paths: + - '.github/workflows/ssh-relay-runtime-artifacts.yml' + - 'config/patches/node-pty@1.1.0.patch' + - 'config/scripts/build-relay.mjs' + - 'config/scripts/*ssh-relay*' + - 'config/ssh-relay-node-release-keys/**' + - 'config/ssh-relay-node-release-v24.18.0.json' + - 'package.json' + - 'pnpm-lock.yaml' + - 'src/main/ipc/parcel-watcher-process-entry.ts' + - 'src/relay/**' + workflow_dispatch: + +concurrency: + group: ssh-relay-runtime-artifacts-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build-posix-runtime: + name: Build ${{ matrix.tuple }} runtime + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-24.04 + tuple: linux-x64-glibc + node_archive: node-v24.18.0-linux-x64.tar.xz + - runner: ubuntu-24.04-arm + tuple: linux-arm64-glibc + node_archive: node-v24.18.0-linux-arm64.tar.xz + - runner: macos-15-intel + tuple: darwin-x64 + node_archive: node-v24.18.0-darwin-x64.tar.xz + - runner: macos-15 + tuple: darwin-arm64 + node_archive: node-v24.18.0-darwin-arm64.tar.xz + runs-on: ${{ matrix.runner }} + timeout-minutes: 20 + + steps: + - name: Checkout exact source revision + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + # Why: artifact evidence must name the exact reviewed source, not GitHub's synthetic merge. + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Setup pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6 + with: + run_install: false + + - name: Setup Node 24.18.0 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 24.18.0 + cache: pnpm + + - name: Record native runner identity + shell: bash + run: | + set -euo pipefail + printf 'requested_runner=%s\n' '${{ matrix.runner }}' + printf 'resolved_image_os=%s\n' "${ImageOS:-unknown}" + printf 'resolved_image_version=%s\n' "${ImageVersion:-unknown}" + printf 'runner_arch=%s\n' "$RUNNER_ARCH" + printf 'runner_environment=%s\n' "$RUNNER_ENVIRONMENT" + uname -a + node --version + printf 'source_commit=%s\n' "$(git rev-parse HEAD)" + + - name: Install Linux build and verification tools + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + sudo apt-get update -qq + sudo apt-get install -y -qq build-essential ca-certificates curl gnupg gpgv python3 xz-utils + + - name: Install macOS signature verifier + if: runner.os == 'macOS' + shell: bash + run: | + set -euo pipefail + brew list gnupg >/dev/null 2>&1 || brew install gnupg + command -v xz + xcodebuild -version + + - name: Install source dependencies without native postinstall + shell: bash + run: | + set -euo pipefail + pnpm install --frozen-lockfile --ignore-scripts + git diff --exit-code package.json pnpm-lock.yaml + + - name: Run runtime artifact contract tests + shell: bash + run: | + set -euo pipefail + 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 \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + + - name: Download exact Node release inputs + shell: bash + run: | + set -euo pipefail + inputs="$RUNNER_TEMP/node-release-inputs" + mkdir -p "$inputs" + base='https://nodejs.org/dist/v24.18.0' + for asset in SHASUMS256.txt SHASUMS256.txt.sig '${{ matrix.node_archive }}'; do + curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \ + --connect-timeout 20 --max-time 300 --retry 2 --retry-delay 2 --retry-all-errors \ + "$base/$asset" --output "$inputs/$asset" + done + printf 'NODE_INPUTS=%s\n' "$inputs" >> "$GITHUB_ENV" + + - name: Build, inspect, and smoke exact runtime + shell: bash + run: | + set -euo pipefail + output="$RUNNER_TEMP/ssh-relay-runtime/${{ matrix.tuple }}" + mkdir -p "$(dirname "$output")" + source_commit=$(git rev-parse HEAD) + source_date_epoch=$(git show -s --format=%ct "$source_commit") + /usr/bin/time -p node config/scripts/build-ssh-relay-runtime.mjs \ + --tuple '${{ matrix.tuple }}' \ + --inputs-directory "$NODE_INPUTS" \ + --output-directory "$output" \ + --source-date-epoch "$source_date_epoch" \ + --git-commit "$source_commit" + identity="$output/orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json" + archive=$(find "$output" -maxdepth 1 -name '*.tar.xz' -print -quit) + /usr/bin/time -p node config/scripts/verify-ssh-relay-runtime.mjs \ + --runtime-directory "$output/runtime" \ + --identity "$identity" \ + --archive "$archive" + if find "$output/runtime" -type f \( -name npm -o -name npx -o -name corepack -o -name '*.map' \) -print -quit | grep -q .; then + echo 'Runtime contains a prohibited package manager or source map.' >&2 + exit 1 + fi + parcel_packages=$(find "$output/runtime/node_modules/@parcel" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ') + test "$parcel_packages" = 2 + mkdir -p "$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}" + cp "$output"/*.tar.xz "$output"/*.spdx.json "$output"/*.provenance.json "$output"/*.identity.json \ + "$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}/" + + - name: Upload unpublished artifact evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: ssh-relay-runtime-${{ matrix.tuple }} + path: runtime-evidence/${{ matrix.tuple }}/ + if-no-files-found: error + retention-days: 7 diff --git a/config/scripts/build-ssh-relay-runtime.mjs b/config/scripts/build-ssh-relay-runtime.mjs new file mode 100644 index 00000000000..00ac12a2808 --- /dev/null +++ b/config/scripts/build-ssh-relay-runtime.mjs @@ -0,0 +1,234 @@ +#!/usr/bin/env node + +import { execFile } from 'node:child_process' +import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { promisify } from 'node:util' + +import { buildPatchedSshRelayNodePty } from './ssh-relay-node-pty-build.mjs' +import { extractVerifiedSshRelayNodeBuildInputs } from './ssh-relay-node-tar-inspection.mjs' +import { + createSshRelayRuntimeArchive, + inspectSshRelayRuntimeArchive +} from './ssh-relay-runtime-archive.mjs' +import { writeSshRelayRuntimeMetadata } from './ssh-relay-runtime-provenance.mjs' +import { assembleSshRelayRuntimeTree } from './ssh-relay-runtime-tree.mjs' +import { validateSshRelayNodeReleaseContract } from './ssh-relay-node-release-verification.mjs' +import { verifyNodeReleaseInputs } from './verify-ssh-relay-node-release-inputs.mjs' + +const execFileAsync = promisify(execFile) +const scriptDirectory = import.meta.dirname +const projectRoot = resolve(scriptDirectory, '..', '..') +const defaultContractPath = resolve(projectRoot, 'config', 'ssh-relay-node-release-v24.18.0.json') +const SUPPORTED_TUPLES = new Set([ + 'linux-x64-glibc', + 'linux-arm64-glibc', + 'darwin-x64', + 'darwin-arm64' +]) +const BUILD_COMMAND_TIMEOUT_MS = 10 * 60 * 1000 + +function valueAfter(argv, index, flag) { + const value = argv[index + 1] + if (!value || value.startsWith('--')) { + throw new Error(`${flag} requires a value`) + } + return value +} + +export function parseBuildArguments(argv) { + const result = { contractPath: defaultContractPath } + for (let index = 0; index < argv.length; index += 1) { + const flag = argv[index] + const value = valueAfter(argv, index, flag) + if (flag === '--tuple') { + result.tuple = value + } else if (flag === '--inputs-directory') { + result.inputsDirectory = resolve(value) + } else if (flag === '--output-directory') { + result.outputDirectory = resolve(value) + } else if (flag === '--contract') { + result.contractPath = resolve(value) + } else if (flag === '--source-date-epoch') { + result.sourceDateEpoch = Number(value) + } else if (flag === '--git-commit') { + result.gitCommit = value + } else { + throw new Error(`Unknown argument: ${flag}`) + } + index += 1 + } + for (const field of [ + 'tuple', + 'inputsDirectory', + 'outputDirectory', + 'sourceDateEpoch', + 'gitCommit' + ]) { + if (result[field] === undefined) { + throw new Error(`Missing required build argument: ${field}`) + } + } + if (!SUPPORTED_TUPLES.has(result.tuple)) { + throw new Error(`Unsupported runtime build tuple: ${result.tuple}`) + } + if (!Number.isSafeInteger(result.sourceDateEpoch) || result.sourceDateEpoch < 0) { + throw new Error('--source-date-epoch must be a non-negative safe integer') + } + if (!/^[0-9a-f]{40}$/.test(result.gitCommit)) { + throw new Error('--git-commit must be a full lowercase SHA-1') + } + return result +} + +function assertTargetNative(tuple) { + const os = tuple.startsWith('linux-') ? 'linux' : 'darwin' + const architecture = tuple.includes('arm64') ? 'arm64' : 'x64' + if (process.platform !== os || process.arch !== architecture) { + throw new Error(`Runtime ${tuple} must be assembled on target-native ${os}/${architecture}`) + } + if (os === 'linux') { + const glibc = process.report?.getReport?.().header?.glibcVersionRuntime + if (typeof glibc !== 'string' || glibc.length === 0) { + throw new Error(`Runtime ${tuple} requires a native glibc build environment`) + } + } +} + +async function run(command, args, options = {}) { + const result = await execFileAsync(command, args, { + ...options, + encoding: 'utf8', + maxBuffer: 16 * 1024 * 1024, + timeout: options.timeout ?? BUILD_COMMAND_TIMEOUT_MS, + windowsHide: true + }) + if (result.stdout) { + process.stdout.write(result.stdout) + } + if (result.stderr) { + process.stderr.write(result.stderr) + } + return result.stdout.trim() +} + +async function toolchainDetails(nodePath) { + const commands = [ + ['bundledNode', nodePath, ['--version']], + ['xz', 'xz', ['--version']], + ['compiler', process.platform === 'win32' ? 'cl.exe' : 'c++', ['--version']], + [ + 'strip', + process.platform === 'darwin' + ? 'xcrun' + : process.platform === 'win32' + ? 'llvm-strip.exe' + : 'strip', + process.platform === 'darwin' ? ['--find', 'strip'] : ['--version'] + ], + ['python', process.platform === 'win32' ? 'python.exe' : 'python3', ['--version']] + ] + const details = {} + for (const [name, command, args] of commands) { + details[name] = (await run(command, args)).split(/\r?\n/)[0] + } + details.buildNode = process.version + return details +} + +function relayPlatform(tuple) { + return tuple.replace('-glibc', '') +} + +export async function buildSshRelayRuntime(options) { + const started = process.hrtime.bigint() + assertTargetNative(options.tuple) + const release = validateSshRelayNodeReleaseContract( + JSON.parse(await readFile(options.contractPath, 'utf8')) + ) + await verifyNodeReleaseInputs({ + contractPath: options.contractPath, + inputsDirectory: options.inputsDirectory, + archiveTuples: [options.tuple] + }) + const workDirectory = await mkdtemp(join(tmpdir(), `orca-runtime-${options.tuple}-`)) + // Why: the final directory is still created exclusively, but callers need not pre-create parents. + await mkdir(dirname(options.outputDirectory), { recursive: true }) + await mkdir(options.outputDirectory) + try { + await run(process.execPath, [resolve(scriptDirectory, 'build-relay.mjs')], { cwd: projectRoot }) + const extractedDirectory = join(workDirectory, 'node-inputs') + const extracted = await extractVerifiedSshRelayNodeBuildInputs( + release, + options.tuple, + join(options.inputsDirectory, release.archives[options.tuple].name), + extractedDirectory + ) + const bundledVersion = await run(extracted.nodePath, ['--version']) + if (bundledVersion !== `v${release.nodeVersion}`) { + throw new Error(`Bundled Node version mismatch: ${bundledVersion}`) + } + const nodePty = await buildPatchedSshRelayNodePty({ + projectRoot, + nodePath: extracted.nodePath, + nodeRoot: extracted.extractedRoot, + nodeVersion: release.nodeVersion, + tuple: options.tuple, + buildDirectory: join(workDirectory, 'node-pty') + }) + const runtimeRoot = join(options.outputDirectory, 'runtime') + const identity = await assembleSshRelayRuntimeTree({ + tuple: options.tuple, + nodeRoot: extracted.extractedRoot, + nodePtyBuildDirectory: nodePty.buildDirectory, + relayDirectory: resolve(projectRoot, 'out', 'relay', relayPlatform(options.tuple)), + runtimeRoot, + nodeVersion: release.nodeVersion + }) + const archive = await createSshRelayRuntimeArchive({ + runtimeRoot, + outputDirectory: options.outputDirectory, + identity, + sourceDateEpoch: options.sourceDateEpoch + }) + const inspection = await inspectSshRelayRuntimeArchive(archive.path, identity) + const toolchain = await toolchainDetails(extracted.nodePath) + const metadata = await writeSshRelayRuntimeMetadata({ + outputDirectory: options.outputDirectory, + identity, + archive, + nodeRelease: release, + sourceDateEpoch: options.sourceDateEpoch, + gitCommit: options.gitCommit, + builder: process.env.GITHUB_WORKFLOW_REF ?? `local://${process.platform}/${process.arch}`, + toolchain + }) + return { + tuple: options.tuple, + contentId: identity.contentId, + archive, + inspection, + metadata, + toolchain, + durationMs: Number(process.hrtime.bigint() - started) / 1e6 + } + } catch (error) { + await rm(options.outputDirectory, { recursive: true, force: true }) + throw error + } finally { + await rm(workDirectory, { recursive: true, force: true }) + } +} + +async function main() { + const result = await buildSshRelayRuntime(parseBuildArguments(process.argv.slice(2))) + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`) +} + +if (process.argv[1] && resolve(process.argv[1]) === import.meta.filename) { + main().catch((error) => { + process.stderr.write(`SSH relay runtime build failed: ${error.message}\n`) + process.exitCode = 1 + }) +} diff --git a/config/scripts/ssh-relay-node-pty-build.mjs b/config/scripts/ssh-relay-node-pty-build.mjs new file mode 100644 index 00000000000..64dd7c93ca1 --- /dev/null +++ b/config/scripts/ssh-relay-node-pty-build.mjs @@ -0,0 +1,141 @@ +import { execFile } from 'node:child_process' +import { cp, mkdir, readFile, stat } from 'node:fs/promises' +import { createRequire } from 'node:module' +import { dirname, join, resolve } from 'node:path' +import { promisify } from 'node:util' + +const require = createRequire(import.meta.url) +const execFileAsync = promisify(execFile) +const NODE_GYP_PATH = require.resolve('node-gyp/bin/node-gyp.js') +const NODE_ADDON_API_DIRECTORY = dirname(require.resolve('node-addon-api/package.json')) +const BUILD_TIMEOUT_MS = 10 * 60 * 1000 +const COMMAND_TIMEOUT_MS = 60 * 1000 +const MAX_COMMAND_OUTPUT_BYTES = 16 * 1024 * 1024 + +async function runCommand(command, args, options = {}) { + // Why: native build tools can be noisy or hang; both cases must settle within explicit bounds. + const result = await execFileAsync(command, args, { + ...options, + encoding: 'utf8', + maxBuffer: MAX_COMMAND_OUTPUT_BYTES, + timeout: options.timeout ?? COMMAND_TIMEOUT_MS, + windowsHide: true + }) + if (result.stdout) { + process.stdout.write(result.stdout) + } + if (result.stderr) { + process.stderr.write(result.stderr) + } + return result +} + +function sourceFilter(source) { + const normalized = source.replaceAll('\\', '/') + return !/(?:^|\/)build(?:\/|$)/.test(normalized) && !/(?:^|\/)prebuilds(?:\/|$)/.test(normalized) +} + +async function assertPatchedSource(nodePtyDirectory) { + const [unixTerminal, ptySource, windowsAgent] = await Promise.all([ + readFile(join(nodePtyDirectory, 'lib', 'unixTerminal.js'), 'utf8'), + readFile(join(nodePtyDirectory, 'src', 'unix', 'pty.cc'), 'utf8'), + readFile(join(nodePtyDirectory, 'lib', 'conpty_console_list_agent.js'), 'utf8') + ]) + if ( + !unixTerminal.includes("if (!helperPath.includes('app.asar.unpacked'))") || + !ptySource.includes('pty_format_spawn_error') || + !windowsAgent.includes('consoleProcessList = [shellPid]') + ) { + throw new Error('node-pty source is missing Orca-required patch markers') + } +} + +async function assertBuiltArtifacts(buildDirectory, tuple) { + const releaseDirectory = join(buildDirectory, 'build', 'Release') + const ptyPath = join(releaseDirectory, 'pty.node') + if (!(await stat(ptyPath)).isFile()) { + throw new Error('node-pty did not produce build/Release/pty.node') + } + if (tuple.startsWith('darwin-')) { + const helperPath = join(releaseDirectory, 'spawn-helper') + const helper = await stat(helperPath) + if (!helper.isFile() || (helper.mode & 0o111) === 0) { + throw new Error('macOS node-pty did not produce executable build/Release/spawn-helper') + } + } +} + +async function stripBuiltArtifacts(buildDirectory, tuple) { + const releaseDirectory = join(buildDirectory, 'build', 'Release') + const ptyPath = join(releaseDirectory, 'pty.node') + if (tuple.startsWith('linux-')) { + await runCommand('strip', ['--strip-unneeded', ptyPath], { + windowsHide: true, + env: process.env + }) + } else if (tuple.startsWith('darwin-')) { + await runCommand('strip', ['-S', ptyPath, join(releaseDirectory, 'spawn-helper')], { + windowsHide: true, + env: process.env + }) + } +} + +export async function buildPatchedSshRelayNodePty({ + projectRoot, + nodePath, + nodeRoot, + nodeVersion, + tuple, + buildDirectory +}) { + const sourceDirectory = resolve(projectRoot, 'node_modules', 'node-pty') + await assertPatchedSource(sourceDirectory) + await mkdir(buildDirectory) + await cp(sourceDirectory, buildDirectory, { + recursive: true, + dereference: true, + filter: sourceFilter + }) + await mkdir(join(buildDirectory, 'node_modules'), { recursive: true }) + await cp(NODE_ADDON_API_DIRECTORY, join(buildDirectory, 'node_modules', 'node-addon-api'), { + recursive: true, + dereference: true + }) + + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), BUILD_TIMEOUT_MS) + try { + await runCommand(nodePath, [NODE_GYP_PATH, 'rebuild', '--release', `--nodedir=${nodeRoot}`], { + cwd: buildDirectory, + signal: controller.signal, + timeout: BUILD_TIMEOUT_MS, + windowsHide: true, + env: { + ...process.env, + PATH: `${dirname(nodePath)}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH ?? ''}`, + npm_config_arch: tuple.includes('arm64') ? 'arm64' : 'x64', + npm_config_build_from_source: 'true', + npm_config_nodedir: nodeRoot, + npm_config_target: nodeVersion + } + }) + } finally { + clearTimeout(timeout) + } + await assertBuiltArtifacts(buildDirectory, tuple) + await stripBuiltArtifacts(buildDirectory, tuple) + + // Why: loading with the bundled executable catches an accidental host-ABI build immediately. + await runCommand( + nodePath, + [ + '-e', + `const {loadNativeModule}=require(${JSON.stringify(join(buildDirectory, 'lib', 'utils.js'))});` + + `const loaded=loadNativeModule('pty');` + + `if(!loaded.dir.replace(/\\\\/g,'/').includes('build/Release/'))process.exit(2);` + ], + { cwd: buildDirectory, windowsHide: true, env: process.env } + ) + return { buildDirectory, releaseDirectory: join(buildDirectory, 'build', 'Release') } +} diff --git a/config/scripts/ssh-relay-node-release-contract.mjs b/config/scripts/ssh-relay-node-release-contract.mjs new file mode 100644 index 00000000000..4170f9f34bf --- /dev/null +++ b/config/scripts/ssh-relay-node-release-contract.mjs @@ -0,0 +1,215 @@ +import { createHash } from 'node:crypto' + +const EXPECTED_TUPLES = Object.freeze([ + 'darwin-arm64', + 'darwin-x64', + 'linux-arm64-glibc', + 'linux-x64-glibc', + 'win32-arm64', + 'win32-x64' +]) + +const ARCHIVE_SUFFIXES = Object.freeze({ + 'darwin-arm64': 'darwin-arm64.tar.xz', + 'darwin-x64': 'darwin-x64.tar.xz', + 'linux-arm64-glibc': 'linux-arm64.tar.xz', + 'linux-x64-glibc': 'linux-x64.tar.xz', + 'win32-arm64': 'win-arm64.zip', + 'win32-x64': 'win-x64.zip' +}) + +const HEX_SHA256 = /^[0-9a-f]{64}$/ +const OPENPGP_FINGERPRINT = /^[0-9A-F]{40}$/ +const NODE_VERSION = /^\d+\.\d+\.\d+$/ +const MAX_METADATA_BYTES = 16 * 1024 * 1024 +const MAX_SIGNATURE_BYTES = 1024 * 1024 +const MAX_ARCHIVE_BYTES = 8 * 1024 * 1024 * 1024 + +function assertRecord(value, label) { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`${label} must be an object`) + } +} + +function assertExactKeys(value, keys, label) { + const actual = Object.keys(value).sort() + const expected = [...keys].sort() + if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) { + throw new Error(`${label} contains unknown or missing fields`) + } +} + +function assertSha256(value, label) { + if (typeof value !== 'string' || !HEX_SHA256.test(value)) { + throw new Error(`${label} must be a lowercase SHA-256 digest`) + } +} + +function assertByteLimit(value, maximum, label) { + if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) { + throw new Error(`${label} must be a positive safe integer no larger than ${maximum}`) + } +} + +function expectedArchiveName(nodeVersion, tuple) { + return `node-v${nodeVersion}-${ARCHIVE_SUFFIXES[tuple]}` +} + +function validateMetadataContract(release) { + assertRecord(release.checksumDocument, 'checksum document') + assertExactKeys(release.checksumDocument, ['name', 'sha256', 'maximumBytes'], 'checksum document') + if (release.checksumDocument.name !== 'SHASUMS256.txt') { + throw new Error('checksum document name must be SHASUMS256.txt') + } + assertSha256(release.checksumDocument.sha256, 'checksum document SHA-256') + assertByteLimit( + release.checksumDocument.maximumBytes, + MAX_METADATA_BYTES, + 'checksum document size limit' + ) + + assertRecord(release.signature, 'signature') + assertExactKeys( + release.signature, + ['name', 'sha256', 'maximumBytes', 'signerFingerprint', 'key'], + 'signature' + ) + if (release.signature.name !== 'SHASUMS256.txt.sig') { + throw new Error('signature name must be SHASUMS256.txt.sig') + } + assertSha256(release.signature.sha256, 'signature SHA-256') + assertByteLimit(release.signature.maximumBytes, MAX_SIGNATURE_BYTES, 'signature size limit') + if (!OPENPGP_FINGERPRINT.test(release.signature.signerFingerprint)) { + throw new Error('signature signer fingerprint must be 40 uppercase hexadecimal characters') + } + + assertRecord(release.signature.key, 'signature key') + assertExactKeys( + release.signature.key, + ['path', 'sha256', 'sourceCommit', 'sourceUrl'], + 'signature key' + ) + if (typeof release.signature.key.path !== 'string' || release.signature.key.path.length === 0) { + throw new Error('signature key path must be a non-empty string') + } + assertSha256(release.signature.key.sha256, 'signature key SHA-256') + if (!/^[0-9a-f]{40}$/.test(release.signature.key.sourceCommit)) { + throw new Error('signature key source commit must be a full lowercase SHA-1') + } + const expectedKeyUrl = + `https://raw.githubusercontent.com/nodejs/release-keys/${release.signature.key.sourceCommit}` + + `/keys/${release.signature.signerFingerprint}.asc` + if (release.signature.key.sourceUrl !== expectedKeyUrl) { + throw new Error('signature key source URL must use its exact immutable repository commit') + } +} + +function validateArchives(release) { + assertByteLimit(release.maximumArchiveBytes, MAX_ARCHIVE_BYTES, 'archive size limit') + assertRecord(release.archives, 'archives') + const tuples = Object.keys(release.archives).sort() + if ( + tuples.length !== EXPECTED_TUPLES.length || + tuples.some((tuple, index) => tuple !== EXPECTED_TUPLES[index]) + ) { + throw new Error(`archive tuple set must be exactly: ${EXPECTED_TUPLES.join(', ')}`) + } + + for (const tuple of EXPECTED_TUPLES) { + const archive = release.archives[tuple] + assertRecord(archive, `archive ${tuple}`) + assertExactKeys(archive, ['name', 'sha256'], `archive ${tuple}`) + if (archive.name !== expectedArchiveName(release.nodeVersion, tuple)) { + throw new Error(`archive ${tuple} name does not match the pinned Node version and tuple`) + } + assertSha256(archive.sha256, `archive ${tuple} SHA-256`) + } +} + +export function sha256(bytes) { + return createHash('sha256').update(bytes).digest('hex') +} + +export function validateSshRelayNodeReleaseContract(release) { + assertRecord(release, 'Node release contract') + assertExactKeys( + release, + [ + 'schemaVersion', + 'nodeVersion', + 'baseUrl', + 'checksumDocument', + 'signature', + 'maximumArchiveBytes', + 'archives' + ], + 'Node release contract' + ) + if (release.schemaVersion !== 1) { + throw new Error('Node release contract schemaVersion must be 1') + } + if (typeof release.nodeVersion !== 'string' || !NODE_VERSION.test(release.nodeVersion)) { + throw new Error('Node version must be an exact three-component release') + } + if (release.baseUrl !== `https://nodejs.org/dist/v${release.nodeVersion}`) { + throw new Error('Node release base URL must be the immutable exact-version nodejs.org URL') + } + + validateMetadataContract(release) + validateArchives(release) + return release +} + +function parseChecksumLines(bytes) { + let source + try { + source = new TextDecoder('utf-8', { fatal: true }).decode(bytes) + } catch { + throw new Error('Node checksum document must be valid UTF-8') + } + const lines = source.endsWith('\n') ? source.slice(0, -1).split('\n') : source.split('\n') + const entries = new Map() + for (const line of lines) { + const match = /^([0-9a-f]{64}) ([!-~]+)$/.exec(line) + if (!match) { + throw new Error('Node checksum document contains a malformed line') + } + const [, digest, name] = match + if (entries.has(name)) { + throw new Error(`Node checksum document contains duplicate entry: ${name}`) + } + entries.set(name, digest) + } + return entries +} + +export function verifySshRelayNodeChecksumDocument(releaseInput, bytesInput) { + const release = validateSshRelayNodeReleaseContract(releaseInput) + if (!Buffer.isBuffer(bytesInput) && !(bytesInput instanceof Uint8Array)) { + throw new Error('Node checksum document must be bytes') + } + const bytes = Buffer.from(bytesInput) + if (bytes.length === 0 || bytes.length > release.checksumDocument.maximumBytes) { + throw new Error('Node checksum document exceeds its size limit') + } + + // Parse first so duplicate or malformed names cannot be hidden behind a digest mismatch. + const entries = parseChecksumLines(bytes) + if (sha256(bytes) !== release.checksumDocument.sha256) { + throw new Error('Node checksum document SHA-256 does not match the pinned contract') + } + + return EXPECTED_TUPLES.map((tuple) => { + const archive = release.archives[tuple] + const documentDigest = entries.get(archive.name) + if (documentDigest === undefined) { + throw new Error(`Node checksum document is missing pinned archive: ${archive.name}`) + } + if (documentDigest !== archive.sha256) { + throw new Error(`Node checksum document digest mismatch for pinned archive: ${archive.name}`) + } + return { tuple, name: archive.name, sha256: archive.sha256 } + }) +} + +export { EXPECTED_TUPLES } diff --git a/config/scripts/ssh-relay-node-release-file-verification.mjs b/config/scripts/ssh-relay-node-release-file-verification.mjs new file mode 100644 index 00000000000..1c9b2834d1e --- /dev/null +++ b/config/scripts/ssh-relay-node-release-file-verification.mjs @@ -0,0 +1,76 @@ +import { createHash } from 'node:crypto' +import { createReadStream } from 'node:fs' +import { stat } from 'node:fs/promises' + +import { validateSshRelayNodeReleaseContract } from './ssh-relay-node-release-contract.mjs' + +async function assertBoundedRegularFile(filePath, maximumBytes, label) { + const metadata = await stat(filePath) + if (!metadata.isFile()) { + throw new Error(`${label} must be a regular file`) + } + if (metadata.size === 0 || metadata.size > maximumBytes) { + throw new Error(`${label} exceeds its size limit`) + } + return metadata +} + +export async function readBoundedFile(filePath, maximumBytes, label) { + await assertBoundedRegularFile(filePath, maximumBytes, label) + const chunks = [] + let bytes = 0 + const stream = createReadStream(filePath) + try { + for await (const chunk of stream) { + bytes += chunk.length + if (bytes > maximumBytes) { + throw new Error(`${label} exceeds its size limit`) + } + chunks.push(chunk) + } + } catch (error) { + stream.destroy() + throw error + } + if (bytes === 0) { + throw new Error(`${label} must not be empty`) + } + return Buffer.concat(chunks, bytes) +} + +export async function hashBoundedFile(filePath, maximumBytes, label) { + await assertBoundedRegularFile(filePath, maximumBytes, label) + + const digest = createHash('sha256') + let bytes = 0 + const stream = createReadStream(filePath) + try { + for await (const chunk of stream) { + bytes += chunk.length + if (bytes > maximumBytes) { + throw new Error(`${label} exceeds its size limit`) + } + digest.update(chunk) + } + } catch (error) { + stream.destroy() + throw error + } + if (bytes === 0) { + throw new Error(`${label} must not be empty`) + } + return { bytes, sha256: digest.digest('hex') } +} + +export async function verifySshRelayNodeArchive(releaseInput, tuple, archivePath) { + const release = validateSshRelayNodeReleaseContract(releaseInput) + const archive = release.archives[tuple] + if (archive === undefined) { + throw new Error(`Unknown Node archive tuple: ${String(tuple)}`) + } + const result = await hashBoundedFile(archivePath, release.maximumArchiveBytes, 'Node archive') + if (result.sha256 !== archive.sha256) { + throw new Error(`Node archive SHA-256 mismatch for ${tuple}`) + } + return { tuple, name: archive.name, ...result } +} diff --git a/config/scripts/ssh-relay-node-release-signature.mjs b/config/scripts/ssh-relay-node-release-signature.mjs new file mode 100644 index 00000000000..173d5288666 --- /dev/null +++ b/config/scripts/ssh-relay-node-release-signature.mjs @@ -0,0 +1,153 @@ +import { execFile } from 'node:child_process' +import { createHash } from 'node:crypto' +import { createReadStream } from 'node:fs' +import { mkdtemp, rm, stat, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { promisify } from 'node:util' + +import { validateSshRelayNodeReleaseContract } from './ssh-relay-node-release-contract.mjs' + +const execFileAsync = promisify(execFile) +const MAX_KEY_BYTES = 1024 * 1024 +const MAX_COMMAND_OUTPUT_BYTES = 1024 * 1024 +const COMMAND_TIMEOUT_MS = 60 * 1000 + +async function readVerifiedFile(filePath, maximumBytes, expectedSha256, label) { + const metadata = await stat(filePath) + if (!metadata.isFile() || metadata.size === 0 || metadata.size > maximumBytes) { + throw new Error(`${label} exceeds its size limit or is not a regular file`) + } + + const chunks = [] + const digest = createHash('sha256') + let bytes = 0 + const stream = createReadStream(filePath) + try { + for await (const chunk of stream) { + bytes += chunk.length + if (bytes > maximumBytes) { + throw new Error(`${label} exceeds its size limit`) + } + chunks.push(chunk) + digest.update(chunk) + } + } catch (error) { + stream.destroy() + throw error + } + if (digest.digest('hex') !== expectedSha256) { + throw new Error(`${label} SHA-256 does not match the pinned contract`) + } + return Buffer.concat(chunks, bytes) +} + +async function defaultCommandRunner(command, args) { + try { + const result = await execFileAsync(command, args, { + encoding: 'utf8', + maxBuffer: MAX_COMMAND_OUTPUT_BYTES, + timeout: COMMAND_TIMEOUT_MS, + windowsHide: true + }) + return { exitCode: 0, stdout: result.stdout, stderr: result.stderr } + } catch (error) { + if (error && typeof error === 'object' && ('stdout' in error || 'stderr' in error)) { + return { + exitCode: typeof error.code === 'number' ? error.code : 1, + stdout: typeof error.stdout === 'string' ? error.stdout : '', + stderr: typeof error.stderr === 'string' ? error.stderr : String(error.message ?? error) + } + } + throw error + } +} + +function validateGpgvResult(result, expectedFingerprint) { + const status = `${result.stdout ?? ''}\n${result.stderr ?? ''}` + const fingerprints = [...status.matchAll(/\[GNUPG:\] VALIDSIG ([0-9A-F]{40})(?:\s|$)/g)].map( + (match) => match[1] + ) + if (fingerprints.length !== 1 || fingerprints[0] !== expectedFingerprint) { + throw new Error('gpgv did not report exactly the pinned Node release signer fingerprint') + } +} + +export async function verifySshRelayNodeSignature( + releaseInput, + { checksumPath, signaturePath, keyPath, commandRunner = defaultCommandRunner } +) { + const release = validateSshRelayNodeReleaseContract(releaseInput) + const resolvedKeyPath = keyPath ?? release.signature.key.path + const [keyBytes, checksumBytes, signatureBytes] = await Promise.all([ + readVerifiedFile( + resolvedKeyPath, + MAX_KEY_BYTES, + release.signature.key.sha256, + 'Node release key' + ), + readVerifiedFile( + checksumPath, + release.checksumDocument.maximumBytes, + release.checksumDocument.sha256, + 'Node checksum document' + ), + readVerifiedFile( + signaturePath, + release.signature.maximumBytes, + release.signature.sha256, + 'Node checksum signature' + ) + ]) + + // Verify copies of the bytes we hashed so a path replacement cannot change what gpgv sees. + const directory = await mkdtemp(join(tmpdir(), 'orca-node-signature-')) + try { + const armoredKeyPath = join(directory, 'release-key.asc') + const keyringPath = join(directory, 'release-key.gpg') + const verifiedChecksumPath = join(directory, release.checksumDocument.name) + const verifiedSignaturePath = join(directory, release.signature.name) + await Promise.all([ + writeFile(armoredKeyPath, keyBytes, { mode: 0o600 }), + writeFile(verifiedChecksumPath, checksumBytes, { mode: 0o600 }), + writeFile(verifiedSignaturePath, signatureBytes, { mode: 0o600 }) + ]) + + const dearmor = await commandRunner('gpg', [ + '--batch', + '--yes', + '--dearmor', + '--output', + keyringPath, + armoredKeyPath + ]) + if (dearmor.exitCode !== 0) { + throw new Error( + `gpgv key preparation failed: ${dearmor.stderr || 'gpg exited unsuccessfully'}` + ) + } + + const verified = await commandRunner('gpgv', [ + '--status-fd', + '1', + '--keyring', + keyringPath, + verifiedSignaturePath, + verifiedChecksumPath + ]) + if (verified.exitCode !== 0) { + throw new Error( + `gpgv rejected the Node checksum signature: ${verified.stderr || 'unknown error'}` + ) + } + validateGpgvResult(verified, release.signature.signerFingerprint) + return { + signerFingerprint: release.signature.signerFingerprint, + checksumSha256: release.checksumDocument.sha256, + signatureSha256: release.signature.sha256, + keySha256: release.signature.key.sha256 + } + } finally { + await rm(directory, { recursive: true, force: true }) + } +} diff --git a/config/scripts/ssh-relay-node-release-verification.mjs b/config/scripts/ssh-relay-node-release-verification.mjs new file mode 100644 index 00000000000..11d7bd1abfd --- /dev/null +++ b/config/scripts/ssh-relay-node-release-verification.mjs @@ -0,0 +1,6 @@ +export { + validateSshRelayNodeReleaseContract, + verifySshRelayNodeChecksumDocument +} from './ssh-relay-node-release-contract.mjs' +export { verifySshRelayNodeArchive } from './ssh-relay-node-release-file-verification.mjs' +export { verifySshRelayNodeSignature } from './ssh-relay-node-release-signature.mjs' diff --git a/config/scripts/ssh-relay-node-release-verification.test.mjs b/config/scripts/ssh-relay-node-release-verification.test.mjs new file mode 100644 index 00000000000..62bd04b109f --- /dev/null +++ b/config/scripts/ssh-relay-node-release-verification.test.mjs @@ -0,0 +1,288 @@ +import { createHash } from 'node:crypto' +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { + validateSshRelayNodeReleaseContract, + verifySshRelayNodeArchive, + verifySshRelayNodeChecksumDocument, + verifySshRelayNodeSignature +} from './ssh-relay-node-release-verification.mjs' +import { parseArguments, verifyNodeReleaseInputs } from './verify-ssh-relay-node-release-inputs.mjs' + +const digest = (bytes) => createHash('sha256').update(bytes).digest('hex') +const archives = { + 'linux-x64-glibc': { + name: 'node-v24.18.0-linux-x64.tar.xz', + sha256: '1'.repeat(64) + }, + 'linux-arm64-glibc': { + name: 'node-v24.18.0-linux-arm64.tar.xz', + sha256: '2'.repeat(64) + }, + 'darwin-x64': { + name: 'node-v24.18.0-darwin-x64.tar.xz', + sha256: '3'.repeat(64) + }, + 'darwin-arm64': { + name: 'node-v24.18.0-darwin-arm64.tar.xz', + sha256: '4'.repeat(64) + }, + 'win32-x64': { + name: 'node-v24.18.0-win-x64.zip', + sha256: '5'.repeat(64) + }, + 'win32-arm64': { + name: 'node-v24.18.0-win-arm64.zip', + sha256: '6'.repeat(64) + } +} + +function contract(overrides = {}) { + return { + schemaVersion: 1, + nodeVersion: '24.18.0', + baseUrl: 'https://nodejs.org/dist/v24.18.0', + checksumDocument: { + name: 'SHASUMS256.txt', + sha256: 'a'.repeat(64), + maximumBytes: 1024 * 1024 + }, + signature: { + name: 'SHASUMS256.txt.sig', + sha256: 'b'.repeat(64), + maximumBytes: 64 * 1024, + signerFingerprint: 'C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C', + key: { + path: 'ssh-relay-node-release-keys/C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C.asc', + sha256: 'c'.repeat(64), + sourceCommit: 'd'.repeat(40), + sourceUrl: + `https://raw.githubusercontent.com/nodejs/release-keys/${'d'.repeat(40)}` + + '/keys/C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C.asc' + } + }, + maximumArchiveBytes: 100 * 1024 * 1024, + archives, + ...overrides + } +} + +const temporaryDirectories = [] +afterEach(async () => { + await Promise.all( + temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true })) + ) +}) + +describe('SSH relay Node release verification', () => { + it('pins the reviewed official Node release contract and release key bytes', async () => { + const contractUrl = new URL('../ssh-relay-node-release-v24.18.0.json', import.meta.url) + const release = JSON.parse(await readFile(contractUrl, 'utf8')) + const keyUrl = new URL(`../${release.signature.key.path}`, import.meta.url) + + expect(validateSshRelayNodeReleaseContract(release)).toBe(release) + expect(digest(await readFile(keyUrl))).toBe(release.signature.key.sha256) + expect(release.checksumDocument.sha256).toBe( + '3927bab574a00ca0560c9583fe19655ba19603a1c5851414e4325d34ac50e469' + ) + expect(release.archives['win32-x64'].sha256).toBe( + '0ae68406b42d7725661da979b1403ec9926da205c6770827f33aac9d8f26e821' + ) + }) + + it('accepts only the exact immutable six-archive contract', () => { + expect(validateSshRelayNodeReleaseContract(contract()).nodeVersion).toBe('24.18.0') + + expect(() => + validateSshRelayNodeReleaseContract(contract({ baseUrl: 'https://nodejs.org/dist/latest' })) + ).toThrow(/base URL/i) + expect(() => + validateSshRelayNodeReleaseContract({ + ...contract(), + archives: { ...archives, 'linux-x64-musl': archives['linux-x64-glibc'] } + }) + ).toThrow(/archive tuple/i) + expect(() => + validateSshRelayNodeReleaseContract({ + ...contract(), + archives: { ...archives, 'win32-arm64': { ...archives['win32-arm64'], sha256: 'ABC' } } + }) + ).toThrow(/SHA-256/i) + }) + + it('cross-checks every pinned archive against the authenticated checksum document', () => { + const body = Object.values(archives) + .map((archive) => `${archive.sha256} ${archive.name}`) + .join('\n') + const release = contract({ + checksumDocument: { ...contract().checksumDocument, sha256: digest(body) } + }) + + expect(verifySshRelayNodeChecksumDocument(release, Buffer.from(body))).toHaveLength(6) + expect(() => + verifySshRelayNodeChecksumDocument(release, Buffer.from(`${body}\n${body.split('\n')[0]}`)) + ).toThrow(/duplicate/i) + expect(() => + verifySshRelayNodeChecksumDocument( + release, + Buffer.from(body.replace('1'.repeat(64), 'f'.repeat(64))) + ) + ).toThrow(/checksum document SHA-256/i) + }) + + it('streams and bounds the exact archive before accepting it', async () => { + const directory = await mkdtemp(join(tmpdir(), 'orca-node-release-test-')) + temporaryDirectories.push(directory) + const archivePath = join(directory, archives['linux-x64-glibc'].name) + await writeFile(archivePath, 'verified archive fixture') + const release = contract({ + maximumArchiveBytes: 64, + archives: { + ...archives, + 'linux-x64-glibc': { + ...archives['linux-x64-glibc'], + sha256: digest('verified archive fixture') + } + } + }) + + await expect( + verifySshRelayNodeArchive(release, 'linux-x64-glibc', archivePath) + ).resolves.toMatchObject({ bytes: 24 }) + await writeFile(archivePath, 'x'.repeat(65)) + await expect( + verifySshRelayNodeArchive(release, 'linux-x64-glibc', archivePath) + ).rejects.toThrow(/size limit/i) + }) + + it('requires gpgv success and the exact pinned signing fingerprint', async () => { + const directory = await mkdtemp(join(tmpdir(), 'orca-node-signature-test-')) + temporaryDirectories.push(directory) + const keyPath = join(directory, 'release-key.asc') + const checksumPath = join(directory, 'SHASUMS256.txt') + const signaturePath = join(directory, 'SHASUMS256.txt.sig') + await writeFile(keyPath, 'key') + await writeFile(checksumPath, 'checksums') + await writeFile(signaturePath, 'signature') + const release = contract({ + checksumDocument: { ...contract().checksumDocument, sha256: digest('checksums') }, + signature: { + ...contract().signature, + sha256: digest('signature'), + key: { ...contract().signature.key, path: keyPath, sha256: digest('key') } + } + }) + const commandRunner = async () => ({ + exitCode: 0, + stdout: + '[GNUPG:] VALIDSIG C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C 2026-07-14 0 4 0 1 8 00 C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C\n', + stderr: '' + }) + + await expect( + verifySshRelayNodeSignature(release, { + checksumPath, + signaturePath, + commandRunner + }) + ).resolves.toMatchObject({ signerFingerprint: release.signature.signerFingerprint }) + await expect( + verifySshRelayNodeSignature(release, { + checksumPath, + signaturePath, + commandRunner: async () => ({ exitCode: 1, stdout: '', stderr: 'BAD signature' }) + }) + ).rejects.toThrow(/gpgv/i) + + await expect( + verifySshRelayNodeSignature(release, { + checksumPath, + signaturePath, + commandRunner: async () => ({ + exitCode: 0, + stdout: `[GNUPG:] VALIDSIG ${'D'.repeat(40)} 2026-07-14 0 4 0 1 8 00 ${'D'.repeat(40)}\n`, + stderr: '' + }) + }) + ).rejects.toThrow(/pinned Node release signer fingerprint/i) + }) + + it('verifies an explicitly scoped archive through the purpose-named CLI API', async () => { + const directory = await mkdtemp(join(tmpdir(), 'orca-node-cli-test-')) + temporaryDirectories.push(directory) + const inputsDirectory = join(directory, 'inputs') + await mkdir(inputsDirectory) + const keyPath = join(directory, 'release-key.asc') + const archiveBytes = Buffer.from('archive bytes') + const checksumBody = Object.entries(archives) + .map(([tuple, archive]) => { + const sha256 = tuple === 'linux-x64-glibc' ? digest(archiveBytes) : archive.sha256 + return `${sha256} ${archive.name}` + }) + .join('\n') + const signatureBytes = Buffer.from('signature bytes') + const release = contract({ + checksumDocument: { ...contract().checksumDocument, sha256: digest(checksumBody) }, + signature: { + ...contract().signature, + sha256: digest(signatureBytes), + key: { ...contract().signature.key, path: keyPath, sha256: digest('release key') } + }, + archives: { + ...archives, + 'linux-x64-glibc': { + ...archives['linux-x64-glibc'], + sha256: digest(archiveBytes) + } + } + }) + const contractPath = join(directory, 'release.json') + await Promise.all([ + writeFile(contractPath, JSON.stringify(release)), + writeFile(keyPath, 'release key'), + writeFile(join(inputsDirectory, release.checksumDocument.name), checksumBody), + writeFile(join(inputsDirectory, release.signature.name), signatureBytes), + writeFile(join(inputsDirectory, release.archives['linux-x64-glibc'].name), archiveBytes) + ]) + const commandRunner = vi.fn(async () => ({ + exitCode: 0, + stdout: `[GNUPG:] VALIDSIG ${release.signature.signerFingerprint} signed metadata\n`, + stderr: '' + })) + + const result = await verifyNodeReleaseInputs( + { contractPath, inputsDirectory, archiveTuples: ['linux-x64-glibc'] }, + { commandRunner } + ) + + expect(result).toMatchObject({ + nodeVersion: release.nodeVersion, + signerFingerprint: release.signature.signerFingerprint, + checksumEntriesVerified: 6, + archives: [{ tuple: 'linux-x64-glibc', bytes: archiveBytes.length }] + }) + expect(commandRunner).toHaveBeenCalledTimes(2) + }) + + it('rejects ambiguous or unknown CLI archive scopes', () => { + expect( + parseArguments(['--inputs-directory', '/tmp/node-inputs', '--archive', 'linux-x64-glibc']) + ).toMatchObject({ archiveTuples: ['linux-x64-glibc'] }) + expect(() => + parseArguments([ + '--inputs-directory', + '/tmp/node-inputs', + '--archive', + 'linux-x64-glibc', + '--all-archives' + ]) + ).toThrow(/cannot be combined/i) + expect(() => + parseArguments(['--inputs-directory', '/tmp/node-inputs', '--archive', 'linux-x64-musl']) + ).toThrow(/unknown archive tuple/i) + }) +}) diff --git a/config/scripts/ssh-relay-node-tar-inspection.mjs b/config/scripts/ssh-relay-node-tar-inspection.mjs new file mode 100644 index 00000000000..73771de5cc9 --- /dev/null +++ b/config/scripts/ssh-relay-node-tar-inspection.mjs @@ -0,0 +1,296 @@ +import { spawn } from 'node:child_process' +import { constants } from 'node:fs' +import { copyFile, mkdir, mkdtemp, rm, stat } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join, posix } from 'node:path' +import { pipeline } from 'node:stream/promises' + +import { Parser, Unpack } from 'tar' + +import { verifySshRelayNodeArchive } from './ssh-relay-node-release-file-verification.mjs' +import { validateSshRelayNodeReleaseContract } from './ssh-relay-node-release-contract.mjs' + +const DEFAULT_LIMITS = Object.freeze({ + maximumEntries: 100_000, + maximumExpandedBytes: 1024 * 1024 * 1024, + maximumFileBytes: 256 * 1024 * 1024, + maximumDepth: 32, + maximumPathBytes: 512 +}) +const MAX_XZ_DIAGNOSTIC_BYTES = 64 * 1024 +const XZ_TIMEOUT_MS = 5 * 60 * 1000 +const ALLOWED_TYPES = new Set(['Directory', 'File', 'SymbolicLink']) + +function hasControlOrBackslash(value) { + for (const character of value) { + const code = character.charCodeAt(0) + if (code <= 0x1f || code === 0x7f || character === '\\') { + return true + } + } + return false +} + +function archiveRoot(release, tuple) { + const name = release.archives[tuple]?.name + if (typeof name !== 'string' || !name.endsWith('.tar.xz')) { + throw new Error(`Node tuple ${String(tuple)} does not use a tar.xz archive`) + } + return name.slice(0, -'.tar.xz'.length) +} + +function portablePath(value, limits, label) { + if ( + typeof value !== 'string' || + value.length === 0 || + Buffer.byteLength(value) > limits.maximumPathBytes || + hasControlOrBackslash(value) || + value.startsWith('/') || + /^[A-Za-z]:/.test(value) + ) { + throw new Error(`${label} is not a bounded portable relative path`) + } + const withoutTrailingSlash = value.endsWith('/') ? value.slice(0, -1) : value + const segments = withoutTrailingSlash.split('/') + if (segments.some((segment) => segment === '' || segment === '.' || segment === '..')) { + throw new Error(`${label} contains an unsafe path segment`) + } + return withoutTrailingSlash +} + +function validateSymlink(entryPath, linkPath, root, limits) { + if ( + typeof linkPath !== 'string' || + linkPath.length === 0 || + Buffer.byteLength(linkPath) > limits.maximumPathBytes || + hasControlOrBackslash(linkPath) || + linkPath.startsWith('/') || + /^[A-Za-z]:/.test(linkPath) || + linkPath.split('/').some((segment) => segment === '' || segment === '.') + ) { + throw new Error('Node archive symlink target is not a bounded portable relative path') + } + // Why: official Node npm links use `..`; normalization is safe only inside the signed root. + const resolved = posix.normalize(posix.join(posix.dirname(entryPath), linkPath)) + if (resolved !== root && !resolved.startsWith(`${root}/`)) { + throw new Error('Node archive symlink resolves outside its versioned root') + } +} + +function createInspectionState(root, limits) { + return { + root, + limits, + seen: new Set(), + entries: 0, + files: 0, + directories: 0, + symlinks: 0, + expandedBytes: 0, + largestFileBytes: 0, + nodeMode: null, + hasLicense: false, + hasNodeHeader: false + } +} + +function inspectEntry(entry, state) { + const entryPath = portablePath(entry.path, state.limits, 'Node archive entry path') + if (entryPath !== state.root && !entryPath.startsWith(`${state.root}/`)) { + throw new Error('Node archive entry is outside its exact versioned root') + } + if (entryPath.split('/').length > state.limits.maximumDepth) { + throw new Error('Node archive entry exceeds the nesting-depth limit') + } + if (state.seen.has(entryPath)) { + throw new Error(`Node archive contains a duplicate entry: ${entryPath}`) + } + state.seen.add(entryPath) + state.entries += 1 + if (state.entries > state.limits.maximumEntries) { + throw new Error('Node archive exceeds the entry-count limit') + } + if (!ALLOWED_TYPES.has(entry.type)) { + throw new Error(`Node archive contains prohibited entry type: ${String(entry.type)}`) + } + + if (entry.type === 'File') { + if ( + !Number.isSafeInteger(entry.size) || + entry.size < 0 || + entry.size > state.limits.maximumFileBytes + ) { + throw new Error('Node archive file exceeds the per-file size limit') + } + state.files += 1 + state.expandedBytes += entry.size + state.largestFileBytes = Math.max(state.largestFileBytes, entry.size) + if (state.expandedBytes > state.limits.maximumExpandedBytes) { + throw new Error('Node archive exceeds the expanded-size limit') + } + } else if (entry.type === 'Directory') { + state.directories += 1 + } else { + state.symlinks += 1 + validateSymlink(entryPath, entry.linkpath, state.root, state.limits) + } + + if (entryPath === `${state.root}/bin/node` && entry.type === 'File') { + state.nodeMode = entry.mode + } + if (entryPath === `${state.root}/LICENSE` && entry.type === 'File') { + state.hasLicense = true + } + if (entryPath === `${state.root}/include/node/node.h` && entry.type === 'File') { + state.hasNodeHeader = true + } +} + +function inspectionResult(state) { + if (!Number.isSafeInteger(state.nodeMode) || (state.nodeMode & 0o111) === 0) { + throw new Error('Node archive is missing an executable bin/node') + } + if (!state.hasLicense || !state.hasNodeHeader) { + throw new Error('Node archive is missing its license or build headers') + } + return { + root: state.root, + entries: state.entries, + files: state.files, + directories: state.directories, + symlinks: state.symlinks, + expandedBytes: state.expandedBytes, + largestFileBytes: state.largestFileBytes, + nodeMode: state.nodeMode + } +} + +export async function inspectSshRelayNodeTarStream(readable, releaseInput, tuple, overrides = {}) { + const release = validateSshRelayNodeReleaseContract(releaseInput) + const limits = { ...DEFAULT_LIMITS, ...overrides } + const state = createInspectionState(archiveRoot(release, tuple), limits) + const parser = new Parser({ strict: true }) + parser.on('entry', (entry) => { + try { + inspectEntry(entry, state) + entry.resume() + } catch (error) { + parser.abort(error) + } + }) + await pipeline(readable, parser) + return inspectionResult(state) +} + +async function pipeXzArchive(archivePath, destination, signal) { + const child = spawn('xz', ['--decompress', '--stdout', '--single-stream', '--', archivePath], { + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + signal + }) + let stderr = '' + child.stderr.setEncoding('utf8') + child.stderr.on('data', (chunk) => { + stderr = `${stderr}${chunk}`.slice(-MAX_XZ_DIAGNOSTIC_BYTES) + }) + const completion = new Promise((resolve, reject) => { + child.once('error', reject) + child.once('close', (code, closeSignal) => { + if (code === 0) { + resolve() + } else { + reject(new Error(`xz failed (${code ?? closeSignal ?? 'unknown'}): ${stderr.trim()}`)) + } + }) + }) + try { + await Promise.all([pipeline(child.stdout, destination, { signal }), completion]) + } catch (error) { + if (child.exitCode === null && child.signalCode === null) { + child.kill() + } + await completion.catch(() => {}) + throw error + } +} + +async function inspectXzArchive(archivePath, release, tuple, signal) { + const state = createInspectionState(archiveRoot(release, tuple), DEFAULT_LIMITS) + const parser = new Parser({ strict: true }) + parser.on('entry', (entry) => { + try { + inspectEntry(entry, state) + entry.resume() + } catch (error) { + parser.abort(error) + } + }) + await pipeXzArchive(archivePath, parser, signal) + return inspectionResult(state) +} + +function extractionFilter(root, entryPath, entry) { + if (entry.type !== 'File' && entry.type !== 'Directory') { + return false + } + return ( + entryPath === root || + entryPath === `${root}/bin` || + entryPath === `${root}/bin/node` || + entryPath === `${root}/LICENSE` || + entryPath === `${root}/include` || + entryPath === `${root}/include/node` || + entryPath.startsWith(`${root}/include/node/`) + ) +} + +export async function extractVerifiedSshRelayNodeBuildInputs( + releaseInput, + tuple, + sourceArchivePath, + destination, + { signal } = {} +) { + const release = validateSshRelayNodeReleaseContract(releaseInput) + const effectiveSignal = signal ?? AbortSignal.timeout(XZ_TIMEOUT_MS) + await verifySshRelayNodeArchive(release, tuple, sourceArchivePath) + const stagingDirectory = await mkdtemp(join(tmpdir(), 'orca-node-verified-archive-')) + const stagedArchivePath = join(stagingDirectory, release.archives[tuple].name) + try { + await copyFile(sourceArchivePath, stagedArchivePath, constants.COPYFILE_EXCL) + await verifySshRelayNodeArchive(release, tuple, stagedArchivePath) + const inspection = await inspectXzArchive(stagedArchivePath, release, tuple, effectiveSignal) + await mkdir(destination) + const unpack = new Unpack({ + cwd: destination, + strict: true, + preservePaths: false, + filter: (entryPath, entry) => extractionFilter(inspection.root, entryPath, entry) + }) + await pipeXzArchive(stagedArchivePath, unpack, effectiveSignal) + + const extractedRoot = join(destination, inspection.root) + const nodePath = join(extractedRoot, 'bin', 'node') + const [nodeMetadata, licenseMetadata, headerMetadata] = await Promise.all([ + stat(nodePath), + stat(join(extractedRoot, 'LICENSE')), + stat(join(extractedRoot, 'include', 'node', 'node.h')) + ]) + if ( + !nodeMetadata.isFile() || + (nodeMetadata.mode & 0o111) === 0 || + !licenseMetadata.isFile() || + !headerMetadata.isFile() + ) { + throw new Error('Extracted Node build inputs do not match the inspected archive contract') + } + return { ...inspection, extractedRoot, nodePath } + } catch (error) { + await rm(destination, { recursive: true, force: true }) + throw error + } finally { + await rm(stagingDirectory, { recursive: true, force: true }) + } +} + +export { DEFAULT_LIMITS } diff --git a/config/scripts/ssh-relay-node-tar-inspection.test.mjs b/config/scripts/ssh-relay-node-tar-inspection.test.mjs new file mode 100644 index 00000000000..9c31f757a02 --- /dev/null +++ b/config/scripts/ssh-relay-node-tar-inspection.test.mjs @@ -0,0 +1,160 @@ +import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { Readable } from 'node:stream' + +import { create, Header } from 'tar' +import { afterEach, describe, expect, it } from 'vitest' + +import { inspectSshRelayNodeTarStream } from './ssh-relay-node-tar-inspection.mjs' + +const tuple = 'darwin-arm64' +const root = 'node-v24.18.0-darwin-arm64' +const archives = { + 'darwin-arm64': { + name: `${root}.tar.xz`, + sha256: '1'.repeat(64) + }, + 'darwin-x64': { + name: 'node-v24.18.0-darwin-x64.tar.xz', + sha256: '2'.repeat(64) + }, + 'linux-arm64-glibc': { + name: 'node-v24.18.0-linux-arm64.tar.xz', + sha256: '3'.repeat(64) + }, + 'linux-x64-glibc': { + name: 'node-v24.18.0-linux-x64.tar.xz', + sha256: '4'.repeat(64) + }, + 'win32-arm64': { + name: 'node-v24.18.0-win-arm64.zip', + sha256: '5'.repeat(64) + }, + 'win32-x64': { + name: 'node-v24.18.0-win-x64.zip', + sha256: '6'.repeat(64) + } +} +const release = { + schemaVersion: 1, + nodeVersion: '24.18.0', + baseUrl: 'https://nodejs.org/dist/v24.18.0', + checksumDocument: { + name: 'SHASUMS256.txt', + sha256: 'a'.repeat(64), + maximumBytes: 1024 + }, + signature: { + name: 'SHASUMS256.txt.sig', + sha256: 'b'.repeat(64), + maximumBytes: 1024, + signerFingerprint: 'C'.repeat(40), + key: { + path: 'release-key.asc', + sha256: 'c'.repeat(64), + sourceCommit: 'd'.repeat(40), + sourceUrl: + `https://raw.githubusercontent.com/nodejs/release-keys/${'d'.repeat(40)}` + + `/keys/${'C'.repeat(40)}.asc` + } + }, + maximumArchiveBytes: 1024 * 1024, + archives +} + +const temporaryDirectories = [] +afterEach(async () => { + await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true }))) +}) + +async function validTarStream() { + const directory = await mkdtemp(join(tmpdir(), 'orca-node-tar-test-')) + temporaryDirectories.push(directory) + await Promise.all([ + mkdir(join(directory, root, 'bin'), { recursive: true }), + mkdir(join(directory, root, 'include', 'node'), { recursive: true }) + ]) + await Promise.all([ + writeFile(join(directory, root, 'bin', 'node'), 'node executable'), + writeFile(join(directory, root, 'LICENSE'), 'Node license'), + writeFile(join(directory, root, 'include', 'node', 'node.h'), 'Node header') + ]) + await chmod(join(directory, root, 'bin', 'node'), 0o755) + return create({ cwd: directory, portable: true }, [root]) +} + +function rawTar(entries) { + const blocks = [] + for (const entry of entries) { + const block = Buffer.alloc(512) + new Header({ + path: entry.path, + linkpath: entry.linkpath, + mode: entry.mode ?? 0o644, + uid: 0, + gid: 0, + size: 0, + mtime: new Date(0), + type: entry.type ?? 'File', + uname: '', + gname: '' + }).encode(block) + blocks.push(block) + } + blocks.push(Buffer.alloc(1024)) + return Readable.from(blocks) +} + +describe('SSH relay Node tar inspection', () => { + it('accepts a bounded exact-root archive with executable Node and build inputs', async () => { + await expect( + inspectSshRelayNodeTarStream(await validTarStream(), release, tuple) + ).resolves.toMatchObject({ + root, + files: 3, + expandedBytes: 38, + nodeMode: 0o755 + }) + }) + + it('rejects traversal, escaping links, special entries, and duplicates', async () => { + await expect( + inspectSshRelayNodeTarStream(rawTar([{ path: '../outside' }]), release, tuple) + ).rejects.toThrow(/unsafe path segment/i) + await expect( + inspectSshRelayNodeTarStream( + rawTar([{ path: `${root}/bin/npm`, type: 'SymbolicLink', linkpath: '../../outside' }]), + release, + tuple + ) + ).rejects.toThrow(/outside its versioned root/i) + await expect( + inspectSshRelayNodeTarStream( + rawTar([{ path: `${root}/device`, type: 'CharacterDevice' }]), + release, + tuple + ) + ).rejects.toThrow(/prohibited entry type/i) + await expect( + inspectSshRelayNodeTarStream( + rawTar([{ path: `${root}/LICENSE` }, { path: `${root}/LICENSE` }]), + release, + tuple + ) + ).rejects.toThrow(/duplicate entry/i) + }) + + it('enforces entry and expanded-size limits while streaming', async () => { + await expect( + inspectSshRelayNodeTarStream(await validTarStream(), release, tuple, { + maximumEntries: 2 + }) + ).rejects.toThrow(/entry-count limit/i) + await expect( + inspectSshRelayNodeTarStream(await validTarStream(), release, tuple, { + maximumExpandedBytes: 8 + }) + ).rejects.toThrow(/expanded-size limit/i) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-archive.mjs b/config/scripts/ssh-relay-runtime-archive.mjs new file mode 100644 index 00000000000..526b5768f1f --- /dev/null +++ b/config/scripts/ssh-relay-runtime-archive.mjs @@ -0,0 +1,201 @@ +import { spawn } from 'node:child_process' +import { createHash } from 'node:crypto' +import { createReadStream, createWriteStream } from 'node:fs' +import { mkdtemp, rm, stat } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pipeline } from 'node:stream/promises' + +import { create, Parser } from 'tar' + +const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024 +const MAX_DIAGNOSTIC_BYTES = 64 * 1024 +const XZ_TIMEOUT_MS = 5 * 60 * 1000 + +function archiveName(tuple, contentId) { + const match = /^sha256:([0-9a-f]{64})$/.exec(contentId) + if (!match) { + throw new Error('Runtime content identity is not a SHA-256 digest') + } + return `orca-ssh-relay-runtime-v1-${tuple}-${match[1]}.tar.xz` +} + +async function compressTar(tarPath, archivePath, signal) { + const child = spawn( + 'xz', + ['--compress', '--stdout', '--threads=1', '--check=crc64', '-9e', '--', tarPath], + { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, signal } + ) + let stderr = '' + child.stderr.setEncoding('utf8') + child.stderr.on('data', (chunk) => { + stderr = `${stderr}${chunk}`.slice(-MAX_DIAGNOSTIC_BYTES) + }) + const completion = new Promise((resolve, reject) => { + child.once('error', reject) + child.once('close', (code, closeSignal) => { + if (code === 0) { + resolve() + } else { + reject(new Error(`xz failed (${code ?? closeSignal ?? 'unknown'}): ${stderr.trim()}`)) + } + }) + }) + try { + await Promise.all([ + pipeline(child.stdout, createWriteStream(archivePath, { flags: 'wx', mode: 0o600 }), { + signal + }), + completion + ]) + } catch (error) { + if (child.exitCode === null && child.signalCode === null) { + child.kill() + } + await completion.catch(() => {}) + throw error + } +} + +async function decompressArchive(archivePath, destination, signal) { + const child = spawn('xz', ['--decompress', '--stdout', '--single-stream', '--', archivePath], { + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + signal + }) + let stderr = '' + child.stderr.setEncoding('utf8') + child.stderr.on('data', (chunk) => { + stderr = `${stderr}${chunk}`.slice(-MAX_DIAGNOSTIC_BYTES) + }) + const completion = new Promise((resolve, reject) => { + child.once('error', reject) + child.once('close', (code, closeSignal) => { + if (code === 0) { + resolve() + } else { + reject(new Error(`xz failed (${code ?? closeSignal ?? 'unknown'}): ${stderr.trim()}`)) + } + }) + }) + try { + await Promise.all([pipeline(child.stdout, destination, { signal }), completion]) + } catch (error) { + if (child.exitCode === null && child.signalCode === null) { + child.kill() + } + await completion.catch(() => {}) + throw error + } +} + +export async function createSshRelayRuntimeArchive({ + runtimeRoot, + outputDirectory, + identity, + sourceDateEpoch, + signal +}) { + if (!Number.isSafeInteger(sourceDateEpoch) || sourceDateEpoch < 0) { + throw new Error('Runtime archive SOURCE_DATE_EPOCH must be a non-negative safe integer') + } + const temporaryDirectory = await mkdtemp(join(tmpdir(), 'orca-runtime-tar-')) + const tarPath = join(temporaryDirectory, 'runtime.tar') + const name = archiveName(identity.tupleId, identity.contentId) + const archivePath = join(outputDirectory, name) + const effectiveSignal = signal ?? AbortSignal.timeout(XZ_TIMEOUT_MS) + try { + const paths = identity.entries.map((entry) => entry.path).sort() + await create( + { + cwd: runtimeRoot, + file: tarPath, + portable: true, + noPax: true, + noDirRecurse: true, + mtime: new Date(sourceDateEpoch * 1000) + }, + paths + ) + await compressTar(tarPath, archivePath, effectiveSignal) + const metadata = await stat(archivePath) + if (!metadata.isFile() || metadata.size === 0 || metadata.size > MAX_ARCHIVE_BYTES) { + throw new Error('Runtime archive exceeds the release-manifest compressed-size limit') + } + const digest = createHash('sha256') + for await (const chunk of createReadStream(archivePath)) { + digest.update(chunk) + } + return { + name, + path: archivePath, + size: metadata.size, + sha256: `sha256:${digest.digest('hex')}` + } + } catch (error) { + await rm(archivePath, { force: true }) + throw error + } finally { + await rm(temporaryDirectory, { recursive: true, force: true }) + } +} + +export async function inspectSshRelayRuntimeArchive(archivePath, identity, { signal } = {}) { + const metadata = await stat(archivePath) + if (!metadata.isFile() || metadata.size === 0 || metadata.size > MAX_ARCHIVE_BYTES) { + throw new Error('Runtime archive exceeds the release-manifest compressed-size limit') + } + const expected = new Map(identity.entries.map((entry) => [entry.path, entry])) + const seen = new Set() + const pendingFiles = [] + const parser = new Parser({ strict: true }) + parser.on('entry', (entry) => { + const path = entry.path.endsWith('/') ? entry.path.slice(0, -1) : entry.path + const expectedEntry = expected.get(path) + try { + if (!expectedEntry || seen.has(path)) { + throw new Error(`Runtime archive has extra or duplicate entry: ${path}`) + } + seen.add(path) + const expectedType = expectedEntry.type === 'file' ? 'File' : 'Directory' + if (entry.type !== expectedType || (entry.mode & 0o777) !== expectedEntry.mode) { + throw new Error(`Runtime archive type or mode mismatch: ${path}`) + } + if (expectedEntry.type === 'file') { + if (entry.size !== expectedEntry.size) { + throw new Error(`Runtime archive size mismatch: ${path}`) + } + const digest = createHash('sha256') + entry.on('data', (chunk) => digest.update(chunk)) + pendingFiles.push( + new Promise((resolve, reject) => { + entry.once('error', reject) + entry.once('end', () => { + const actual = `sha256:${digest.digest('hex')}` + if (actual !== expectedEntry.sha256) { + reject(new Error(`Runtime archive SHA-256 mismatch: ${path}`)) + } else { + resolve() + } + }) + }) + ) + } else { + entry.resume() + } + } catch (error) { + parser.abort(error) + } + }) + await decompressArchive(archivePath, parser, signal ?? AbortSignal.timeout(XZ_TIMEOUT_MS)) + await Promise.all(pendingFiles) + const missing = [...expected.keys()].filter((path) => !seen.has(path)) + if (missing.length > 0) { + throw new Error(`Runtime archive is missing declared entry: ${missing[0]}`) + } + return { + entries: seen.size, + files: identity.entries.filter((entry) => entry.type === 'file').length, + expandedBytes: identity.expandedSize + } +} diff --git a/config/scripts/ssh-relay-runtime-artifact.test.mjs b/config/scripts/ssh-relay-runtime-artifact.test.mjs new file mode 100644 index 00000000000..ed8c1e96d11 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-artifact.test.mjs @@ -0,0 +1,207 @@ +import { createHash } from 'node:crypto' +import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { + createSshRelayRuntimeArchive, + inspectSshRelayRuntimeArchive +} from './ssh-relay-runtime-archive.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' +import { verifyRuntimeTree } from './verify-ssh-relay-runtime.mjs' + +const temporaryDirectories = [] +afterEach(async () => { + await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true }))) +}) + +const digest = (value) => `sha256:${createHash('sha256').update(value).digest('hex')}` + +function canonicalFixture() { + const d = (character) => `sha256:${character.repeat(64)}` + return { + tupleId: 'linux-x64-glibc', + os: 'linux', + architecture: 'x64', + compatibility: { + kind: 'linux', + minimumKernelVersion: '4.18', + libc: { + family: 'glibc', + minimumVersion: '2.28', + minimumLibstdcxxVersion: '6.0.25', + minimumGlibcxxVersion: '3.4.25' + } + }, + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries: [ + { path: 'bin', type: 'directory', mode: 0o755 }, + { path: 'bin/node', type: 'file', role: 'node', size: 40, mode: 0o755, sha256: d('1') }, + { path: 'relay.js', type: 'file', role: 'relay', size: 20, mode: 0o644, sha256: d('2') }, + { + path: 'relay-watcher.js', + type: 'file', + role: 'relay-watcher', + size: 15, + mode: 0o644, + sha256: d('3') + }, + { path: 'node_modules', type: 'directory', mode: 0o755 }, + { path: 'node_modules/node-pty', type: 'directory', mode: 0o755 }, + { + path: 'node_modules/node-pty/package.json', + type: 'file', + role: 'runtime-javascript', + size: 10, + mode: 0o644, + sha256: d('4') + }, + { path: 'node_modules/node-pty/build', type: 'directory', mode: 0o755 }, + { path: 'node_modules/node-pty/build/Release', type: 'directory', mode: 0o755 }, + { + path: 'node_modules/node-pty/build/Release/pty.node', + type: 'file', + role: 'node-pty-native', + size: 30, + mode: 0o755, + sha256: d('5') + }, + { path: 'node_modules/@parcel', type: 'directory', mode: 0o755 }, + { path: 'node_modules/@parcel/watcher', type: 'directory', mode: 0o755 }, + { + path: 'node_modules/@parcel/watcher/package.json', + type: 'file', + role: 'runtime-javascript', + size: 10, + mode: 0o644, + sha256: d('6') + }, + { + path: 'node_modules/@parcel/watcher-linux-x64-glibc', + type: 'directory', + mode: 0o755 + }, + { + path: 'node_modules/@parcel/watcher-linux-x64-glibc/watcher.node', + type: 'file', + role: 'parcel-watcher-native', + size: 25, + mode: 0o755, + sha256: d('7') + }, + { + path: 'THIRD_PARTY_LICENSES.txt', + type: 'file', + role: 'license', + size: 10, + mode: 0o644, + sha256: d('8') + } + ] + } +} + +async function tinyRuntime() { + const directory = await mkdtemp(join(tmpdir(), 'orca-runtime-artifact-test-')) + temporaryDirectories.push(directory) + const runtimeRoot = join(directory, 'runtime') + await mkdir(join(runtimeRoot, 'bin'), { recursive: true }) + await Promise.all([ + writeFile(join(runtimeRoot, 'bin', 'node'), 'node'), + writeFile(join(runtimeRoot, 'relay.js'), 'relay') + ]) + await chmod(join(runtimeRoot, 'bin', 'node'), 0o755) + const base = { + tupleId: 'darwin-arm64', + os: 'darwin', + architecture: 'arm64', + compatibility: { kind: 'darwin', minimumVersion: '13.5' }, + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries: [ + { path: 'bin', type: 'directory', mode: 0o755 }, + { + path: 'bin/node', + type: 'file', + role: 'node', + size: 4, + mode: 0o755, + sha256: digest('node') + }, + { + path: 'relay.js', + type: 'file', + role: 'relay', + size: 5, + mode: 0o644, + sha256: digest('relay') + } + ], + fileCount: 2, + expandedSize: 9 + } + return { + directory, + runtimeRoot, + identity: { ...base, contentId: computeSshRelayRuntimeContentId(base) } + } +} + +describe('SSH relay runtime artifact', () => { + it('matches the reviewed TypeScript canonical content-identity vector', () => { + expect(computeSshRelayRuntimeContentId(canonicalFixture())).toBe( + 'sha256:5afe9c8094ec61a5eec6f7be6d1035faacee7362871985c74cc6ee6aceea8677' + ) + }) + + it('requires the complete exact runtime tree before execution', async () => { + const fixture = await tinyRuntime() + await expect(verifyRuntimeTree(fixture.runtimeRoot, fixture.identity)).resolves.toMatchObject({ + entries: 3, + files: 2, + expandedBytes: 9 + }) + await writeFile(join(fixture.runtimeRoot, 'relay.js'), 'tampered') + await expect(verifyRuntimeTree(fixture.runtimeRoot, fixture.identity)).rejects.toThrow( + /integrity mismatch/i + ) + + await writeFile(join(fixture.runtimeRoot, 'relay.js'), 'relay') + await chmod(join(fixture.runtimeRoot, 'relay.js'), 0o600) + await expect(verifyRuntimeTree(fixture.runtimeRoot, fixture.identity)).rejects.toThrow( + /entry mismatch/i + ) + }) + + it.skipIf(process.platform === 'win32')( + 'creates deterministic archives and rehashes every entry', + async () => { + const fixture = await tinyRuntime() + const firstDirectory = join(fixture.directory, 'first') + const secondDirectory = join(fixture.directory, 'second') + await Promise.all([mkdir(firstDirectory), mkdir(secondDirectory)]) + const first = await createSshRelayRuntimeArchive({ + runtimeRoot: fixture.runtimeRoot, + outputDirectory: firstDirectory, + identity: fixture.identity, + sourceDateEpoch: 1_752_710_400 + }) + const second = await createSshRelayRuntimeArchive({ + runtimeRoot: fixture.runtimeRoot, + outputDirectory: secondDirectory, + identity: fixture.identity, + sourceDateEpoch: 1_752_710_400 + }) + + expect(await readFile(first.path)).toEqual(await readFile(second.path)) + await expect(inspectSshRelayRuntimeArchive(first.path, fixture.identity)).resolves.toEqual({ + entries: 3, + files: 2, + expandedBytes: 9 + }) + } + ) +}) diff --git a/config/scripts/ssh-relay-runtime-identity.mjs b/config/scripts/ssh-relay-runtime-identity.mjs new file mode 100644 index 00000000000..c078f989fb4 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-identity.mjs @@ -0,0 +1,64 @@ +import { createHash } from 'node:crypto' + +function canonicalCompatibility(compatibility) { + if (compatibility.kind === 'linux') { + return { + kind: compatibility.kind, + minimumKernelVersion: compatibility.minimumKernelVersion, + libc: { + family: compatibility.libc.family, + minimumVersion: compatibility.libc.minimumVersion, + minimumLibstdcxxVersion: compatibility.libc.minimumLibstdcxxVersion, + minimumGlibcxxVersion: compatibility.libc.minimumGlibcxxVersion + } + } + } + if (compatibility.kind === 'darwin') { + return { kind: compatibility.kind, minimumVersion: compatibility.minimumVersion } + } + return { + kind: compatibility.kind, + minimumBuild: compatibility.minimumBuild, + minimumOpenSshVersion: compatibility.minimumOpenSshVersion, + minimumPowerShellVersion: compatibility.minimumPowerShellVersion, + minimumDotNetFrameworkRelease: compatibility.minimumDotNetFrameworkRelease + } +} + +export function canonicalSshRelayRuntimeIdentityBytes(runtime) { + const entries = [...runtime.entries] + .sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0)) + .map((entry) => + entry.type === 'directory' + ? { path: entry.path, type: entry.type, mode: entry.mode } + : { + path: entry.path, + type: entry.type, + role: entry.role, + size: entry.size, + mode: entry.mode, + sha256: entry.sha256 + } + ) + const projection = { + identitySchemaVersion: 1, + tupleId: runtime.tupleId, + os: runtime.os, + architecture: runtime.architecture, + compatibility: canonicalCompatibility(runtime.compatibility), + nodeVersion: runtime.nodeVersion, + dependencies: { + nodePtyVersion: runtime.dependencies.nodePtyVersion, + parcelWatcherVersion: runtime.dependencies.parcelWatcherVersion + }, + entries + } + return Buffer.from(JSON.stringify(projection), 'utf8') +} + +export function computeSshRelayRuntimeContentId(runtime) { + const hex = createHash('sha256') + .update(canonicalSshRelayRuntimeIdentityBytes(runtime)) + .digest('hex') + return `sha256:${hex}` +} diff --git a/config/scripts/ssh-relay-runtime-provenance.mjs b/config/scripts/ssh-relay-runtime-provenance.mjs new file mode 100644 index 00000000000..9c017ab02b3 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-provenance.mjs @@ -0,0 +1,159 @@ +import { createHash } from 'node:crypto' +import { readFile, stat, writeFile } from 'node:fs/promises' +import { createRequire } from 'node:module' +import { join } from 'node:path' + +const require = createRequire(import.meta.url) + +function sha256(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +function spdxId(value) { + return `SPDXRef-${value.replace(/[^A-Za-z0-9.-]/g, '-')}` +} + +function packageRecord(name, version, license) { + return { + name, + SPDXID: spdxId(`Package-${name}`), + versionInfo: version, + downloadLocation: 'NOASSERTION', + filesAnalyzed: false, + licenseConcluded: license, + licenseDeclared: license, + copyrightText: 'NOASSERTION' + } +} + +function installedPackage(name) { + const parsed = require(`${name}/package.json`) + return packageRecord(parsed.name, parsed.version, parsed.license ?? 'NOASSERTION') +} + +function watcherNativePackage(tuple) { + if (tuple.startsWith('linux-')) { + return `@parcel/watcher-linux-${tuple.slice('linux-'.length)}` + } + return `@parcel/watcher-${tuple}` +} + +async function writeJson(path, value) { + const bytes = Buffer.from(`${JSON.stringify(value, null, 2)}\n`, 'utf8') + await writeFile(path, bytes, { flag: 'wx', mode: 0o600 }) + return { name: path.split(/[\\/]/).at(-1), size: bytes.length, sha256: sha256(bytes) } +} + +export async function writeSshRelayRuntimeMetadata({ + outputDirectory, + identity, + archive, + nodeRelease, + sourceDateEpoch, + gitCommit, + builder, + toolchain +}) { + const createdAt = new Date(sourceDateEpoch * 1000).toISOString() + const sbomName = `orca-ssh-relay-runtime-${identity.tupleId}.spdx.json` + const provenanceName = `orca-ssh-relay-runtime-${identity.tupleId}.provenance.json` + const identityName = `orca-ssh-relay-runtime-${identity.tupleId}.identity.json` + const files = identity.entries + .filter((entry) => entry.type === 'file') + .map((entry, index) => ({ + fileName: entry.path, + SPDXID: `SPDXRef-File-${index + 1}`, + checksums: [{ algorithm: 'SHA256', checksumValue: entry.sha256.slice('sha256:'.length) }], + licenseConcluded: 'NOASSERTION', + copyrightText: 'NOASSERTION' + })) + const packages = [ + packageRecord('node', identity.nodeVersion, 'NOASSERTION'), + packageRecord('orca-ssh-relay', '0.1.0', 'NOASSERTION'), + installedPackage('node-pty'), + installedPackage('@parcel/watcher'), + installedPackage(watcherNativePackage(identity.tupleId)), + installedPackage('detect-libc'), + installedPackage('is-glob'), + installedPackage('is-extglob'), + installedPackage('picomatch') + ] + const documentId = identity.contentId.slice('sha256:'.length) + const sbom = { + spdxVersion: 'SPDX-2.3', + dataLicense: 'CC0-1.0', + SPDXID: 'SPDXRef-DOCUMENT', + name: `Orca SSH relay runtime ${identity.tupleId}`, + documentNamespace: `https://github.com/stablyai/orca/ssh-relay-runtime/${documentId}`, + creationInfo: { created: createdAt, creators: ['Organization: Stably AI'] }, + packages, + files, + relationships: packages.map((component) => ({ + spdxElementId: 'SPDXRef-DOCUMENT', + relationshipType: 'DESCRIBES', + relatedSpdxElement: component.SPDXID + })) + } + const nativeFiles = identity.entries + .filter( + (entry) => + entry.type === 'file' && + ['node', 'node-pty-native', 'parcel-watcher-native', 'native-runtime'].includes(entry.role) + ) + .map((entry) => ({ path: entry.path, sha256: entry.sha256 })) + const provenance = { + _type: 'https://in-toto.io/Statement/v1', + subject: [{ name: archive.name, digest: { sha256: archive.sha256.slice('sha256:'.length) } }], + predicateType: 'https://slsa.dev/provenance/v1', + predicate: { + buildDefinition: { + buildType: 'https://github.com/stablyai/orca/ssh-relay-runtime-build/v1', + externalParameters: { + tuple: identity.tupleId, + nodeVersion: identity.nodeVersion, + sourceDateEpoch + }, + internalParameters: { toolchain }, + resolvedDependencies: [ + { + uri: nodeRelease.baseUrl, + digest: { sha256: nodeRelease.archives[identity.tupleId].sha256 } + }, + { + uri: nodeRelease.signature.key.sourceUrl, + digest: { sha256: nodeRelease.signature.key.sha256 } + }, + { uri: `git+https://github.com/stablyai/orca@${gitCommit}`, digest: { gitCommit } } + ] + }, + runDetails: { + builder: { id: builder }, + metadata: { invocationId: process.env.GITHUB_RUN_ID ?? 'local-unpublished-build' }, + byproducts: [{ name: 'native-files', content: nativeFiles }] + } + } + } + const identityDocument = { + ...identity, + archive: { + name: archive.name, + size: archive.size, + expandedSize: identity.expandedSize, + fileCount: identity.fileCount, + sha256: archive.sha256 + } + } + const [sbomAsset, provenanceAsset, identityAsset] = await Promise.all([ + writeJson(join(outputDirectory, sbomName), sbom), + writeJson(join(outputDirectory, provenanceName), provenance), + writeJson(join(outputDirectory, identityName), identityDocument) + ]) + for (const asset of [sbomAsset, provenanceAsset, identityAsset]) { + const metadata = await stat(join(outputDirectory, asset.name)) + const bytes = await readFile(join(outputDirectory, asset.name)) + if (metadata.size !== asset.size || sha256(bytes) !== asset.sha256) { + throw new Error(`Runtime metadata changed while being finalized: ${asset.name}`) + } + } + return { sbom: sbomAsset, provenance: provenanceAsset, identity: identityAsset } +} diff --git a/config/scripts/ssh-relay-runtime-smoke-child.cjs b/config/scripts/ssh-relay-runtime-smoke-child.cjs new file mode 100644 index 00000000000..7ac0b483df4 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-smoke-child.cjs @@ -0,0 +1,158 @@ +'use strict' + +const { appendFile, mkdtemp, rename, rm, unlink, writeFile } = require('node:fs/promises') +const { tmpdir } = require('node:os') +const { join } = require('node:path') +const { createRequire } = require('node:module') + +const runtimeRoot = process.argv[2] +if (!runtimeRoot) { + throw new Error('runtime root argument is required') +} +const runtimeRequire = createRequire(join(runtimeRoot, 'relay.js')) +const nodePty = runtimeRequire('node-pty') +const watcher = runtimeRequire('@parcel/watcher') +const TIMEOUT_MS = 15_000 + +function deadline(label) { + let timeout + const promise = new Promise((_, reject) => { + timeout = setTimeout(() => reject(new Error(`${label} exceeded ${TIMEOUT_MS} ms`)), TIMEOUT_MS) + }) + return { promise, cancel: () => clearTimeout(timeout) } +} + +async function ptySmoke() { + if (process.platform === 'win32') { + throw new Error('Windows PTY smoke is not implemented yet') + } + const shell = + process.env.SHELL && process.env.SHELL.startsWith('/') ? process.env.SHELL : '/bin/sh' + const terminal = nodePty.spawn( + shell, + [ + '-c', + 'printf "ORCA_PTY_READY\\n"; IFS= read -r line; stty size; printf "ORCA_PTY_INPUT:%s\\n" "$line"; exit 23' + ], + { name: 'xterm-256color', cols: 80, rows: 24, cwd: runtimeRoot, env: process.env } + ) + let output = '' + let wroteInput = false + terminal.onData((data) => { + output += data + if (!wroteInput && output.includes('ORCA_PTY_READY')) { + wroteInput = true + terminal.resize(101, 37) + terminal.write('bounded-marker\r') + } + }) + const exited = new Promise((resolve, reject) => { + terminal.onExit(resolve) + terminal.onData(() => { + if (output.length > 1024 * 1024) { + reject(new Error('PTY smoke output exceeded 1 MiB')) + } + }) + }) + const timer = deadline('PTY smoke') + let result + try { + result = await Promise.race([exited, timer.promise]) + } finally { + timer.cancel() + if (!result) { + terminal.kill() + } + } + if ( + result.exitCode !== 23 || + !output.includes('ORCA_PTY_INPUT:bounded-marker') || + !output.includes('37 101') + ) { + throw new Error(`PTY smoke mismatch: exit=${result.exitCode} output=${JSON.stringify(output)}`) + } + const { loadNativeModule } = runtimeRequire('node-pty/lib/utils') + const native = loadNativeModule('pty') + if (!native.dir.replaceAll('\\', '/').includes('build/Release/')) { + throw new Error(`node-pty did not load the patched build/Release artifact: ${native.dir}`) + } + return { + exitCode: result.exitCode, + resizedRows: 37, + resizedColumns: 101, + nativeDirectory: native.dir + } +} + +async function waitFor(events, predicate, label) { + const timer = deadline(label) + try { + while (!predicate(events)) { + await Promise.race([new Promise((resolve) => setTimeout(resolve, 25)), timer.promise]) + } + } finally { + timer.cancel() + } +} + +async function watcherSmoke() { + const directory = await mkdtemp(join(tmpdir(), 'orca-runtime-watcher-')) + const first = join(directory, 'first.txt') + const second = join(directory, 'renamed.txt') + const events = [] + let subscription + try { + subscription = await watcher.subscribe(directory, (error, batch) => { + if (error) { + throw error + } + events.push(...batch.map((event) => ({ type: event.type, path: event.path }))) + }) + await writeFile(first, 'created') + await waitFor( + events, + (seen) => seen.some((event) => event.type === 'create' && event.path === first), + 'watcher create' + ) + await appendFile(first, '-modified') + await waitFor( + events, + (seen) => seen.some((event) => event.type === 'update' && event.path === first), + 'watcher modify' + ) + await rename(first, second) + await waitFor( + events, + (seen) => + seen.some((event) => event.type === 'delete' && event.path === first) && + seen.some((event) => event.type === 'create' && event.path === second), + 'watcher rename' + ) + await unlink(second) + await waitFor( + events, + (seen) => seen.some((event) => event.type === 'delete' && event.path === second), + 'watcher delete' + ) + return { + events: events.map((event) => ({ type: event.type, name: event.path.split(/[\\/]/).at(-1) })) + } + } finally { + await subscription?.unsubscribe() + await rm(directory, { recursive: true, force: true }) + } +} + +async function main() { + const started = process.hrtime.bigint() + const [pty, watched] = await Promise.all([ptySmoke(), watcherSmoke()]) + const durationMs = Number(process.hrtime.bigint() - started) / 1e6 + process.stdout.write( + `${JSON.stringify({ nodeVersion: process.version, modulesAbi: process.versions.modules, pty, watcher: watched, durationMs, rssBytes: process.memoryUsage().rss })}\n` + ) +} + +main().catch((error) => { + process.stderr.write(`Bundled runtime smoke failed: ${error.stack ?? error.message}\n`) + process.exitCode = 1 +}) diff --git a/config/scripts/ssh-relay-runtime-tree.mjs b/config/scripts/ssh-relay-runtime-tree.mjs new file mode 100644 index 00000000000..33785f8d933 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-tree.mjs @@ -0,0 +1,346 @@ +import { createHash } from 'node:crypto' +import { chmod, copyFile, mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises' +import { createRequire } from 'node:module' +import { dirname, join, relative, sep } from 'node:path' + +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' + +const require = createRequire(import.meta.url) +const MAX_FILES = 5_000 +const MAX_EXPANDED_BYTES = 350 * 1024 * 1024 +const MAX_FILE_BYTES = 250 * 1024 * 1024 +const MAX_PATH_BYTES = 240 +const MAX_PATH_DEPTH = 32 +const PORTABLE_PATH = /^[A-Za-z0-9._@+/-]+$/ +const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i + +const COMPATIBILITY = Object.freeze({ + 'linux-x64-glibc': { + kind: 'linux', + minimumKernelVersion: '4.18', + libc: { + family: 'glibc', + minimumVersion: '2.28', + minimumLibstdcxxVersion: '6.0.25', + minimumGlibcxxVersion: '3.4.25' + } + }, + 'linux-arm64-glibc': { + kind: 'linux', + minimumKernelVersion: '4.18', + libc: { + family: 'glibc', + minimumVersion: '2.28', + minimumLibstdcxxVersion: '6.0.25', + minimumGlibcxxVersion: '3.4.25' + } + }, + 'darwin-x64': { kind: 'darwin', minimumVersion: '13.5' }, + 'darwin-arm64': { kind: 'darwin', minimumVersion: '13.5' } +}) + +const WATCHER_PACKAGES = Object.freeze({ + 'linux-x64-glibc': '@parcel/watcher-linux-x64-glibc', + 'linux-arm64-glibc': '@parcel/watcher-linux-arm64-glibc', + 'darwin-x64': '@parcel/watcher-darwin-x64', + 'darwin-arm64': '@parcel/watcher-darwin-arm64' +}) + +function packageDirectory(name) { + return dirname(require.resolve(`${name}/package.json`)) +} + +async function copyNormalized(source, destination, mode = 0o644) { + await mkdir(dirname(destination), { recursive: true }) + await copyFile(source, destination) + await chmod(destination, mode) +} + +async function writeJson(destination, value) { + await mkdir(dirname(destination), { recursive: true }) + await writeFile(destination, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o644 }) + await chmod(destination, 0o644) +} + +async function listFiles(directory) { + const results = [] + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = join(directory, entry.name) + if (entry.isDirectory()) { + results.push(...(await listFiles(path))) + } else if (entry.isFile()) { + results.push(path) + } else { + throw new Error(`Runtime source contains unsupported entry: ${path}`) + } + } + return results.sort() +} + +async function copyJavaScriptTree(source, destination, predicate = () => true) { + for (const sourcePath of await listFiles(source)) { + const relativePath = relative(source, sourcePath) + if (predicate(relativePath)) { + await copyNormalized(sourcePath, join(destination, relativePath)) + } + } +} + +async function minimalPackage(name) { + const parsed = JSON.parse(await readFile(join(packageDirectory(name), 'package.json'), 'utf8')) + return { + name: parsed.name, + version: parsed.version, + main: parsed.main, + license: parsed.license + } +} + +async function copyNodePty(buildDirectory, runtimeRoot, tuple) { + const destination = join(runtimeRoot, 'node_modules', 'node-pty') + const platformFiles = tuple.startsWith('win32-') + ? [ + 'conpty_console_list_agent.js', + 'eventEmitter2.js', + 'index.js', + 'terminal.js', + 'utils.js', + 'windowsConoutConnection.js', + 'windowsPtyAgent.js', + 'windowsTerminal.js', + 'shared/conout.js', + 'worker/conoutSocketWorker.js' + ] + : ['eventEmitter2.js', 'index.js', 'terminal.js', 'unixTerminal.js', 'utils.js'] + for (const path of platformFiles) { + await copyNormalized(join(buildDirectory, 'lib', path), join(destination, 'lib', path)) + } + await writeJson(join(destination, 'package.json'), await minimalPackage('node-pty')) + await copyNormalized( + join(buildDirectory, 'build', 'Release', 'pty.node'), + join(destination, 'build', 'Release', 'pty.node'), + 0o755 + ) + if (tuple.startsWith('darwin-')) { + await copyNormalized( + join(buildDirectory, 'build', 'Release', 'spawn-helper'), + join(destination, 'build', 'Release', 'spawn-helper'), + 0o755 + ) + } +} + +async function copyPackageRuntime(name, runtimeRoot, relativePaths) { + const source = packageDirectory(name) + const destination = join(runtimeRoot, 'node_modules', ...name.split('/')) + for (const relativePath of relativePaths) { + const sourcePath = join(source, relativePath) + const metadata = await stat(sourcePath) + await (metadata.isDirectory() + ? copyJavaScriptTree(sourcePath, join(destination, relativePath), (path) => + path.endsWith('.js') + ) + : copyNormalized(sourcePath, join(destination, relativePath))) + } + await writeJson(join(destination, 'package.json'), await minimalPackage(name)) +} + +async function copyParcelWatcher(runtimeRoot, tuple) { + const watcherPackage = WATCHER_PACKAGES[tuple] + if (!watcherPackage) { + throw new Error(`No bundled Parcel watcher package is defined for ${tuple}`) + } + await copyPackageRuntime('@parcel/watcher', runtimeRoot, ['index.js', 'wrapper.js']) + await copyPackageRuntime(watcherPackage, runtimeRoot, ['watcher.node']) + await chmod( + join(runtimeRoot, 'node_modules', ...watcherPackage.split('/'), 'watcher.node'), + 0o755 + ) + await copyPackageRuntime('detect-libc', runtimeRoot, ['lib']) + await copyPackageRuntime('is-glob', runtimeRoot, ['index.js']) + await copyPackageRuntime('is-extglob', runtimeRoot, ['index.js']) + await copyPackageRuntime('picomatch', runtimeRoot, ['index.js', 'lib']) + return watcherPackage +} + +async function relayBuildVersion(relayDirectory) { + const [relay, watcher, version] = await Promise.all([ + readFile(join(relayDirectory, 'relay.js')), + readFile(join(relayDirectory, 'relay-watcher.js')), + readFile(join(relayDirectory, '.version'), 'utf8') + ]) + const hash = createHash('sha256').update(relay).update(watcher).digest('hex').slice(0, 12) + if (!version.trim().endsWith(`+${hash}`)) { + throw new Error('Existing relay build version does not authenticate relay and watcher bytes') + } + return version.trim() +} + +async function writeLicenses(runtimeRoot, nodeRoot, packages) { + const sections = [] + const inputs = [['Node.js', join(nodeRoot, 'LICENSE')]] + for (const packageName of packages) { + inputs.push([packageName, join(packageDirectory(packageName), 'LICENSE')]) + } + for (const [name, path] of inputs) { + sections.push(`===== ${name} =====\n${(await readFile(path, 'utf8')).trim()}\n`) + } + await writeFile(join(runtimeRoot, 'THIRD_PARTY_LICENSES.txt'), `${sections.join('\n')}\n`, { + mode: 0o644 + }) + await chmod(join(runtimeRoot, 'THIRD_PARTY_LICENSES.txt'), 0o644) +} + +function runtimeRole(path) { + if (path === 'bin/node') { + return 'node' + } + if (path === 'relay.js') { + return 'relay' + } + if (path === 'relay-watcher.js') { + return 'relay-watcher' + } + if (path.endsWith('/pty.node')) { + return 'node-pty-native' + } + if (path.endsWith('/watcher.node')) { + return 'parcel-watcher-native' + } + if (path.endsWith('/spawn-helper')) { + return 'native-runtime' + } + if (path === 'THIRD_PARTY_LICENSES.txt') { + return 'license' + } + return 'runtime-javascript' +} + +function assertPortableRuntimePath(path) { + const segments = path.split('/') + if ( + !PORTABLE_PATH.test(path) || + Buffer.byteLength(path) > MAX_PATH_BYTES || + segments.length > MAX_PATH_DEPTH + ) { + throw new Error(`Runtime tree contains non-portable path: ${path}`) + } + for (const segment of segments) { + if ( + !segment || + segment === '.' || + segment === '..' || + segment.endsWith('.') || + segment.endsWith(' ') || + WINDOWS_DEVICE_NAME.test(segment) + ) { + throw new Error(`Runtime tree contains non-portable path segment: ${path}`) + } + } +} + +async function collectEntries(runtimeRoot) { + const entries = [] + const folded = new Set() + async function visit(directory) { + const children = await readdir(directory, { withFileTypes: true }) + children.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) + for (const child of children) { + const absolutePath = join(directory, child.name) + const path = relative(runtimeRoot, absolutePath).split(sep).join('/') + assertPortableRuntimePath(path) + const foldedPath = path.toLowerCase() + if (folded.has(foldedPath)) { + throw new Error(`Runtime tree contains a case-fold collision: ${path}`) + } + folded.add(foldedPath) + if (child.isDirectory()) { + await chmod(absolutePath, 0o755) + entries.push({ path, type: 'directory', mode: 0o755 }) + await visit(absolutePath) + } else if (child.isFile()) { + const metadata = await stat(absolutePath) + if (metadata.size > MAX_FILE_BYTES) { + throw new Error(`Runtime file exceeds size limit: ${path}`) + } + const mode = metadata.mode & 0o777 + if (mode !== 0o644 && mode !== 0o755) { + throw new Error(`Runtime file has a non-canonical mode: ${path}`) + } + const bytes = await readFile(absolutePath) + entries.push({ + path, + type: 'file', + role: runtimeRole(path), + size: bytes.length, + mode, + sha256: `sha256:${createHash('sha256').update(bytes).digest('hex')}` + }) + } else { + throw new Error(`Runtime tree contains a prohibited special entry: ${path}`) + } + } + } + await visit(runtimeRoot) + const files = entries.filter((entry) => entry.type === 'file') + const expandedSize = files.reduce((total, file) => total + file.size, 0) + if (files.length > MAX_FILES || expandedSize > MAX_EXPANDED_BYTES) { + throw new Error('Runtime tree exceeds the manifest file-count or expanded-size limit') + } + return { entries, fileCount: files.length, expandedSize } +} + +export async function assembleSshRelayRuntimeTree({ + tuple, + nodeRoot, + nodePtyBuildDirectory, + relayDirectory, + runtimeRoot, + nodeVersion +}) { + const compatibility = COMPATIBILITY[tuple] + if (!compatibility) { + throw new Error(`Runtime tree assembly is not implemented for ${tuple}`) + } + await mkdir(runtimeRoot) + await copyNormalized(join(nodeRoot, 'bin', 'node'), join(runtimeRoot, 'bin', 'node'), 0o755) + const version = await relayBuildVersion(relayDirectory) + await Promise.all([ + copyNormalized(join(relayDirectory, 'relay.js'), join(runtimeRoot, 'relay.js')), + copyNormalized(join(relayDirectory, 'relay-watcher.js'), join(runtimeRoot, 'relay-watcher.js')), + copyNormalized(join(relayDirectory, '.version'), join(runtimeRoot, '.version')) + ]) + await copyNodePty(nodePtyBuildDirectory, runtimeRoot, tuple) + const watcherPackage = await copyParcelWatcher(runtimeRoot, tuple) + const licensePackages = [ + 'node-pty', + '@parcel/watcher', + watcherPackage, + 'detect-libc', + 'is-glob', + 'is-extglob', + 'picomatch' + ] + await writeLicenses(runtimeRoot, nodeRoot, licensePackages) + await writeJson(join(runtimeRoot, 'runtime-metadata.json'), { + schemaVersion: 1, + tuple, + nodeVersion, + relayBuildVersion: version, + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' } + }) + + const tree = await collectEntries(runtimeRoot) + const os = tuple.startsWith('linux-') ? 'linux' : 'darwin' + const architecture = tuple.includes('arm64') ? 'arm64' : 'x64' + const identity = { + tupleId: tuple, + os, + architecture, + compatibility, + nodeVersion, + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries: tree.entries + } + return { ...identity, contentId: computeSshRelayRuntimeContentId(identity), ...tree } +} diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs new file mode 100644 index 00000000000..8200dfe73c8 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -0,0 +1,56 @@ +import { readFile } from 'node:fs/promises' + +import { parse } from 'yaml' +import { describe, expect, it } from 'vitest' + +const workflowUrl = new URL( + '../../.github/workflows/ssh-relay-runtime-artifacts.yml', + import.meta.url +) + +describe('SSH relay runtime artifact workflow', () => { + it('uses exact native runner labels and SHA-pinned actions without publication authority', async () => { + const workflow = parse(await readFile(workflowUrl, 'utf8')) + const job = workflow.jobs['build-posix-runtime'] + const matrix = job.strategy.matrix.include + + expect(matrix.map((entry) => [entry.runner, entry.tuple])).toEqual([ + ['ubuntu-24.04', 'linux-x64-glibc'], + ['ubuntu-24.04-arm', 'linux-arm64-glibc'], + ['macos-15-intel', 'darwin-x64'], + ['macos-15', 'darwin-arm64'] + ]) + expect(workflow.permissions).toEqual({ contents: 'read' }) + expect(job['timeout-minutes']).toBe(20) + expect(job.steps[0].with.ref).toBe('${{ github.event.pull_request.head.sha || github.sha }}') + for (const step of job.steps.filter((candidate) => candidate.uses)) { + expect(step.uses).toMatch(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+@[0-9a-f]{40}$/) + } + }) + + it('uploads only archive and metadata evidence after executable verification', async () => { + const source = await readFile(workflowUrl, 'utf8') + const workflow = parse(source) + const steps = workflow.jobs['build-posix-runtime'].steps + const buildIndex = steps.findIndex( + (step) => step.name === 'Build, inspect, and smoke exact runtime' + ) + const uploadIndex = steps.findIndex( + (step) => step.name === 'Upload unpublished artifact evidence' + ) + + expect(buildIndex).toBeGreaterThan(-1) + expect(uploadIndex).toBeGreaterThan(buildIndex) + expect(source).toContain('verify-ssh-relay-runtime.mjs') + expect(source).toContain('ssh-relay-runtime-workflow.test.mjs') + expect(source).toContain('pnpm install --frozen-lockfile --ignore-scripts') + expect(source).toContain('--connect-timeout 20 --max-time 300 --retry 2') + expect(source).toContain('mkdir -p "$(dirname "$output")"') + expect(source).toContain('source_commit=$(git rev-parse HEAD)') + expect(source).toContain('--git-commit "$source_commit"') + expect(source).not.toContain('--git-commit "$GITHUB_SHA"') + expect(source).toContain('cp "$output"/*.tar.xz') + expect(steps[uploadIndex].with.path).toBe('runtime-evidence/${{ matrix.tuple }}/') + expect(source).not.toMatch(/releases\/|gh release|contents:\s*write/i) + }) +}) diff --git a/config/scripts/verify-ssh-relay-node-release-inputs.mjs b/config/scripts/verify-ssh-relay-node-release-inputs.mjs new file mode 100644 index 00000000000..120ab51c8fe --- /dev/null +++ b/config/scripts/verify-ssh-relay-node-release-inputs.mjs @@ -0,0 +1,135 @@ +#!/usr/bin/env node + +import { readFile } from 'node:fs/promises' +import { dirname, isAbsolute, join, resolve } from 'node:path' +import { pathToFileURL } from 'node:url' + +import { EXPECTED_TUPLES } from './ssh-relay-node-release-contract.mjs' +import { readBoundedFile } from './ssh-relay-node-release-file-verification.mjs' +import { + validateSshRelayNodeReleaseContract, + verifySshRelayNodeArchive, + verifySshRelayNodeChecksumDocument, + verifySshRelayNodeSignature +} from './ssh-relay-node-release-verification.mjs' + +const scriptDirectory = import.meta.dirname +const DEFAULT_CONTRACT_PATH = resolve(scriptDirectory, '..', 'ssh-relay-node-release-v24.18.0.json') + +function takeValue(argv, index, flag) { + const value = argv[index + 1] + if (value === undefined || value.startsWith('--')) { + throw new Error(`${flag} requires a value`) + } + return value +} + +export function parseArguments(argv) { + let contractPath = DEFAULT_CONTRACT_PATH + let inputsDirectory + let allArchives = false + const archiveTuples = [] + + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index] + if (argument === '--contract') { + contractPath = resolve(takeValue(argv, index, argument)) + index += 1 + } else if (argument === '--inputs-directory') { + if (inputsDirectory !== undefined) { + throw new Error('--inputs-directory may be specified once') + } + inputsDirectory = resolve(takeValue(argv, index, argument)) + index += 1 + } else if (argument === '--archive') { + archiveTuples.push(takeValue(argv, index, argument)) + index += 1 + } else if (argument === '--all-archives') { + allArchives = true + } else { + throw new Error(`Unknown argument: ${argument}`) + } + } + + if (inputsDirectory === undefined) { + throw new Error('--inputs-directory is required') + } + if (allArchives && archiveTuples.length > 0) { + throw new Error('--all-archives cannot be combined with --archive') + } + const selectedTuples = allArchives ? [...EXPECTED_TUPLES] : archiveTuples + if (new Set(selectedTuples).size !== selectedTuples.length) { + throw new Error('Archive tuples must not be duplicated') + } + for (const tuple of selectedTuples) { + if (!EXPECTED_TUPLES.includes(tuple)) { + throw new Error(`Unknown archive tuple: ${tuple}`) + } + } + return { contractPath, inputsDirectory, archiveTuples: selectedTuples } +} + +async function loadContract(contractPath) { + const source = await readFile(contractPath, 'utf8') + let contract + try { + contract = JSON.parse(source) + } catch (error) { + throw new Error(`Node release contract is not valid JSON: ${error.message}`) + } + return validateSshRelayNodeReleaseContract(contract) +} + +export async function verifyNodeReleaseInputs( + { contractPath, inputsDirectory, archiveTuples }, + { commandRunner } = {} +) { + const release = await loadContract(contractPath) + const checksumPath = join(inputsDirectory, release.checksumDocument.name) + const signaturePath = join(inputsDirectory, release.signature.name) + const keyPath = isAbsolute(release.signature.key.path) + ? release.signature.key.path + : resolve(dirname(contractPath), release.signature.key.path) + + const signature = await verifySshRelayNodeSignature(release, { + checksumPath, + signaturePath, + keyPath, + commandRunner + }) + const checksumBytes = await readBoundedFile( + checksumPath, + release.checksumDocument.maximumBytes, + 'Node checksum document' + ) + const checksums = verifySshRelayNodeChecksumDocument(release, checksumBytes) + const archives = [] + for (const tuple of archiveTuples) { + const archive = release.archives[tuple] + archives.push( + await verifySshRelayNodeArchive(release, tuple, join(inputsDirectory, archive.name)) + ) + } + + return { + schemaVersion: release.schemaVersion, + nodeVersion: release.nodeVersion, + baseUrl: release.baseUrl, + signerFingerprint: signature.signerFingerprint, + checksumSha256: signature.checksumSha256, + checksumEntriesVerified: checksums.length, + archives + } +} + +async function main() { + const result = await verifyNodeReleaseInputs(parseArguments(process.argv.slice(2))) + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`) +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + main().catch((error) => { + process.stderr.write(`SSH relay Node release verification failed: ${error.message}\n`) + process.exitCode = 1 + }) +} diff --git a/config/scripts/verify-ssh-relay-runtime.mjs b/config/scripts/verify-ssh-relay-runtime.mjs new file mode 100644 index 00000000000..3b0cdc99f23 --- /dev/null +++ b/config/scripts/verify-ssh-relay-runtime.mjs @@ -0,0 +1,154 @@ +#!/usr/bin/env node + +import { execFile } from 'node:child_process' +import { createHash } from 'node:crypto' +import { readFile, readdir, stat } from 'node:fs/promises' +import { join, relative, resolve, sep } from 'node:path' +import { promisify } from 'node:util' + +import { inspectSshRelayRuntimeArchive } from './ssh-relay-runtime-archive.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' + +const execFileAsync = promisify(execFile) +const scriptDirectory = import.meta.dirname +const MAX_SMOKE_OUTPUT_BYTES = 4 * 1024 * 1024 + +function parseArguments(argv) { + const result = {} + for (let index = 0; index < argv.length; index += 2) { + const flag = argv[index] + const value = argv[index + 1] + if (!value || value.startsWith('--')) { + throw new Error(`${flag} requires a value`) + } + if (flag === '--runtime-directory') { + result.runtimeDirectory = resolve(value) + } else if (flag === '--identity') { + result.identityPath = resolve(value) + } else if (flag === '--archive') { + result.archivePath = resolve(value) + } else { + throw new Error(`Unknown argument: ${flag}`) + } + } + if (!result.runtimeDirectory || !result.identityPath) { + throw new Error('--runtime-directory and --identity are required') + } + return result +} + +async function actualTree(runtimeDirectory) { + const results = new Map() + async function visit(directory) { + const children = await readdir(directory, { withFileTypes: true }) + for (const child of children) { + const absolutePath = join(directory, child.name) + const path = relative(runtimeDirectory, absolutePath).split(sep).join('/') + if (results.has(path.toLowerCase())) { + throw new Error(`Runtime tree case-fold collision: ${path}`) + } + if (child.isDirectory()) { + const metadata = await stat(absolutePath) + results.set(path.toLowerCase(), { + path, + type: 'directory', + mode: metadata.mode & 0o777 + }) + await visit(absolutePath) + } else if (child.isFile()) { + const [metadata, bytes] = await Promise.all([stat(absolutePath), readFile(absolutePath)]) + results.set(path.toLowerCase(), { + path, + type: 'file', + size: bytes.length, + mode: metadata.mode & 0o777, + sha256: `sha256:${createHash('sha256').update(bytes).digest('hex')}` + }) + } else { + throw new Error(`Runtime tree contains prohibited entry: ${path}`) + } + } + } + await visit(runtimeDirectory) + return results +} + +export async function verifyRuntimeTree(runtimeDirectory, identity) { + const actual = await actualTree(runtimeDirectory) + for (const expected of identity.entries) { + const entry = actual.get(expected.path.toLowerCase()) + if ( + !entry || + entry.path !== expected.path || + entry.type !== expected.type || + entry.mode !== expected.mode + ) { + throw new Error(`Runtime tree entry mismatch: ${expected.path}`) + } + if ( + expected.type === 'file' && + (entry.size !== expected.size || entry.sha256 !== expected.sha256) + ) { + throw new Error(`Runtime tree file integrity mismatch: ${expected.path}`) + } + actual.delete(expected.path.toLowerCase()) + } + if (actual.size > 0) { + throw new Error(`Runtime tree has undeclared entry: ${actual.values().next().value.path}`) + } + const contentId = computeSshRelayRuntimeContentId(identity) + if (contentId !== identity.contentId) { + throw new Error('Runtime tree content identity mismatch') + } + return { + entries: identity.entries.length, + files: identity.entries.filter((entry) => entry.type === 'file').length, + expandedBytes: identity.expandedSize, + contentId + } +} + +async function runBundledSmoke(runtimeDirectory, identity) { + const nodePath = join(runtimeDirectory, 'bin', 'node') + const childPath = resolve(scriptDirectory, 'ssh-relay-runtime-smoke-child.cjs') + const result = await execFileAsync(nodePath, [childPath, runtimeDirectory], { + cwd: runtimeDirectory, + encoding: 'utf8', + maxBuffer: MAX_SMOKE_OUTPUT_BYTES, + timeout: 45_000, + windowsHide: true + }) + const smoke = JSON.parse(result.stdout) + if (smoke.nodeVersion !== `v${identity.nodeVersion}`) { + throw new Error(`Bundled smoke used unexpected Node version: ${smoke.nodeVersion}`) + } + return smoke +} + +export async function verifySshRelayRuntime({ runtimeDirectory, identityPath, archivePath }) { + const identity = JSON.parse(await readFile(identityPath, 'utf8')) + const started = process.hrtime.bigint() + const archive = archivePath ? await inspectSshRelayRuntimeArchive(archivePath, identity) : null + const tree = await verifyRuntimeTree(runtimeDirectory, identity) + // Why: native code runs only after the complete extracted tree matches its content identity. + const smoke = await runBundledSmoke(runtimeDirectory, identity) + return { + tuple: identity.tupleId, + archive, + tree, + smoke, + durationMs: Number(process.hrtime.bigint() - started) / 1e6 + } +} + +async function main() { + const result = await verifySshRelayRuntime(parseArguments(process.argv.slice(2))) + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`) +} + +if (process.argv[1] && resolve(process.argv[1]) === import.meta.filename) { + main().catch((error) => { + process.stderr.write(`SSH relay runtime verification failed: ${error.stack ?? error.message}\n`) + process.exitCode = 1 + }) +} diff --git a/config/ssh-relay-node-release-keys/C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C.asc b/config/ssh-relay-node-release-keys/C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C.asc new file mode 100644 index 00000000000..dfeea1499c4 --- /dev/null +++ b/config/ssh-relay-node-release-keys/C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C.asc @@ -0,0 +1,90 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBF222c0BEAC/wIiI7EYmA7yprNa/0en2leF+CrF09BlCItTHH5IgjSLGq2tI +Bi3hIhf7TitDlu6GphHlFjvhj6UDgdEmr0itcoOLRhtER6WlmMaXtS+im5fPSLWW +skZSAh1YC3iqOQCErkAnVFUWY5nUbWfxgv0pKrc5GTT2RkiD6ngor5YIAkRaYQ+n +niHsekUYOuLln0p4n/K0/iRw5NMok9Q2FwlEj7H0kCfDPuqsgfEoDXoVv8QSVIpB +1gdOQ7e2MeMAB6U5o0OjqzMxPUrIkDmgGIBvCgcCN33lMNO4DWlhr3vF90EhKvWP +Ly4kTa4Gctt9f5kICzb7AYZJG7+F8hrVHpbF0fXzTgZsO/BBf4ERTMKGfj7ZXq7e +GARTfDgzR9yxWfxfo47jUd8amVk/qj9O94lIPobEUeP7SKPy2jIRTx04HcdMDPAF +okzJf1cwCghc19mN+ro31rcnud0Za0EM7Yxq89GHvHiUEzxj2XWny3n583V4Lh6c +2bAm0tcqmJlLholeFo3qW/nBSCde0BjwfamKd4KB80tsF8Qu6OdahFT8ybaCA2ls +dcks9uAXXqwyTmXhwe22CHk/919Ubk0DFPozgj0AaDf+vQz+lKxDUuY0FaSEo3sP +nw7JmG1fdz2jAw0UvK4xGrYE8nTlF85mOBhV7zjwW57b9x07Og0zilqZhwARAQAB +tB1SaWNoYXJkIExhdSA8cmxhdUByZWRoYXQuY29tPokCOQQTAQIAIwUCX3X/5wIb +AwcLCQgHAwIBBhUIAgkKCwQWAgMBAh4BAheAAAoJEMQ87EXBerk8y0gQALZv85ID +eHIDsbf05A3q5G614MQzRuU3rXBlLpTEv6DJorQll3hhmvRU4aQzX2D3JTzOAb8W +su+cHUXJvU11rCIsAQN3L5yWEG11hld/oDG3j5S0sM+/3YvqKjluWhQpYvOrTQUR +oJqn5UvKNzZ68cWgQDJOS3XVw5SLstB3ctoa3me0sSkSVs7Gy9L2SJUnnc33GGEr +UJCo7u/0eD5+ihNnP4buga+xd8w4zqe9gEzh8BZMi+soAv2kNC4wlMbErJUh0Msq +LTEJKmbNsVeGX0gwjlJR0sPGbfNWlN4lw6xAljFeRjRJg59Z9gk5P5+UHabUZLFk +9U98dj/8tclmxoa9ceMW1VVOBcpVskhUpF7DK7j0ojjz9iRXXWIxkn5dgnAP58eK ++WZtSQ5lBbzyLFFlr5c51/FZoFurVACWymHNiyA3Xg5tsv2e7a31T1oPC0K9T3Zx +navSgnx0Gb0SRJM2W0kJwIjdX61iHN8yisnD4+DMHWGZmNgPN/tVhpSVKVKJC1do +WlDgaoi2agnRtgfI0G9Em1CN7yHBCRpWaVERAaP/+uBogAwWOZFNgQjwVtU6jZYz +oLgazKbQXDf859eJ6C6AwWxrBiMMsdLsuQoFTmAztZS3efbLXU0qO4NkQ5QpEMP/ +9U6873G4QApdzEHojTyTt18ffmvZjkzpbj7WtB9SaWNoYXJkIExhdSA8cmljbGF1 +QHVrLmlibS5jb20+iQJRBDABCAA7FiEEyC+jrhy+3Gvka5NgxDzsRcF6uTwFAmDR +t8QdHSBDaGFuZ2VkIGVtcGxveWVyIDIwMjAtMTAtMDEACgkQxDzsRcF6uTyfNA/+ +IchjvyChF8f5CEIKvfT46yg3JrCl8sETf6cTR+ZY6ZleVgHD4/AM6yZXDg6B3gfj +UgZMpp1fu76N1zY+OQ6fPH9hgOGEsfKX//n5AJCCxhESXPlxQ7rvAkOLqUa8UsML +I8ueh19Rv+afEL4Z7l+mJF3HjGZAInVhbXg7OBp9x2Y1YhyGQe074bT1gX7UPdfT +PmmkT7cD92ec1uxJVSYbG4UzUZb46fZSUmgyRAlz3sTng2y7an9t/auzPNrzm+DC +SplupDqwu07MrfiKUnGfJs6C0tx9LGY6KQQKZR5yCVqp+bWhRuFd04e8gIX2qq34 +vgU+GbulJoz63OJ3tu1igouoVnS7OwoYrEIiWZI3agzkb0VvpAlqVInOrXmZtvFn +V3wLW7RfXf6sO3sSjgVBGCunxNasbaTCtUk1jpiMQKZpg1LwSEcLEAXt5wIDZuWl +827/CHSEuLqJNkJsTRYXZe/hXvWm09OGSPQglcAeRH9fytWxBBMd82LRcsuYw/b6 +dxw9qkwaLEZKN2vtvdJYu3BrS4PndU5Z2T530rdpign07ZpIDPvxElJ8KuFRgikN +FujKeAcMQ+PaZVWXBTEXsmqVJNPpLcun+Rb+ufpiwhehlqbnhdyqF40Rxivf2pk+ +gy0bKrCl+AEYiqJ+dKpUs50PYApvQCoKq6eVjoEnsACJAk4EEwEKADgWIQTIL6Ou +HL7ca+Rrk2DEPOxFwXq5PAUCXbbZzQIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIX +gAAKCRDEPOxFwXq5PGi/D/4yqrdmfkGqJBDHXfE6wnbm2+rtpr0F587CFtKeC7P9 +lJzDBXGvU22nf4pYB1c+UsLRvM+l3pQAfxJe8lg1fc6Phso1cb9WVquqHhkPDT+c +lgkGV8c0M9T2lRpDOLXmn7UfoqVq6aGCI7fqVf6mq0YCcprsAx6cPFd6G6jF6qEq +Maw7Yx+6gESRymI2FbifEHAEOWZHcS/H+itRN6OQG76n5W/PmVO8bsCqxTQlTqLS +ARX7bMFVI/J/F00DNjqI2sWzNx2FLsEbfG+NSrnGmun/Dz0M//TTwpAORTdQ5xGF +HcxNCsuNeRHVvbArP7NXIydMl5mBCs8W9fAZkNVchiVVgMovn+APA0/zhCjw/+I6 +EwGnPtOYzENcriKPF/5ZTrv/moGVWktfdgTO8Ygbx2Z4953U0JMAQc88CyY1Rnw7 +5S/tjmT+hwlGMb+YKiwq9+P3Zo5+9dPHz+aL4M5gskZDBP2Iu+c0ixgukoKdYf8d +mje7UsuF2RvW0y0Z/wIhNSo6/N/hYr6i5cMoDeZ6zMVZZEyFPx/JnvAX6B7t+7Br +MNFRtjvG9TNt5P4BkoBy0fL5Admid/oWlDLz6posj5ayfjb4ihB91BI1ORaqG8oL +AUC6C1vb8dmvCPGIyM0pmJnALc5/glnDNqyOVRrYAQXowNrlVPdbX3dYbZ0ooGAe +wbQhUmljaGFyZCBMYXUgPHJpY2hhcmQubGF1QGlibS5jb20+iQJUBBMBCgA+AhsD +BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAFiEEyC+jrhy+3Gvka5NgxDzsRcF6 +uTwFAmhcK9QCGQEACgkQxDzsRcF6uTzocw/9HMMhhddNoJeqOAFnKSKcKm5uw1cc +gTezizurj6W7GGKmJAvzLHz9gD+xz3TjAe+E4ZAkQNF2x0OmuEiHN1ST3lqtmF1J +fz3C1/WvtTTm03h6t5NQRtBzCLhXW7mH6Qbof8yI3pozM9t/mhmvkkr7FjJxC8cO +A8VbNXIVChZrRBrQZXfr06PkjDg7qEzSKZgDA10oSBJexZksf8g1L0D7NuEUu3dL +gwrL2+FlT3RCmEEhEei//V4gTbwA2DXjgro556vVv1SlgJWOsaD3228XT05jSta7 +jKswMyhS/U+/e7rt3BWhm/sxdHpZXOnQ0tqB8/veu4aI79DFTP//oDI45xwXXFSw +ZwF9GEvOlX1q5ZUA4eS2mxY1hHrj7Lo9VZwW1cZou5msqKXpe6+CLcSjPbIPT51V +VFTbmx9ps+0sxCa2cbNu/DvOlxAmZj2c9ceW4fsAiS4m8K4Z0nM8yy+t+2JHh8d4 +/GpkLMMWdYGq3xTxRS4mCekEZ28LWvwjo9/X06UvSGSu+Z47VxmR7oVeSIt8gPw0 +73gAYz4gb+cXTfAm+LOk5i4j564Ixqh5pIkjwasMQS5vlswWETq1hmDsCDCNMgLI +bbQYlUQgCFClPl+i4TXfWc7b0K3AUSjeThG4ZpsjDhhAl/uD6ykNMugZt66XEJoD +OvzI76MwP0rwuX25Ag0EXbbZzQEQAL9YA50WXd+iCK2rTlDk4Pv8piGkBu92CJ8Z +lbP37AriS0xKYm78sWFqRwflFoerUwdPbC7PV5FuslRC3Y7T2T7uJOU08YEue5nC +XgLJxnbtpjCKoLlgDWgLgLcmtfQnQrZkIZmYQ6zlTFHrJZ+Mg29ku/JeZ7HHB3EP +dQoFd0EIJJJy5IUhJA10xnjND3RrSaApS0Q7swui8LXNOtLa/tpGXF/0OA6OjIXB +xuT8lgTTUtk6zz+ESw0BgI+GnYZmL65m/PSfTVd79PujWhP3e/OmDti8JxWVsvjh +tbmS2ZNuQEKAvGJS/ZoMrap2cuA7OrDM0/Ndoq9dft5yhWZbkwBNevPwXSo9ZVgt +K+mZyVrQPKTfPusjF27HwPw1xrSvj1h1b1bAlg19FJTTAv5GqOePlnbgUlPvJYt9 +R3gipBy2ZIwWV9dfRZe848dz3dReD/+q1fNkQJJC76pv7T0CboEMHvKryKfsKNp6 +THyHWRE472kkQZ7qF2ssxu3d+JHFvn+KEAmIbR+UfS5hnENVallHXRU4gtEAae8c +BlXNnP8swzO6BkgGqsERtvATTdb8xjRSUIcqPxv7EIRG57lnEHuGr6N1Xa6FAxTm +hOy27AlmnbQyaJmpDMe5s5XHApUbRA4Mq1qSbcOFNml3zUVKnCXaXY0B6MM5F3RP +hRnx9LHRABEBAAGJAjYEGAEKACAWIQTIL6OuHL7ca+Rrk2DEPOxFwXq5PAUCXbbZ +zQIbDAAKCRDEPOxFwXq5PLlHD/9kI4bENYzm/IE1EK0zp48LZCsaeZGZ/hyW0qJ9 +lnLyXKRmilHAeLmD+tRXEnCOzmFwqftJpQJditx79uoyUiTiOA/yexia4/hrItZ8 ++E7lXQmvA6vEFRMNZ+5+TtlOMMS8BL1kdyJIC589nDZorA8l8401e9nAGVtowhk/ +jWpF3tuRfb7sVKvvWpzee/EJ1ntmUXi8FrhflMhBJafQaRdLFnrGTjr3iwh05TDE +AbpwzTfZQkj6YljJAFz+QwNZV9mK9hGzq1ExGo3B7EXdY7qelQIYTEByPXkRXa9j +tUYJZOKX12hyCYfONIFFcZcNryxbF+X7UtCnTrgyK8LZ3L1t2xKOxumvoeLnfOqX +Wdizm25eu1vaBKSmdYwwC9z9DYZqImYtOksLQoolqEh0LcqJBKJuItMKmDsBW5EB +5IFinbpmzxa2X4rvX7qNNQ46ky0sJzLW+bollsrn40S021IUyXq/FsisJTyFz7Nd +eTvEgr8uvOZJf++r9oEZw48siZNqjvPsejQjacu9UyXM0dCnJSaOm8OnZy8TpRB/ +ibYKg149T1Y2+JrwHaXdKiM0U4k3a9X+TjA8BCV0+m3eY97cuFqXItaEGg/3FJyf +9l5FS1bmjbondfTk3+rBsZ/tzDds+Jw2ir2mncO1jXgVls8xieBGoRWvH1N7nSSM +NBG2Fw== +=Cg9A +-----END PGP PUBLIC KEY BLOCK----- diff --git a/config/ssh-relay-node-release-v24.18.0.json b/config/ssh-relay-node-release-v24.18.0.json new file mode 100644 index 00000000000..392205060b9 --- /dev/null +++ b/config/ssh-relay-node-release-v24.18.0.json @@ -0,0 +1,49 @@ +{ + "schemaVersion": 1, + "nodeVersion": "24.18.0", + "baseUrl": "https://nodejs.org/dist/v24.18.0", + "checksumDocument": { + "name": "SHASUMS256.txt", + "sha256": "3927bab574a00ca0560c9583fe19655ba19603a1c5851414e4325d34ac50e469", + "maximumBytes": 1048576 + }, + "signature": { + "name": "SHASUMS256.txt.sig", + "sha256": "d771440acfe010e7510a3c01d248525f771daa9cf75dae5784c97ea2b08d9393", + "maximumBytes": 65536, + "signerFingerprint": "C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C", + "key": { + "path": "ssh-relay-node-release-keys/C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C.asc", + "sha256": "84b1ca614406f341cb86e72920f5a64687a13ab67ab84038bcf2abba97898a84", + "sourceCommit": "b28073028e6d6855cfb53bf7fa0137599c01f967", + "sourceUrl": "https://raw.githubusercontent.com/nodejs/release-keys/b28073028e6d6855cfb53bf7fa0137599c01f967/keys/C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C.asc" + } + }, + "maximumArchiveBytes": 209715200, + "archives": { + "darwin-arm64": { + "name": "node-v24.18.0-darwin-arm64.tar.xz", + "sha256": "4477b9f78efb77744cf5eb57a0e9594dba66466b38b4e93fa9f35cb907a095a6" + }, + "darwin-x64": { + "name": "node-v24.18.0-darwin-x64.tar.xz", + "sha256": "4a3b6bc81542154430825128d9a279e8b364e8d90581544e506ef7579fd1ab6f" + }, + "linux-arm64-glibc": { + "name": "node-v24.18.0-linux-arm64.tar.xz", + "sha256": "58c9520501f6ae2b52d5b210444e24b9d0c029a58c5011b797bc1fe7105886f6" + }, + "linux-x64-glibc": { + "name": "node-v24.18.0-linux-x64.tar.xz", + "sha256": "55aa7153f9d88f28d765fcdad5ae6945b5c0f98a36881703817e4c450fa76742" + }, + "win32-arm64": { + "name": "node-v24.18.0-win-arm64.zip", + "sha256": "f274669adb93b1fd0fbf8f21fd078609e9dcc84333d4f2718d2dde3f9a161a01" + }, + "win32-x64": { + "name": "node-v24.18.0-win-x64.zip", + "sha256": "0ae68406b42d7725661da979b1403ec9926da205c6770827f33aac9d8f26e821" + } + } +} 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 64f21b06483..a0f6a45c359 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 @@ -60,6 +60,12 @@ same change as the work it records. - Active package: Work Package 2 target-native runtime assembly, archive inspection, executable smoke, SBOM, and provenance only. It 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, + 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 + watcher events. E-M3-STATIC-001 records the current focused, type, lint, format, line-budget, and + diff gates. Oldest-baseline, native-trust, SSH, and every other tuple cell remain open. - Production behavior: unchanged; Orca embeds relay JavaScript and installs `node-pty` plus `@parcel/watcher` with remote npm. - New runtime assets published: none. @@ -72,10 +78,10 @@ same change as the work it records. - Rollout control: existing per-SSH-target configuration; legacy is the default and the bundled runtime is an explicit per-target Beta opt-in under E-M1-ROLLOUT-DECISION-001. - Legacy fallback removal: not authorized. -- Next required action: create the isolated stacked Work Package 2 branch/PR and implement the first - target-native runtime assembly plus archive inspection and executable smoke without publication or - production consumption. Keep cross-family remote infrastructure and measured legacy baseline gates - open and do not introduce resolver, transfer, rollout, or default behavior. +- Next required action: run the isolated four-tuple POSIX artifact workflow on the exact draft-PR + head and record each native GitHub runner result. Keep Windows runtime/zip implementation, + cross-family remote infrastructure, signing/trust, and measured legacy baseline gates open; do not + introduce publication, resolver, transfer, rollout, tuple enablement, or default behavior. ## Non-Negotiable Invariants @@ -541,9 +547,11 @@ Each runtime must contain only the executable closure required by the relay. - [ ] Replace or extend `config/scripts/build-relay.mjs` without weakening its existing relay and watcher content-hash guarantees. -- [ ] Add a clearly named runtime assembly script, for example +- [x] Add a clearly named runtime assembly script, for example `config/scripts/build-ssh-relay-runtime.mjs`. -- [ ] Pin Node and verify downloaded source/binary checksums and upstream signatures. + (E-M3-RUNTIME-LOCAL-001) +- [x] Pin Node and verify downloaded source/binary checksums and upstream signatures. + (E-M3-NODE-PROVENANCE-001; real archive execution remains a separate per-tuple gate) - [ ] Build Orca’s patched `node-pty@1.1.0` against the exact bundled Node runtime. - [ ] Assert Orca-required patched exports/diagnostics exist; do not silently use an upstream prebuild that omits the patch. @@ -551,26 +559,30 @@ Each runtime must contain only the executable closure required by the relay. - [ ] Include relay JavaScript, watcher child, required runtime JavaScript closure, licenses, SBOM, provenance, and runtime metadata. - [ ] Exclude package managers, development dependencies, compilers, sources, caches, build - directories, and debug symbols unless an approved diagnostics requirement needs them. -- [ ] Make archive output deterministic or document and isolate unavoidable nondeterminism. -- [ ] Verify required executable modes before archiving. + directories, and Orca-built debug symbols unless an approved diagnostics requirement needs + them. Preserve verified official Node executable bytes without stripping or rewriting them, + even when the upstream binary contains debug metadata. +- [x] Make POSIX `tar.xz` output deterministic for the same tree, epoch, and pinned compression + toolchain; keep Windows zip determinism as an open per-tuple gate. (E-M3-RUNTIME-LOCAL-001) +- [x] Verify required executable modes before archiving. (E-M3-RUNTIME-LOCAL-001) - [ ] Sign native code according to the platform decisions in Milestone 1. - [ ] Verify platform-native signatures/policy on target-native runners after signing and before aggregation; attest the exact verified bytes for the signed manifest. -- [ ] Run local archive inspection and target-runtime smoke before upload. +- [x] Run local POSIX archive inspection and target-runtime smoke before any upload; Windows remains + open until its separate builder/verifier exists. (E-M3-RUNTIME-LOCAL-001) ### Per-tuple build and executable proof -| Runtime tuple | Build/provenance | Bundled Node | `node-pty` load + real PTY | Watcher events | Oldest baseline | Native trust | Evidence | -| ----------------- | ---------------- | ------------ | -------------------------- | -------------- | --------------- | ------------ | -------- | -| linux-x64-glibc | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | -| linux-arm64-glibc | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | -| linux-x64-musl | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | -| linux-arm64-musl | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | -| darwin-x64 | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | -| darwin-arm64 | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | -| win32-x64 | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | -| win32-arm64 | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | +| Runtime tuple | Build/provenance | Bundled Node | `node-pty` load + real PTY | Watcher events | Oldest baseline | Native trust | Evidence | +| ----------------- | ---------------- | ------------ | -------------------------- | -------------- | --------------- | ------------ | ---------------------- | +| linux-x64-glibc | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | +| linux-arm64-glibc | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-RUNTIME-LOCAL-001 | +| linux-x64-musl | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | +| linux-arm64-musl | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | +| darwin-x64 | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | +| darwin-arm64 | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | +| win32-x64 | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | +| win32-arm64 | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | Rules: @@ -1119,16 +1131,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 | In progress | No implementation evidence yet | -| 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 | In progress; one local POSIX tuple green | E-M3-NODE-RED-001, E-M3-NODE-PROVENANCE-001, E-M3-RUNTIME-LOCAL-001, E-M3-STATIC-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 | — | Every PR must document: @@ -1170,11 +1182,18 @@ focused commands as their scripts/tests are introduced. - [x] `pnpm exec vitest run --config config/vitest.config.ts src/main/ssh/ssh-remote-node-toolchain-resolution.test.ts` (E-M0-UNIT-001) - [x] `pnpm run test:e2e:ssh-node-toolchain-resolution` (E-M0-LIVE-001) +### Milestone 3 commands added + +- [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-node-release-verification.test.mjs` (E-M3-NODE-RED-001, E-M3-NODE-PROVENANCE-001) +- [x] `node config/scripts/verify-ssh-relay-node-release-inputs.mjs --inputs-directory --archive linux-x64-glibc` (E-M3-NODE-PROVENANCE-001; metadata and archive verification only) +- [x] `node config/scripts/build-ssh-relay-runtime.mjs --tuple linux-arm64-glibc --inputs-directory --output-directory --source-date-epoch --git-commit ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) +- [x] `node config/scripts/verify-ssh-relay-runtime.mjs --runtime-directory --identity --archive ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) + ### Commands/scripts that must be added or formally identified - [x] Manifest/schema/identity/signature/selector unit-test command. (E-M2-CONTRACT-001) -- [ ] Per-tuple runtime assembly command. -- [ ] Per-tuple archive inspection and native smoke command. +- [x] Per-tuple POSIX runtime assembly command. (E-M3-RUNTIME-LOCAL-001; Windows command remains open) +- [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, @@ -2168,6 +2187,213 @@ fragmentLinks=9`. - Follow-up: keep PR #8728 draft and isolated; begin Work Package 2 on a new stacked branch with no production consumer. +### E-M3-NODE-RED-001 — Node release verifier red gate + +- Date: 2026-07-14 +- Commit SHA / PR: `0c299fe189310b6dbd539f0f0f506b240524ba6a` plus uncommitted Work Package 2 test; no Work Package 2 PR yet +- Runner: macOS 26.2 arm64 native; Node v26.0.0 and pnpm 10.24.0 +- Remote: not applicable; local test discovery only +- Transport/network: none +- Exact command: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-release-verification.test.mjs + ``` + +- Result: expected FAIL before implementation. Vitest could not import the intentionally missing + `config/scripts/ssh-relay-node-release-verification.mjs`; one suite failed before collecting tests. +- Duration and resource metrics: 136 ms Vitest duration; archive/runtime resources do not apply. +- Artifact/log/trace link: durable test path above; local command output retained in the + implementation session. +- Oracle proved: the focused suite was discriminating before the verifier facade and domain modules + existed. +- Does not prove: any validation behavior, cryptographic verification, downloaded byte, archive, + extraction, runtime execution, SSH behavior, or platform tuple. +- Checklist items satisfied: red half of the Milestone 3 Node provenance implementation gate only. +- Follow-up: green implementation and real release-input proof are recorded in + E-M3-NODE-PROVENANCE-001. + +### E-M3-NODE-PROVENANCE-001 — Pinned Node metadata and archive verification + +- Date: 2026-07-14 +- Commit SHA / PR: `0c299fe189310b6dbd539f0f0f506b240524ba6a` plus uncommitted Work Package 2 implementation; no Work Package 2 PR yet +- Runner: focused tests on macOS 26.2 arm64 with Node v26.0.0/pnpm 10.24.0; real proof in + Docker Engine 29.2.1 on Linux/arm64 using `node:24-bookworm` resolved to + `node@sha256:032e78d7e54e352129831743737e3a83171d9cc5b5896f411649c597ce0b11ea`, + Node v24.17.0, and GnuPG/gpgv 2.2.40 +- Remote: not applicable; local container build-input verification only +- Transport/network: HTTPS downloads from exact `https://nodejs.org/dist/v24.18.0/` URLs; Debian + package mirrors supplied `ca-certificates`, `curl`, `gnupg`, and `gpgv`; no SSH transport +- Exact command: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-release-verification.test.mjs + + docker run --rm -v "$PWD:/workspace:ro" -w /workspace node:24-bookworm \ + bash -lc 'set -euo pipefail + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ca-certificates curl gnupg gpgv >/dev/null + mkdir -p /tmp/node-inputs + curl --fail --silent --show-error --location --proto "=https" --tlsv1.2 https://nodejs.org/dist/v24.18.0/SHASUMS256.txt --output /tmp/node-inputs/SHASUMS256.txt + curl --fail --silent --show-error --location --proto "=https" --tlsv1.2 https://nodejs.org/dist/v24.18.0/SHASUMS256.txt.sig --output /tmp/node-inputs/SHASUMS256.txt.sig + curl --fail --silent --show-error --location --proto "=https" --tlsv1.2 https://nodejs.org/dist/v24.18.0/node-v24.18.0-linux-x64.tar.xz --output /tmp/node-inputs/node-v24.18.0-linux-x64.tar.xz + node config/scripts/verify-ssh-relay-node-release-inputs.mjs --inputs-directory /tmp/node-inputs --archive linux-x64-glibc' + ``` + +- Result: PASS. Seven focused tests passed. The real verifier accepted the pinned release-key hash + `84b1ca614406f341cb86e72920f5a64687a13ab67ab84038bcf2abba97898a84`, exact signer + `C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C`, authenticated checksum-document hash + `3927bab574a00ca0560c9583fe19655ba19603a1c5851414e4325d34ac50e469`, all six pinned + checksum entries, and the 31,511,588-byte Linux x64 archive hash + `55aa7153f9d88f28d765fcdad5ae6945b5c0f98a36881703817e4c450fa76742`. +- Duration and resource metrics: final focused Vitest duration 160 ms; container provisioning, + metadata download, archive download, and verification completed in approximately 11 seconds. + Peak memory and network bytes were not instrumented; the accepted archive size was 31,511,588 + bytes and hashing was streaming/bounded. +- Artifact/log/trace link: committed contract, pinned ASCII release key, purpose-named verifier + modules/CLI, and local command output; no artifact was published or retained in the repository. +- Oracle proved: strict immutable six-tuple contract validation; bounded metadata/archive hashing; + exact release-key bytes; detached-signature success through gpgv; exact signer fingerprint; + authenticated checksum-to-archive cross-checking; duplicate/malformed/missing/mismatched input + rejection in focused tests; and one real official Linux x64 archive byte hash. +- Does not prove: archive safety or extraction, Linux x64 execution (the container was arm64), any + bundled Node/native dependency/PTY/watcher behavior, the other five archive downloads, target-native + assembly/signing, SSH transfer, release publication, cache behavior, or any enabled tuple. +- Checklist items satisfied: Milestone 3 pinned Node/downloaded-binary checksum/upstream-signature + implementation gate and the two Milestone 3 verification-command inventory entries only. +- Follow-up: assemble the first runtime on a target-native runner, inspect its archive, execute its + bundled Node and native dependencies, then fill only the evidence cells actually proved. + +### E-M3-RUNTIME-LOCAL-001 — First target-native Linux arm64 runtime artifact + +- Date: 2026-07-14 +- Commit SHA / PR: `0c299fe189310b6dbd539f0f0f506b240524ba6a` plus uncommitted Work + Package 2 implementation; no Work Package 2 PR yet +- Runner: macOS 26.2 arm64 host with Docker Engine 29.2.1; native Linux/arm64 container + `node@sha256:032e78d7e54e352129831743737e3a83171d9cc5b5896f411649c597ce0b11ea` + (Debian bookworm, build Node v24.17.0), bundled Node v24.18.0 ABI 137, Python 3.11.2, GNU + C++ 12.2.0, GNU strip 2.40, XZ Utils 5.4.1, GnuPG/gpgv 2.2.40 +- Remote: none; native container artifact build/execution only, not an SSH host or oldest-baseline VM +- Transport/network: exact HTTPS downloads from `nodejs.org/dist/v24.18.0`; Debian package mirrors + for build/signature-verification tools; no SSH, SFTP, system SSH, release upload, or remote egress +- Exact command: + + ```sh + docker run --rm \ + -v "$PWD:/workspace" \ + -v /tmp/orca-runtime-linux-arm64-v3:/evidence \ + -w /workspace \ + node@sha256:032e78d7e54e352129831743737e3a83171d9cc5b5896f411649c597ce0b11ea \ + bash -lc 'set -euo pipefail + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq build-essential ca-certificates curl gnupg gpgv python3 xz-utils >/dev/null + mkdir -p /tmp/node-inputs + base=https://nodejs.org/dist/v24.18.0 + for asset in SHASUMS256.txt SHASUMS256.txt.sig node-v24.18.0-linux-arm64.tar.xz; do + curl --fail --silent --show-error --location --proto "=https" --tlsv1.2 \ + --connect-timeout 20 --max-time 300 --retry 2 --retry-delay 2 --retry-all-errors \ + "$base/$asset" --output "/tmp/node-inputs/$asset" + done + output=/evidence/new-parent/artifact + node config/scripts/build-ssh-relay-runtime.mjs --tuple linux-arm64-glibc --inputs-directory /tmp/node-inputs --output-directory "$output" --source-date-epoch 1784030321 --git-commit 0c299fe189310b6dbd539f0f0f506b240524ba6a + identity="$output/orca-ssh-relay-runtime-linux-arm64-glibc.identity.json" + archive=$(find "$output" -maxdepth 1 -name "*.tar.xz" -print -quit) + node config/scripts/verify-ssh-relay-runtime.mjs --runtime-directory "$output/runtime" --identity "$identity" --archive "$archive"' + ``` + + A second clean `createSshRelayRuntimeArchive` invocation used the same runtime tree, + `SOURCE_DATE_EPOCH`, container digest, Node tar implementation, and XZ 5.4.1, followed by `cmp` + and `sha256sum` against the first archive. + +- Result: PASS. The target-native build authenticated the official Node metadata/archive, inspected + 5,774 upstream tar entries before selective extraction, executed Node v24.18.0, source-built + Orca-patched `node-pty@1.1.0` against the exact Node 24 headers, stripped the resulting Linux + addon, copied exactly one Linux arm64 glibc watcher native package, and preserved the existing + relay/watcher content hash. Full archive and unpacked-tree verification agreed on content ID + `sha256:cf0acba7a8839d5ee422755562ce30ffa4433a663991054d0b5d9681ebc54832`. + The bundled runtime produced PTY input, 101×37 resize, and exit code 23 plus watcher create, + update, rename-as-delete/create, and delete events. The same tree repacked byte-for-byte + identically: both archives hashed to + `sha256:b1b9857a42d45a068f03f2484f98fcebe82b3d21ddd53eac7d0bdabf494a7f9e`. +- Duration and resource metrics: exclusive artifact staging to finalized identity took 138.666 + seconds based on output birth/finalization timestamps; deterministic repack compression took + approximately 109 seconds; full archive/tree/native verification took 3,746.221 ms; bundled + PTY/watcher smoke took 248.879 ms with 35,328,000-byte RSS. The archive is 28,192,132 bytes, + expands to 122,865,156 bytes, and contains 34 files/49 total entries. Build peak memory, open-file + count, and cancellation settlement were not instrumented and remain CI follow-ups. After adding + bounded native-command/xz execution, compressed-size prechecks, exact mode verification, frozen + installs, and automatic parent creation, a fresh build into the previously absent + `/evidence/new-parent/artifact` completed in 89,580.560 ms and retained the exact content/archive + hashes above; verification took 3,515.039 ms and smoke took 192.609 ms with 34,885,632-byte RSS. +- Artifact/log/trace link: unpublished local artifact directory + `/tmp/orca-runtime-linux-arm64-v3/new-parent/artifact`; archive hash above; SBOM hash + `sha256:27f81b5650b088dc8e01563b0229c978f8ea05828e90a1a4fd0d11640d6cc54a`; + provenance hash + `sha256:dc1433c481b57de44929248ddb033f2a35bfdb40fc32d9f442b9d8f8ed7f816c`; + identity hash + `sha256:95b4ca6bcce846c65c6b15c35df548db0598d8156aee1c54c196b95f998824a6`. +- Oracle proved: one native Linux arm64 glibc artifact-only build; exact official Node signature and + archive input; bounded source-tar inspection; exclusive selective extraction; Node-ABI-matched + patched `node-pty`; one correct watcher package; runtime-only JavaScript/license closure; SPDX + SBOM/provenance/identity assets; executable modes; deterministic `tar.xz`; exact archive/tree + hashes; and direct bundled Node/native PTY/watcher execution before any upload. +- Does not prove: GitHub runner reproducibility, Linux x64, macOS, Windows/zip, musl, oldest glibc or + kernel, native code signing/trust, quarantine/Gatekeeper/WDAC/AV, SSH/SFTP/system-SSH transfer, + relay RPC conformance, cancellation/file-handle/peak-memory budgets, release publication, cache, + fallback, UI, or any enabled tuple. Docker Desktop architecture is native arm64 but this is not an + approved oldest-baseline or cross-family remote cell. The verified official Node binary reports + upstream debug metadata and was intentionally preserved byte-for-byte; only Orca-built + `pty.node` was stripped. Both plan artifacts now state that precedence explicitly. +- Checklist items satisfied: named assembly script; POSIX deterministic archive; pre-archive mode + verification; local POSIX archive/smoke commands; Linux arm64 build/provenance, bundled Node, + patched real PTY, and watcher cells only. +- Follow-up: run `.github/workflows/ssh-relay-runtime-artifacts.yml` on the exact draft-PR head, + record resolved runner image identities and artifacts, implement Windows zip/native assembly + separately, and leave every tuple disabled until oldest-baseline, trust, and both live SSH layers + are complete. + +### E-M3-STATIC-001 — Work Package 2 focused and repository static gates + +- Date: 2026-07-14 +- Commit SHA / PR: `0c299fe189310b6dbd539f0f0f506b240524ba6a` plus uncommitted Work + Package 2 implementation; no Work Package 2 PR yet +- Runner: macOS 26.2 arm64; Node v26.0.0 and pnpm 10.24.0 +- Remote and transport: not applicable; local source/static verification only +- Exact commands: + + ```sh + 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 \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm run typecheck + pnpm run lint + pnpm exec oxfmt --check .github/workflows/ssh-relay-runtime-artifacts.yml \ + config/scripts/*ssh-relay* config/ssh-relay-node-release-v24.18.0.json + pnpm run check:max-lines-ratchet + git diff --check + ``` + +- Result: PASS. Four focused suites passed 15/15 tests in 993 ms; typecheck passed; repository lint + passed with only pre-existing warnings outside Work Package 2; formatting passed for 20 matched + files; max-lines reported 355 grandfathered suppressions and no new bypass; diff check passed. +- Duration and resource metrics: full lint completed in 20.438 seconds. Peak memory, open files, and + cancellation settlement were not instrumented by these static commands. +- Artifact/log/trace link: purpose-named tests and local command output; durable GitHub job links are + pending the draft PR. +- Oracle proved: hostile release-input/archive cases, canonical identity parity, deterministic + archive/exact-tree and permission rejection, workflow runner/action/no-publication/frozen-install + contracts, compilation, repository lint policy, formatting, line budget, and whitespace safety. +- Does not prove: Node 24 source-tool execution, target-native runners beyond E-M3-RUNTIME-LOCAL-001, + macOS/x64, signing/trust, oldest baselines, SSH transfer, relay RPCs, publication, cancellation + settlement, or any enabled tuple. The exact workflow head and Node 24 gates require PR CI. +- Checklist items satisfied: current Work Package 2 local handoff/static gate only. +- Follow-up: create the isolated stacked draft PR, run the four target-native POSIX jobs, and replace + uncommitted/local evidence with exact commit, run, job, image, artifact, and duration identifiers. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -2175,8 +2401,9 @@ owner and promotion condition. | Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | | ------------------------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------ | -| Bundled runtime not implemented | Proven legacy path only; WP1 contracts have no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Work Packages 2–7 plus Milestones 3–14 | Open | +| Bundled runtime only partially implemented | One unpublished Linux arm64 artifact proof; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | | No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | +| Windows runtime/zip builder absent | Windows targets remain entirely on the legacy path | POSIX proof cannot establish Windows behavior | Codex implementation owner | Bounded deterministic zip, Node/native build, signing, smoke, and live gates | Open | | Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | | Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | | Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | @@ -2223,9 +2450,9 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Create the isolated stacked Work Package 2 branch/PR and implement target-native runtime assembly, -archive inspection, and executable bundled-Node/native-module/PTY/watcher smoke as artifact-only -capabilities. The cross-family Layer B target pool, protected manifest-signing environment, and -paired legacy performance baseline remain release/default-path blockers; no publication, desktop -resolver, SSH transfer/install, per-target Beta, fallback, or default behavior may be connected by -this package. +Run `.github/workflows/ssh-relay-runtime-artifacts.yml` on the exact stacked draft-PR head and record +the four native POSIX runner results without publishing assets. Then implement Windows zip/native +assembly as its own evidence-gated slice. The cross-family Layer B target pool, protected +manifest-signing environment, and paired legacy performance baseline remain release/default-path +blockers; no publication, desktop resolver, SSH transfer/install, per-target Beta, fallback, tuple +enablement, or default behavior may be connected by this package. diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 64201e24438..b3198b31b8b 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1037,9 +1037,11 @@

Make fallback explicit and observable

Gate rollout by evidence

- Publish assets before consuming them, then progress through opt-in RC, internal - canary, default-on RC, and stable. Enable only tuples with complete live evidence; - keep the fallback through at least two stable cycles. + Publish assets before consuming them, then ship the per-target Beta through RC and + stable while every target remains on legacy unless explicitly opted in. Any + default-on RC or stable promotion requires a separate reviewed decision. Enable + only tuples with complete live evidence; keep the fallback through at least two + stable cycles.

@@ -1147,8 +1149,10 @@

Build for the runtime, architecture, and libc together.

  • Runtime metadata, dependency licenses, and a file-level SHA-256 manifest.
  • - No development dependencies, package manager, compiler, source maps, or PDBs - unless explicitly needed for diagnostics. + No development dependencies, package manager, compiler, source maps, PDBs, or + Orca-built debug symbols unless explicitly needed for diagnostics. Preserve the + verified official Node executable byte-for-byte even when upstream metadata is + present; never strip or rewrite those authenticated bytes.
  • diff --git a/package.json b/package.json index ef3d4534b01..12c0fe1b542 100644 --- a/package.json +++ b/package.json @@ -43,11 +43,13 @@ "dev-stable-name": "pnpm run ensure:electron-runtime && node config/scripts/run-electron-vite-dev.mjs --stable-name", "dev:web": "vite --config vite.web.config.ts --host 127.0.0.1", "build:relay": "node config/scripts/build-relay.mjs", + "build:ssh-relay-runtime": "node config/scripts/build-ssh-relay-runtime.mjs", "build:computer-macos": "node config/scripts/build-computer-macos.mjs", "build:notification-status-macos": "node config/scripts/build-notification-status-macos.mjs", "build:native": "node config/scripts/build-native-for-platform.mjs", "smoke:computer": "node config/scripts/computer-use-smoke.mjs", "verify:computer-native": "node config/scripts/verify-computer-native.mjs", + "verify:ssh-relay-runtime": "node config/scripts/verify-ssh-relay-runtime.mjs", "verify:cli-bin": "node config/scripts/verify-cli-bin.mjs", "verify:localization-catalog": "node config/scripts/verify-localization-catalog.mjs", "sync:localization-catalog": "node config/scripts/verify-localization-catalog.mjs --fix", @@ -211,6 +213,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.4", + "tar": "7.5.16", "tw-animate-css": "^1.4.0", "typescript": "^7.0.2", "typescript-api": "npm:typescript@6.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a3136394d1..d879a29059f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -347,6 +347,9 @@ importers: tailwindcss: specifier: ^4.2.4 version: 4.2.4 + tar: + specifier: 7.5.16 + version: 7.5.16 tw-animate-css: specifier: ^1.4.0 version: 1.4.0 From f8a230ef301fc6f2bb76082f549e3b3ae725c142 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 09:04:47 -0400 Subject: [PATCH 002/267] docs(ssh): record runtime artifact draft PR Co-authored-by: Orca --- ...-github-release-implementation-checklist.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) 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 a0f6a45c359..d2d5d41c22f 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 @@ -58,8 +58,9 @@ same change as the work it records. [#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 - smoke, SBOM, and provenance only. It may produce test artifacts but must not publish, resolve, - transfer, install, launch, or enable them. + smoke, SBOM, and provenance only at implementation commit `f2b387b21` in stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). It 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, 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 @@ -1135,7 +1136,7 @@ Update status and evidence as work begins. Do not combine these into one large b | ------------------------- | ------------------------------------------------------------------------------------------ | --------------------------- | ---------------------------------------- | ----------------------------------------------------------------------- | | 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 | In progress; one local POSIX tuple green | E-M3-NODE-RED-001, E-M3-NODE-PROVENANCE-001, E-M3-RUNTIME-LOCAL-001, E-M3-STATIC-001 | +| 2. Runtime builds | Per-tuple assembly, native smoke, SBOM/provenance/signing | None | Draft PR #8741; native CI pending | `f2b387b21`; E-M3-NODE-RED-001, E-M3-NODE-PROVENANCE-001, E-M3-RUNTIME-LOCAL-001, E-M3-STATIC-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 | — | @@ -2217,7 +2218,8 @@ fragmentLinks=9`. ### E-M3-NODE-PROVENANCE-001 — Pinned Node metadata and archive verification - Date: 2026-07-14 -- Commit SHA / PR: `0c299fe189310b6dbd539f0f0f506b240524ba6a` plus uncommitted Work Package 2 implementation; no Work Package 2 PR yet +- Commit SHA / PR: implementation commit `f2b387b21bbe6a3863ff1a492a1a03b65e0a0477` in stacked + draft PR [#8741](https://github.com/stablyai/orca/pull/8741); native PR CI pending - Runner: focused tests on macOS 26.2 arm64 with Node v26.0.0/pnpm 10.24.0; real proof in Docker Engine 29.2.1 on Linux/arm64 using `node:24-bookworm` resolved to `node@sha256:032e78d7e54e352129831743737e3a83171d9cc5b5896f411649c597ce0b11ea`, @@ -2269,8 +2271,8 @@ fragmentLinks=9`. ### E-M3-RUNTIME-LOCAL-001 — First target-native Linux arm64 runtime artifact - Date: 2026-07-14 -- Commit SHA / PR: `0c299fe189310b6dbd539f0f0f506b240524ba6a` plus uncommitted Work - Package 2 implementation; no Work Package 2 PR yet +- Commit SHA / PR: implementation commit `f2b387b21bbe6a3863ff1a492a1a03b65e0a0477` in stacked + draft PR [#8741](https://github.com/stablyai/orca/pull/8741); native PR CI pending - Runner: macOS 26.2 arm64 host with Docker Engine 29.2.1; native Linux/arm64 container `node@sha256:032e78d7e54e352129831743737e3a83171d9cc5b5896f411649c597ce0b11ea` (Debian bookworm, build Node v24.17.0), bundled Node v24.18.0 ABI 137, Python 3.11.2, GNU @@ -2357,8 +2359,8 @@ fragmentLinks=9`. ### E-M3-STATIC-001 — Work Package 2 focused and repository static gates - Date: 2026-07-14 -- Commit SHA / PR: `0c299fe189310b6dbd539f0f0f506b240524ba6a` plus uncommitted Work - Package 2 implementation; no Work Package 2 PR yet +- Commit SHA / PR: implementation commit `f2b387b21bbe6a3863ff1a492a1a03b65e0a0477` in stacked + draft PR [#8741](https://github.com/stablyai/orca/pull/8741); native PR CI pending - Runner: macOS 26.2 arm64; Node v26.0.0 and pnpm 10.24.0 - Remote and transport: not applicable; local source/static verification only - Exact commands: From 151628992f8d05d11902604650b3ed884992da5c Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 09:10:28 -0400 Subject: [PATCH 003/267] fix(ssh): canonicalize runtime watcher smoke paths Co-authored-by: Orca --- .../scripts/ssh-relay-runtime-smoke-child.cjs | 8 ++- ...github-release-implementation-checklist.md | 62 +++++++++++++++++-- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/config/scripts/ssh-relay-runtime-smoke-child.cjs b/config/scripts/ssh-relay-runtime-smoke-child.cjs index 7ac0b483df4..f4432a6d236 100644 --- a/config/scripts/ssh-relay-runtime-smoke-child.cjs +++ b/config/scripts/ssh-relay-runtime-smoke-child.cjs @@ -1,6 +1,6 @@ 'use strict' -const { appendFile, mkdtemp, rename, rm, unlink, writeFile } = require('node:fs/promises') +const { appendFile, mkdtemp, realpath, rename, rm, unlink, writeFile } = require('node:fs/promises') const { tmpdir } = require('node:os') const { join } = require('node:path') const { createRequire } = require('node:module') @@ -90,13 +90,17 @@ async function waitFor(events, predicate, label) { while (!predicate(events)) { await Promise.race([new Promise((resolve) => setTimeout(resolve, 25)), timer.promise]) } + } catch (error) { + throw new Error(`${error.message}; observed=${JSON.stringify(events.slice(-20))}`) } finally { timer.cancel() } } async function watcherSmoke() { - const directory = await mkdtemp(join(tmpdir(), 'orca-runtime-watcher-')) + const createdDirectory = await mkdtemp(join(tmpdir(), 'orca-runtime-watcher-')) + // Why: macOS FSEvents reports `/private/var` even when tmpdir returned the `/var` symlink. + const directory = await realpath(createdDirectory) const first = join(directory, 'first.txt') const second = join(directory, 'renamed.txt') const events = [] 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 d2d5d41c22f..1ccbf0806b1 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 @@ -66,7 +66,9 @@ same change as the work it records. 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 watcher events. E-M3-STATIC-001 records the current focused, type, lint, format, line-budget, and - diff gates. Oldest-baseline, native-trust, SSH, and every other tuple cell remain open. + diff gates. The first exact-head native run exposed a macOS `/var` versus `/private/var` watcher + oracle mismatch under E-M3-CI-RED-001; the canonical-path fix is pending rerun. Oldest-baseline, + native-trust, SSH, and every other tuple cell remain open. - Production behavior: unchanged; Orca embeds relay JavaScript and installs `node-pty` plus `@parcel/watcher` with remote npm. - New runtime assets published: none. @@ -79,8 +81,9 @@ same change as the work it records. - Rollout control: existing per-SSH-target configuration; legacy is the default and the bundled runtime is an explicit per-target Beta opt-in under E-M1-ROLLOUT-DECISION-001. - Legacy fallback removal: not authorized. -- Next required action: run the isolated four-tuple POSIX artifact workflow on the exact draft-PR - head and record each native GitHub runner result. Keep Windows runtime/zip implementation, +- Next required action: push the macOS watcher-path oracle correction, rerun the isolated four-tuple + POSIX artifact workflow on the exact draft-PR head, and record each native GitHub runner result. + Keep Windows runtime/zip implementation, cross-family remote infrastructure, signing/trust, and measured legacy baseline gates open; do not introduce publication, resolver, transfer, rollout, tuple enablement, or default behavior. @@ -1136,7 +1139,7 @@ Update status and evidence as work begins. Do not combine these into one large b | ------------------------- | ------------------------------------------------------------------------------------------ | --------------------------- | ---------------------------------------- | ----------------------------------------------------------------------- | | 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 CI pending | `f2b387b21`; E-M3-NODE-RED-001, E-M3-NODE-PROVENANCE-001, E-M3-RUNTIME-LOCAL-001, E-M3-STATIC-001 | +| 2. Runtime builds | Per-tuple assembly, native smoke, SBOM/provenance/signing | None | Draft PR #8741; macOS oracle fix pending | `f2b387b21`; E-M3-NODE-RED-001, E-M3-NODE-PROVENANCE-001, E-M3-RUNTIME-LOCAL-001, E-M3-STATIC-001, E-M3-CI-RED-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 | — | @@ -2396,6 +2399,57 @@ fragmentLinks=9`. - Follow-up: create the isolated stacked draft PR, run the four target-native POSIX jobs, and replace uncommitted/local evidence with exact commit, run, job, image, artifact, and duration identifiers. +### E-M3-CI-RED-001 — Native macOS watcher path-oracle failure + +- Date: 2026-07-14 +- Commit SHA / PR: exact draft-PR head `f8a230ef301fc6f2bb76082f549e3b3ae725c142` in + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner and jobs: GitHub Actions run + [29335018198](https://github.com/stablyai/orca/actions/runs/29335018198); macOS arm64 job + [87092056703](https://github.com/stablyai/orca/actions/runs/29335018198/job/87092056703) used + `macos-15-arm64` image `20260706.0213.1`, macOS 15.7.7, runner arm64, and Node v24.18.0; + macOS x64 job + [87092056769](https://github.com/stablyai/orca/actions/runs/29335018198/job/87092056769) used + `macos-15` image `20260629.0276.1`, runner x64, and Node v24.18.0 +- Remote and transport: no SSH remote; target-native artifact build and local execution on each + hosted runner; Node inputs downloaded from the exact pinned nodejs.org URLs +- Exact workflow/log commands: + + ```sh + gh run watch 29335018198 --repo stablyai/orca --exit-status --interval 10 + gh api -H 'Accept: application/vnd.github+json' \ + repos/stablyai/orca/actions/jobs/87092056703/logs + gh api -H 'Accept: application/vnd.github+json' \ + repos/stablyai/orca/actions/jobs/87092056769/logs + ``` + +- Result: expected discriminating FAIL. Both macOS jobs passed exact-head checkout, Node 24 setup, + frozen dependency install, 15/15 contract tests, authenticated Node input download, target-native + Node/`node-pty`/watcher assembly, deterministic archive inspection, and full tree hashing. Native + smoke then timed out after 15 seconds waiting for watcher `create`; evidence upload correctly did + not run. Both Linux jobs completed build, verification, smoke, and unpublished upload, but their + cells remain provisional until the corrected exact-head matrix reruns. +- Duration and resource metrics: macOS arm64 job failed in 1m51s after a 54,699.324 ms artifact + build; macOS x64 failed in 3m44s after a 105,665.482 ms artifact build. Each watcher wait settled + at its declared 15-second bound. Peak memory and open-file counts were not recorded in the failed + macOS smoke because no success payload was emitted. +- Artifact/log/trace link: run and job links above. The arm64 pre-smoke artifact had content ID + `sha256:cc205af32168718f9662e8513338ad844ba4c1210c247623d84f92778afa1610` and archive hash + `sha256:cd9cca6f56c9645a73b6b022515668ef7392c2eeb8f01d63b8a46b046cf337a3`; + the x64 pre-smoke artifact had content ID + `sha256:82a7017f6c1f703258a9d6b9f3d5ad4d5c57320f1b667fd87902cb1f4cd21cd3` and archive hash + `sha256:88670165fcdf55236fb2a81d70566b913b39824362b5cf5f0136e646bf9c9b2c`. +- Oracle proved: the native matrix and exact-head provenance gates discriminate after successful + builds; macOS FSEvents can report canonical `/private/var/...` paths while `tmpdir()` supplied the + `/var/...` symlink spelling, so the equality oracle could reject the same filesystem object. +- Does not prove: a passing macOS watcher, upload, native trust/signing, oldest baseline, SSH, + relay RPCs, Windows, or any enabled tuple. Successful Linux results from this failed aggregate do + not satisfy the final corrected-head evidence requirement. +- Checklist items satisfied: red half of the macOS target-native watcher-smoke gate only; no tuple + checkbox was changed. +- Follow-up: canonicalize the watched temporary directory before constructing expected event paths, + retain bounded observed-event diagnostics, rerun all four jobs, and require every upload to pass. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From c308107a96d90c57155b6684ee5f657c6e077ef2 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 09:23:40 -0400 Subject: [PATCH 004/267] docs(ssh): record native runtime artifact CI Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 136 ++++++++++++------ 1 file changed, 95 insertions(+), 41 deletions(-) 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 1ccbf0806b1..e913d49f7f4 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
    Last updated: 2026-07-14
    -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — Work Package 1 contracts are CI-green; cross-family remote infrastructure and measured baselines remain open; no bundled-runtime path is enabled
    +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — four POSIX native-runner artifact builds are CI-green; Windows runtime/ZIP, reproducibility, oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
    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) @@ -58,7 +58,7 @@ same change as the work it records. [#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 - smoke, SBOM, and provenance only at implementation commit `f2b387b21` in stacked draft PR + smoke, SBOM, and provenance only at exact implementation head `151628992` in stacked draft PR [#8741](https://github.com/stablyai/orca/pull/8741). It 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, @@ -67,8 +67,10 @@ same change as the work it records. archive inspection, deterministic repack, bundled Node 24.18.0 execution, real patched PTY, and watcher events. E-M3-STATIC-001 records the current focused, type, lint, format, line-budget, and diff gates. The first exact-head native run exposed a macOS `/var` versus `/private/var` watcher - oracle mismatch under E-M3-CI-RED-001; the canonical-path fix is pending rerun. Oldest-baseline, - native-trust, SSH, and every other tuple cell remain open. + oracle mismatch under E-M3-CI-RED-001. The corrected exact-head run passed target-native build, + archive/tree verification, bundled Node, patched PTY, and watcher smoke for Linux x64/arm64 and + macOS x64/arm64 under E-M3-CI-001. Same-runner clean-rebuild identity, oldest-baseline, + native-trust, SSH, Windows, and musl cells remain open. - Production behavior: unchanged; Orca embeds relay JavaScript and installs `node-pty` plus `@parcel/watcher` with remote npm. - New runtime assets published: none. @@ -81,9 +83,9 @@ same change as the work it records. - Rollout control: existing per-SSH-target configuration; legacy is the default and the bundled runtime is an explicit per-target Beta opt-in under E-M1-ROLLOUT-DECISION-001. - Legacy fallback removal: not authorized. -- Next required action: push the macOS watcher-path oracle correction, rerun the isolated four-tuple - POSIX artifact workflow on the exact draft-PR head, and record each native GitHub runner result. - Keep Windows runtime/zip implementation, +- Next required action: implement the bounded deterministic Windows runtime/ZIP builder and verifier + as the next artifact-only slice, add native Windows x64/arm64 runner jobs, and separately add a + same-head/same-runner clean-rebuild identity oracle for native build reproducibility. Keep cross-family remote infrastructure, signing/trust, and measured legacy baseline gates open; do not introduce publication, resolver, transfer, rollout, tuple enablement, or default behavior. @@ -577,16 +579,16 @@ Each runtime must contain only the executable closure required by the relay. ### Per-tuple build and executable proof -| Runtime tuple | Build/provenance | Bundled Node | `node-pty` load + real PTY | Watcher events | Oldest baseline | Native trust | Evidence | -| ----------------- | ---------------- | ------------ | -------------------------- | -------------- | --------------- | ------------ | ---------------------- | -| linux-x64-glibc | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | -| linux-arm64-glibc | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-RUNTIME-LOCAL-001 | -| linux-x64-musl | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | -| linux-arm64-musl | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | -| darwin-x64 | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | -| darwin-arm64 | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | -| win32-x64 | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | -| win32-arm64 | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | +| Runtime tuple | Build/provenance | Bundled Node | `node-pty` load + real PTY | Watcher events | Oldest baseline | Native trust | Evidence | +| ----------------- | ---------------- | ------------ | -------------------------- | -------------- | --------------- | ------------ | ----------------------------------- | +| linux-x64-glibc | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-CI-001 | +| linux-arm64-glibc | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-RUNTIME-LOCAL-001; E-M3-CI-001 | +| linux-x64-musl | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | +| linux-arm64-musl | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | +| darwin-x64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-CI-001 | +| darwin-arm64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-CI-001 | +| win32-x64 | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | +| win32-arm64 | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | Rules: @@ -1135,16 +1137,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; macOS oracle fix pending | `f2b387b21`; E-M3-NODE-RED-001, E-M3-NODE-PROVENANCE-001, E-M3-RUNTIME-LOCAL-001, E-M3-STATIC-001, E-M3-CI-RED-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; four POSIX jobs green; Windows active | `151628992`; E-M3-NODE-RED-001, E-M3-NODE-PROVENANCE-001, E-M3-RUNTIME-LOCAL-001, E-M3-STATIC-001, E-M3-CI-RED-001, E-M3-CI-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 | — | Every PR must document: @@ -2450,22 +2452,74 @@ fragmentLinks=9`. - Follow-up: canonicalize the watched temporary directory before constructing expected event paths, retain bounded observed-event diagnostics, rerun all four jobs, and require every upload to pass. +### E-M3-CI-001 — Corrected four-tuple POSIX native artifact run + +- Date: 2026-07-14 +- Commit SHA / PR: exact draft-PR head `151628992f8d05d11902604650b3ed884992da5c` in + [#8741](https://github.com/stablyai/orca/pull/8741) +- Workflow: GitHub Actions run + [29335399279](https://github.com/stablyai/orca/actions/runs/29335399279), conclusion `success`; + no release asset was published +- Exact evidence commands: + + ```sh + gh run view 29335399279 --repo stablyai/orca --json databaseId,headSha,status,conclusion,url,jobs + gh run view 29335399279 --repo stablyai/orca --job --log + gh api --paginate repos/stablyai/orca/actions/runs/29335399279/artifacts + gh api repos/stablyai/orca/actions/artifacts//zip + unzip -p '*.identity.json' | jq + ``` + +- Native runner, artifact, identity, and resource results: + + | Tuple | Job / requested runner / resolved image | Artifact ID / Actions ZIP bytes | Runtime archive bytes / expanded bytes / files | Content ID / archive SHA-256 | Build / full verify / smoke / RSS | + | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | + | `darwin-arm64` | [87093265074](https://github.com/stablyai/orca/actions/runs/29335399279/job/87093265074); `macos-15`; `macos15` `20260706.0213.1`; native ARM64 | `8311761833`; 24,755,748 | 24,742,840 / 122,027,869 / 35 | `16a9814a2af4adfbcce55ead3ab53f3982d8043d4b18d85762586212a481bdca` / `a7e31f92c1a0793b656f79ceff5fe63b7a1cf56a70394336a92c2ab207d30ea7` | 52,648.921 ms / 2,090.791 ms / 556.324 ms / 51,068,928 bytes | + | `darwin-x64` | [87093265143](https://github.com/stablyai/orca/actions/runs/29335399279/job/87093265143); `macos-15-intel`; `macos15` `20260629.0276.1`; native X64 | `8311834044`; 26,420,360 | 26,405,780 / 124,316,655 / 35 | `ad3a4cdaf25cd8f2657e348fca5c68df69923f9f50295afa2227a8c195e2014b` / `9b6c492b4fda250c6b6688e343499e79afc7ca53be355b1e687e61ed8430a759` | 108,328.464 ms / 1,956.297 ms / 325.008 ms / 40,546,304 bytes | + | `linux-x64-glibc` | [87093265139](https://github.com/stablyai/orca/actions/runs/29335399279/job/87093265139); `ubuntu-24.04`; `ubuntu24` `20260705.232.1`; native X64 | `8311766570`; 29,276,673 | 29,260,688 / 124,846,502 / 34 | `960546cd96c67fcf9bb0a61e96ecdbecbffd9104d3a495578f8bb19dd810649a` / `b28fc4837d17399246926ab4d565e30f21b6adc6c22401910627e820aca7c52b` | 61,805.875 ms / 2,288.790 ms / 159.950 ms / 56,315,904 bytes | + | `linux-arm64-glibc` | [87093265142](https://github.com/stablyai/orca/actions/runs/29335399279/job/87093265142); `ubuntu-24.04-arm`; `ubuntu24` `20260706.52.2`; native ARM64 | `8311785832`; 28,215,447 | 28,200,600 / 122,865,172 / 34 | `aa3aa8ae8b42334ba7b0dbe5c43fd1184e36b3f4f4a9bec0e990e9b78f090756` / `d73d03c0507408e538b93549d449359e78c986c45f1bd5811c15197720fc5da8` | 69,030.100 ms / 1,618.140 ms / 159.037 ms / 52,219,904 bytes | + +- Result: PASS. All four jobs checked out the exact source head, used Node v24.18.0, authenticated + the pinned Node release metadata and tuple archive, used a frozen source install, passed 15/15 + focused contracts, assembled on the target-native architecture, verified the complete archive and + unpacked tree before native execution, loaded the Orca-patched PTY addon, exercised a real PTY + input/101×37 resize/exit-23 lifecycle, observed bounded create/update/rename/delete watcher events, + and uploaded only an unpublished seven-day Actions artifact. +- Oracle proved: target-native build/provenance, bundled Node execution, patched native PTY load and + lifecycle, watcher lifecycle, exact archive/tree equality, size bounds, and artifact-only workflow + behavior for the four named POSIX tuples on the exact runner images above. +- Does not prove: deterministic native rebuilding. The local versus CI Linux arm64 runtime differed + in the Orca-built `pty.node`, and the red versus corrected macOS runs produced different content + identities despite no intended runtime-tree source change. The current jobs record image and + toolchain versions but do not pin all apt/Homebrew/compiler inputs or compare two clean native + builds on the same runner. This evidence also does not prove oldest OS/libc/kernel baselines, + native signing/trust, Windows/ZIP, musl, SSH/SFTP/system-SSH transfer, relay RPC conformance, + cancellation/file/channel peaks, release publication/read-back, cache, fallback, UI, or any + enabled tuple. +- Checklist items satisfied: build/provenance, bundled Node, real patched PTY, and watcher cells for + `linux-x64-glibc`, `linux-arm64-glibc`, `darwin-x64`, and `darwin-arm64` only. Oldest-baseline and + native-trust cells remain unchecked. +- Follow-up: add a same-head/same-runner two-clean-build identity comparison, implement bounded + deterministic Windows ZIP assembly and native Windows jobs, and leave all tuples disabled until + the remaining trust, baseline, and two-layer live SSH gates pass. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit owner and promotion condition. -| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | -| ------------------------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------ | -| Bundled runtime only partially implemented | One unpublished Linux arm64 artifact proof; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | -| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | -| Windows runtime/zip builder absent | Windows targets remain entirely on the legacy path | POSIX proof cannot establish Windows behavior | Codex implementation owner | Bounded deterministic zip, Node/native build, signing, smoke, and live gates | Open | -| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | -| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | -| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | -| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | -| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | -| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | +| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | +| ------------------------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------ | +| Bundled runtime only partially implemented | Four unpublished POSIX native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | +| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | +| Windows runtime/zip builder absent | Windows targets remain entirely on the legacy path | POSIX proof cannot establish Windows behavior | Codex implementation owner | Bounded deterministic zip, Node/native build, signing, smoke, and live gates | Open | +| Native clean-rebuild identity unproved | One successful artifact per corrected-head hosted runner | Toolchain drift may change native content IDs | Codex implementation owner | Same-head, same-runner clean builds match or a reviewed reproducibility policy | Open | +| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | +| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | +| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | +| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | +| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | +| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | ## Final Definition of Done @@ -2506,9 +2560,9 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Run `.github/workflows/ssh-relay-runtime-artifacts.yml` on the exact stacked draft-PR head and record -the four native POSIX runner results without publishing assets. Then implement Windows zip/native -assembly as its own evidence-gated slice. The cross-family Layer B target pool, protected +Implement bounded deterministic Windows ZIP/native assembly as its own evidence-gated slice, add +native Windows x64/arm64 jobs without publishing assets, and add a same-head/same-runner clean-build +identity comparison for native reproducibility. The cross-family Layer B target pool, protected manifest-signing environment, and paired legacy performance baseline remain release/default-path blockers; no publication, desktop resolver, SSH transfer/install, per-target Beta, fallback, tuple enablement, or default behavior may be connected by this package. From 922edb6ff28199b394f508731fd18a635bec49a0 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 09:58:21 -0400 Subject: [PATCH 005/267] build(ssh): add Windows relay runtime artifacts Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 163 +++++++++++++ config/scripts/build-ssh-relay-runtime.mjs | 73 ++++-- .../ssh-relay-node-headers-extraction.mjs | 176 ++++++++++++++ config/scripts/ssh-relay-node-pty-build.mjs | 53 ++++- .../ssh-relay-node-release-contract.mjs | 63 ++++- ...h-relay-node-release-file-verification.mjs | 22 ++ .../ssh-relay-node-release-verification.mjs | 5 +- ...h-relay-node-release-verification.test.mjs | 93 ++++++-- .../ssh-relay-node-tar-inspection.test.mjs | 9 +- .../scripts/ssh-relay-node-zip-inspection.mjs | 141 ++++++++++++ .../ssh-relay-node-zip-inspection.test.mjs | 195 ++++++++++++++++ config/scripts/ssh-relay-runtime-archive.mjs | 17 +- .../scripts/ssh-relay-runtime-provenance.mjs | 33 ++- .../scripts/ssh-relay-runtime-smoke-child.cjs | 64 ++++-- config/scripts/ssh-relay-runtime-tree.mjs | 78 +++++-- .../ssh-relay-runtime-windows-tree.test.mjs | 92 ++++++++ .../ssh-relay-runtime-workflow.test.mjs | 27 ++- config/scripts/ssh-relay-runtime-zip.mjs | 138 +++++++++++ config/scripts/ssh-relay-runtime-zip.test.mjs | 134 +++++++++++ config/scripts/ssh-relay-zip-reader.mjs | 217 ++++++++++++++++++ .../verify-ssh-relay-node-release-inputs.mjs | 29 ++- config/scripts/verify-ssh-relay-runtime.mjs | 9 +- config/ssh-relay-node-release-v24.18.0.json | 16 ++ ...github-release-implementation-checklist.md | 209 ++++++++++++++++- ...6-07-14-ssh-relay-github-release-plan.html | 24 +- package.json | 2 + pnpm-lock.yaml | 32 +++ 27 files changed, 1994 insertions(+), 120 deletions(-) create mode 100644 config/scripts/ssh-relay-node-headers-extraction.mjs create mode 100644 config/scripts/ssh-relay-node-zip-inspection.mjs create mode 100644 config/scripts/ssh-relay-node-zip-inspection.test.mjs create mode 100644 config/scripts/ssh-relay-runtime-windows-tree.test.mjs create mode 100644 config/scripts/ssh-relay-runtime-zip.mjs create mode 100644 config/scripts/ssh-relay-runtime-zip.test.mjs create mode 100644 config/scripts/ssh-relay-zip-reader.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 63a7b9b1b5a..1c0b2b51349 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -106,7 +106,10 @@ jobs: 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 \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs \ config/scripts/ssh-relay-runtime-workflow.test.mjs - name: Download exact Node release inputs @@ -160,3 +163,163 @@ jobs: path: runtime-evidence/${{ matrix.tuple }}/ if-no-files-found: error retention-days: 7 + + build-windows-runtime: + name: Build ${{ matrix.tuple }} runtime + strategy: + fail-fast: false + matrix: + include: + - runner: windows-2022 + tuple: win32-x64 + node_archive: node-v24.18.0-win-x64.zip + node_library: win-x64/node.lib + msvc_arch: x64 + - runner: windows-11-arm + tuple: win32-arm64 + node_archive: node-v24.18.0-win-arm64.zip + node_library: win-arm64/node.lib + msvc_arch: arm64 + runs-on: ${{ matrix.runner }} + timeout-minutes: 30 + + steps: + - name: Checkout exact source revision + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + # Why: artifact evidence must name the exact reviewed source, not GitHub's synthetic merge. + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Configure target-native MSVC + uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1 + with: + arch: ${{ matrix.msvc_arch }} + + - name: Setup pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6 + with: + run_install: false + + - name: Setup Node 24.18.0 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 24.18.0 + cache: pnpm + + - name: Record native runner identity + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + "requested_runner=${{ matrix.runner }}" + "resolved_image_os=$env:ImageOS" + "resolved_image_version=$env:ImageVersion" + "runner_arch=$env:RUNNER_ARCH" + "runner_environment=$env:RUNNER_ENVIRONMENT" + Get-ComputerInfo | Select-Object WindowsProductName, WindowsVersion, OsBuildNumber + node --version + "source_commit=$(git rev-parse HEAD)" + + - name: Select bundled Git signature tools + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $gitUsrBin = Join-Path $env:ProgramFiles 'Git/usr/bin' + foreach ($tool in @('gpg.exe', 'gpgv.exe')) { + if (-not (Test-Path -LiteralPath (Join-Path $gitUsrBin $tool) -PathType Leaf)) { + throw "Git for Windows does not provide required signature tool: $tool" + } + } + $gitUsrBin | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Install source dependencies without native postinstall + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + pnpm install --frozen-lockfile --ignore-scripts + if ($LASTEXITCODE -ne 0) { throw 'pnpm install failed' } + git diff --exit-code package.json pnpm-lock.yaml + if ($LASTEXITCODE -ne 0) { throw 'frozen install changed dependency files' } + + - name: Run runtime artifact contract tests + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + 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 ` + config/scripts/ssh-relay-node-zip-inspection.test.mjs ` + config/scripts/ssh-relay-runtime-artifact.test.mjs ` + config/scripts/ssh-relay-runtime-windows-tree.test.mjs ` + config/scripts/ssh-relay-runtime-zip.test.mjs ` + config/scripts/ssh-relay-runtime-workflow.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime artifact contract tests failed' } + + - name: Download exact Node release inputs + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $inputs = Join-Path $env:RUNNER_TEMP 'node-release-inputs' + New-Item -ItemType Directory -Path $inputs | Out-Null + $base = 'https://nodejs.org/dist/v24.18.0' + $assets = @( + 'SHASUMS256.txt', + 'SHASUMS256.txt.sig', + '${{ matrix.node_archive }}', + 'node-v24.18.0-headers.tar.gz', + '${{ matrix.node_library }}' + ) + foreach ($asset in $assets) { + $destination = Join-Path $inputs $asset + New-Item -ItemType Directory -Path (Split-Path -Parent $destination) -Force | Out-Null + & curl.exe --fail --silent --show-error --location --proto '=https' --tlsv1.2 ` + --connect-timeout 20 --max-time 300 --retry 2 --retry-delay 2 --retry-all-errors ` + "$base/$asset" --output $destination + if ($LASTEXITCODE -ne 0) { throw "Node input download failed: $asset" } + } + "NODE_INPUTS=$inputs" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Build, inspect, and smoke exact runtime + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $output = Join-Path $env:RUNNER_TEMP 'ssh-relay-runtime/${{ matrix.tuple }}' + New-Item -ItemType Directory -Path (Split-Path -Parent $output) -Force | Out-Null + $sourceCommit = git rev-parse HEAD + $sourceDateEpoch = git show -s --format=%ct $sourceCommit + node config/scripts/build-ssh-relay-runtime.mjs ` + --tuple '${{ matrix.tuple }}' ` + --inputs-directory $env:NODE_INPUTS ` + --output-directory $output ` + --source-date-epoch $sourceDateEpoch ` + --git-commit $sourceCommit + if ($LASTEXITCODE -ne 0) { throw 'runtime build failed' } + $identity = Join-Path $output 'orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json' + $archive = Get-ChildItem -LiteralPath $output -Filter '*.zip' -File | Select-Object -First 1 + if (-not $archive) { throw 'runtime ZIP is missing' } + node config/scripts/verify-ssh-relay-runtime.mjs ` + --runtime-directory (Join-Path $output 'runtime') ` + --identity $identity ` + --archive $archive.FullName + if ($LASTEXITCODE -ne 0) { throw 'runtime verification failed' } + $prohibited = Get-ChildItem -LiteralPath (Join-Path $output 'runtime') -Recurse -File | + Where-Object { $_.Name -in @('npm', 'npm.cmd', 'npx', 'npx.cmd', 'corepack', 'corepack.cmd') -or $_.Name.EndsWith('.map') } | + Select-Object -First 1 + if ($prohibited) { throw "Runtime contains a prohibited file: $($prohibited.FullName)" } + $parcelRoot = Join-Path $output 'runtime/node_modules/@parcel' + if (@(Get-ChildItem -LiteralPath $parcelRoot -Directory).Count -ne 2) { + throw 'Runtime must contain exactly the watcher wrapper and one native package' + } + $evidence = Join-Path $env:GITHUB_WORKSPACE 'runtime-evidence/${{ matrix.tuple }}' + New-Item -ItemType Directory -Path $evidence -Force | Out-Null + Copy-Item -LiteralPath $archive.FullName -Destination $evidence + Get-ChildItem -LiteralPath $output -File | + Where-Object { $_.Name -match '\.(?:spdx|provenance|identity)\.json$' } | + Copy-Item -Destination $evidence + + - name: Upload unpublished artifact evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: ssh-relay-runtime-${{ matrix.tuple }} + path: runtime-evidence/${{ matrix.tuple }}/ + if-no-files-found: error + retention-days: 7 diff --git a/config/scripts/build-ssh-relay-runtime.mjs b/config/scripts/build-ssh-relay-runtime.mjs index 00ac12a2808..346481f5c06 100644 --- a/config/scripts/build-ssh-relay-runtime.mjs +++ b/config/scripts/build-ssh-relay-runtime.mjs @@ -8,6 +8,7 @@ import { promisify } from 'node:util' import { buildPatchedSshRelayNodePty } from './ssh-relay-node-pty-build.mjs' import { extractVerifiedSshRelayNodeBuildInputs } from './ssh-relay-node-tar-inspection.mjs' +import { extractVerifiedSshRelayNodeZipBuildInputs } from './ssh-relay-node-zip-inspection.mjs' import { createSshRelayRuntimeArchive, inspectSshRelayRuntimeArchive @@ -25,7 +26,9 @@ const SUPPORTED_TUPLES = new Set([ 'linux-x64-glibc', 'linux-arm64-glibc', 'darwin-x64', - 'darwin-arm64' + 'darwin-arm64', + 'win32-x64', + 'win32-arm64' ]) const BUILD_COMMAND_TIMEOUT_MS = 10 * 60 * 1000 @@ -83,7 +86,7 @@ export function parseBuildArguments(argv) { } function assertTargetNative(tuple) { - const os = tuple.startsWith('linux-') ? 'linux' : 'darwin' + const os = tuple.startsWith('linux-') ? 'linux' : tuple.startsWith('darwin-') ? 'darwin' : 'win32' const architecture = tuple.includes('arm64') ? 'arm64' : 'x64' if (process.platform !== os || process.arch !== architecture) { throw new Error(`Runtime ${tuple} must be assembled on target-native ${os}/${architecture}`) @@ -114,25 +117,31 @@ async function run(command, args, options = {}) { } async function toolchainDetails(nodePath) { - const commands = [ - ['bundledNode', nodePath, ['--version']], - ['xz', 'xz', ['--version']], - ['compiler', process.platform === 'win32' ? 'cl.exe' : 'c++', ['--version']], - [ - 'strip', - process.platform === 'darwin' - ? 'xcrun' - : process.platform === 'win32' - ? 'llvm-strip.exe' - : 'strip', - process.platform === 'darwin' ? ['--find', 'strip'] : ['--version'] - ], - ['python', process.platform === 'win32' ? 'python.exe' : 'python3', ['--version']] - ] + const commands = + process.platform === 'win32' + ? [ + ['bundledNode', nodePath, ['--version']], + ['compiler', 'cl.exe', []], + ['python', 'python.exe', ['--version']] + ] + : [ + ['bundledNode', nodePath, ['--version']], + ['xz', 'xz', ['--version']], + ['compiler', 'c++', ['--version']], + [ + 'strip', + process.platform === 'darwin' ? 'xcrun' : 'strip', + process.platform === 'darwin' ? ['--find', 'strip'] : ['--version'] + ], + ['python', 'python3', ['--version']] + ] const details = {} for (const [name, command, args] of commands) { details[name] = (await run(command, args)).split(/\r?\n/)[0] } + if (process.platform === 'win32') { + details.zip = 'yazl 3.3.1' + } details.buildNode = process.version return details } @@ -159,12 +168,30 @@ export async function buildSshRelayRuntime(options) { try { await run(process.execPath, [resolve(scriptDirectory, 'build-relay.mjs')], { cwd: projectRoot }) const extractedDirectory = join(workDirectory, 'node-inputs') - const extracted = await extractVerifiedSshRelayNodeBuildInputs( - release, - options.tuple, - join(options.inputsDirectory, release.archives[options.tuple].name), - extractedDirectory - ) + const nodeArchivePath = join(options.inputsDirectory, release.archives[options.tuple].name) + const extracted = options.tuple.startsWith('win32-') + ? await extractVerifiedSshRelayNodeZipBuildInputs( + release, + options.tuple, + nodeArchivePath, + extractedDirectory, + { + headersArchivePath: join( + options.inputsDirectory, + release.windowsBuildInputs.headersArchive.name + ), + importLibraryPath: join( + options.inputsDirectory, + ...release.windowsBuildInputs.importLibraries[options.tuple].name.split('/') + ) + } + ) + : await extractVerifiedSshRelayNodeBuildInputs( + release, + options.tuple, + nodeArchivePath, + extractedDirectory + ) const bundledVersion = await run(extracted.nodePath, ['--version']) if (bundledVersion !== `v${release.nodeVersion}`) { throw new Error(`Bundled Node version mismatch: ${bundledVersion}`) diff --git a/config/scripts/ssh-relay-node-headers-extraction.mjs b/config/scripts/ssh-relay-node-headers-extraction.mjs new file mode 100644 index 00000000000..0d771240c26 --- /dev/null +++ b/config/scripts/ssh-relay-node-headers-extraction.mjs @@ -0,0 +1,176 @@ +import { constants } from 'node:fs' +import { createReadStream } from 'node:fs' +import { copyFile, mkdtemp, rm, stat } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pipeline } from 'node:stream/promises' +import { createGunzip } from 'node:zlib' + +import { Parser, Unpack } from 'tar' + +import { verifySshRelayNodeWindowsBuildInput } from './ssh-relay-node-release-file-verification.mjs' +import { validateSshRelayNodeReleaseContract } from './ssh-relay-node-release-contract.mjs' + +const MAX_ENTRIES = 100_000 +const MAX_EXPANDED_BYTES = 1024 * 1024 * 1024 +const MAX_FILE_BYTES = 256 * 1024 * 1024 +const MAX_PATH_BYTES = 512 +const MAX_DEPTH = 32 +const GZIP_TIMEOUT_MS = 5 * 60 * 1000 +const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i + +export function assertPortableSshRelayNodeHeaderPath(value) { + if ( + typeof value !== 'string' || + value.length === 0 || + Buffer.byteLength(value) > MAX_PATH_BYTES || + value.startsWith('/') || + value.includes('\\') || + /^[A-Za-z]:/.test(value) + ) { + throw new Error('Node headers entry is not a bounded portable relative path') + } + for (const character of value) { + const code = character.charCodeAt(0) + if (code <= 0x1f || code === 0x7f) { + throw new Error('Node headers entry contains a control character') + } + } + const path = value.endsWith('/') ? value.slice(0, -1) : value + const segments = path.split('/') + if ( + segments.length > MAX_DEPTH || + segments.some( + (segment) => + segment === '' || + segment === '.' || + segment === '..' || + segment.includes(':') || + segment.endsWith('.') || + segment.endsWith(' ') || + WINDOWS_DEVICE_NAME.test(segment) + ) + ) { + throw new Error('Node headers entry contains an unsafe path segment') + } + return path +} + +function createState(root) { + return { + root, + seen: new Set(), + foldedPaths: new Set(), + entries: 0, + files: 0, + expandedBytes: 0, + hasNodeHeader: false, + hasCommonGypi: false, + hasConfigGypi: false + } +} + +function inspectEntry(entry, state) { + const path = assertPortableSshRelayNodeHeaderPath(entry.path) + if (path !== state.root && !path.startsWith(`${state.root}/`)) { + throw new Error('Node headers entry is outside its exact versioned root') + } + if (state.seen.has(path)) { + throw new Error(`Node headers archive contains a duplicate entry: ${path}`) + } + const foldedPath = path.toLowerCase() + if (state.foldedPaths.has(foldedPath)) { + throw new Error(`Node headers archive contains a case-fold collision: ${path}`) + } + state.seen.add(path) + state.foldedPaths.add(foldedPath) + state.entries += 1 + if (state.entries > MAX_ENTRIES) { + throw new Error('Node headers archive exceeds the entry-count limit') + } + if (entry.type !== 'File' && entry.type !== 'Directory') { + throw new Error(`Node headers archive contains a prohibited entry type: ${entry.type}`) + } + if (entry.type === 'File') { + if (!Number.isSafeInteger(entry.size) || entry.size < 0 || entry.size > MAX_FILE_BYTES) { + throw new Error('Node headers archive file exceeds the per-file size limit') + } + state.files += 1 + state.expandedBytes += entry.size + if (state.expandedBytes > MAX_EXPANDED_BYTES) { + throw new Error('Node headers archive exceeds the expanded-size limit') + } + } + state.hasNodeHeader ||= path === `${state.root}/include/node/node.h` && entry.type === 'File' + state.hasCommonGypi ||= path === `${state.root}/include/node/common.gypi` && entry.type === 'File' + state.hasConfigGypi ||= path === `${state.root}/include/node/config.gypi` && entry.type === 'File' +} + +function result(state) { + if (!state.hasNodeHeader || !state.hasCommonGypi || !state.hasConfigGypi) { + throw new Error('Node headers archive is missing required node-gyp inputs') + } + return { + root: state.root, + entries: state.entries, + files: state.files, + expandedBytes: state.expandedBytes + } +} + +async function pipeGzip(archivePath, destination, signal) { + await pipeline(createReadStream(archivePath), createGunzip(), destination, { signal }) +} + +async function inspectArchive(archivePath, root, signal) { + const state = createState(root) + const parser = new Parser({ strict: true }) + parser.on('entry', (entry) => { + try { + inspectEntry(entry, state) + entry.resume() + } catch (error) { + parser.abort(error) + } + }) + await pipeGzip(archivePath, parser, signal) + return result(state) +} + +export async function extractVerifiedSshRelayNodeHeaders( + releaseInput, + archivePath, + nodeRoot, + { signal } = {} +) { + const release = validateSshRelayNodeReleaseContract(releaseInput) + const effectiveSignal = signal ?? AbortSignal.timeout(GZIP_TIMEOUT_MS) + await verifySshRelayNodeWindowsBuildInput(release, 'headers', undefined, archivePath) + const stagingDirectory = await mkdtemp(join(tmpdir(), 'orca-node-verified-headers-')) + const stagedArchivePath = join(stagingDirectory, release.windowsBuildInputs.headersArchive.name) + const root = `node-v${release.nodeVersion}` + try { + await copyFile(archivePath, stagedArchivePath, constants.COPYFILE_EXCL) + await verifySshRelayNodeWindowsBuildInput(release, 'headers', undefined, stagedArchivePath) + const inspection = await inspectArchive(stagedArchivePath, root, effectiveSignal) + const unpack = new Unpack({ + cwd: nodeRoot, + strict: true, + preservePaths: false, + strip: 1, + filter: (path, entry) => + (entry.type === 'File' || entry.type === 'Directory') && + (path === `${root}/include` || path.startsWith(`${root}/include/`)) + }) + await pipeGzip(stagedArchivePath, unpack, effectiveSignal) + if (!(await stat(join(nodeRoot, 'include', 'node', 'node.h'))).isFile()) { + throw new Error('Extracted Node headers do not contain include/node/node.h') + } + return inspection + } catch (error) { + await rm(join(nodeRoot, 'include'), { recursive: true, force: true }) + throw error + } finally { + await rm(stagingDirectory, { recursive: true, force: true }) + } +} diff --git a/config/scripts/ssh-relay-node-pty-build.mjs b/config/scripts/ssh-relay-node-pty-build.mjs index 64dd7c93ca1..a405d0618dd 100644 --- a/config/scripts/ssh-relay-node-pty-build.mjs +++ b/config/scripts/ssh-relay-node-pty-build.mjs @@ -1,5 +1,5 @@ import { execFile } from 'node:child_process' -import { cp, mkdir, readFile, stat } from 'node:fs/promises' +import { copyFile, cp, mkdir, readFile, readdir, stat } from 'node:fs/promises' import { createRequire } from 'node:module' import { dirname, join, resolve } from 'node:path' import { promisify } from 'node:util' @@ -52,9 +52,19 @@ async function assertPatchedSource(nodePtyDirectory) { async function assertBuiltArtifacts(buildDirectory, tuple) { const releaseDirectory = join(buildDirectory, 'build', 'Release') - const ptyPath = join(releaseDirectory, 'pty.node') - if (!(await stat(ptyPath)).isFile()) { - throw new Error('node-pty did not produce build/Release/pty.node') + const nativePaths = tuple.startsWith('win32-') + ? [ + 'conpty.node', + 'conpty_console_list.node', + 'pty.node', + 'conpty/conpty.dll', + 'conpty/OpenConsole.exe' + ] + : ['pty.node'] + for (const path of nativePaths) { + if (!(await stat(join(releaseDirectory, ...path.split('/')))).isFile()) { + throw new Error(`node-pty did not produce build/Release/${path}`) + } } if (tuple.startsWith('darwin-')) { const helperPath = join(releaseDirectory, 'spawn-helper') @@ -65,6 +75,31 @@ async function assertBuiltArtifacts(buildDirectory, tuple) { } } +async function stageWindowsConptyRuntime(buildDirectory, tuple) { + if (!tuple.startsWith('win32-')) { + return + } + const conptyRoot = join(buildDirectory, 'third_party', 'conpty') + const versions = (await readdir(conptyRoot, { withFileTypes: true })).filter((entry) => + entry.isDirectory() + ) + if (versions.length !== 1) { + throw new Error('node-pty must contain exactly one pinned ConPTY runtime version') + } + const architecture = tuple.endsWith('-arm64') ? 'arm64' : 'x64' + const sourceDirectory = join(conptyRoot, versions[0].name, `win10-${architecture}`) + const destinationDirectory = join(buildDirectory, 'build', 'Release', 'conpty') + await mkdir(destinationDirectory) + // Why: direct node-gyp bypasses node-pty's postinstall, while Orca explicitly uses this DLL path. + for (const name of ['conpty.dll', 'OpenConsole.exe']) { + const sourcePath = join(sourceDirectory, name) + if (!(await stat(sourcePath)).isFile()) { + throw new Error(`node-pty is missing pinned ${architecture} ConPTY runtime file: ${name}`) + } + await copyFile(sourcePath, join(destinationDirectory, name)) + } +} + async function stripBuiltArtifacts(buildDirectory, tuple) { const releaseDirectory = join(buildDirectory, 'build', 'Release') const ptyPath = join(releaseDirectory, 'pty.node') @@ -117,23 +152,29 @@ export async function buildPatchedSshRelayNodePty({ npm_config_arch: tuple.includes('arm64') ? 'arm64' : 'x64', npm_config_build_from_source: 'true', npm_config_nodedir: nodeRoot, + // Why: a successful build must prove all Node inputs were staged, never fetched implicitly. + npm_config_offline: 'true', + npm_config_disturl: 'http://127.0.0.1:9', npm_config_target: nodeVersion } }) } finally { clearTimeout(timeout) } + await stageWindowsConptyRuntime(buildDirectory, tuple) await assertBuiltArtifacts(buildDirectory, tuple) await stripBuiltArtifacts(buildDirectory, tuple) // Why: loading with the bundled executable catches an accidental host-ABI build immediately. + const nativeNames = tuple.startsWith('win32-') ? ['conpty', 'conpty_console_list'] : ['pty'] await runCommand( nodePath, [ '-e', `const {loadNativeModule}=require(${JSON.stringify(join(buildDirectory, 'lib', 'utils.js'))});` + - `const loaded=loadNativeModule('pty');` + - `if(!loaded.dir.replace(/\\\\/g,'/').includes('build/Release/'))process.exit(2);` + `for(const name of ${JSON.stringify(nativeNames)}){` + + `const loaded=loadNativeModule(name);` + + `if(!loaded.dir.replace(/\\\\/g,'/').includes('build/Release/'))process.exit(2);}` ], { cwd: buildDirectory, windowsHide: true, env: process.env } ) diff --git a/config/scripts/ssh-relay-node-release-contract.mjs b/config/scripts/ssh-relay-node-release-contract.mjs index 4170f9f34bf..bcf24f3fcfc 100644 --- a/config/scripts/ssh-relay-node-release-contract.mjs +++ b/config/scripts/ssh-relay-node-release-contract.mjs @@ -17,6 +17,10 @@ const ARCHIVE_SUFFIXES = Object.freeze({ 'win32-arm64': 'win-arm64.zip', 'win32-x64': 'win-x64.zip' }) +const WINDOWS_LIBRARY_NAMES = Object.freeze({ + 'win32-arm64': 'win-arm64/node.lib', + 'win32-x64': 'win-x64/node.lib' +}) const HEX_SHA256 = /^[0-9a-f]{64}$/ const OPENPGP_FINGERPRINT = /^[0-9A-F]{40}$/ @@ -126,6 +130,35 @@ function validateArchives(release) { } } +function validateWindowsBuildInputs(release) { + assertRecord(release.windowsBuildInputs, 'Windows build inputs') + assertExactKeys( + release.windowsBuildInputs, + ['headersArchive', 'importLibraries'], + 'Windows build inputs' + ) + const headers = release.windowsBuildInputs.headersArchive + assertRecord(headers, 'Windows headers archive') + assertExactKeys(headers, ['name', 'sha256'], 'Windows headers archive') + if (headers.name !== `node-v${release.nodeVersion}-headers.tar.gz`) { + throw new Error('Windows headers archive name does not match the pinned Node version') + } + assertSha256(headers.sha256, 'Windows headers archive SHA-256') + + const libraries = release.windowsBuildInputs.importLibraries + assertRecord(libraries, 'Windows import libraries') + assertExactKeys(libraries, Object.keys(WINDOWS_LIBRARY_NAMES), 'Windows import libraries') + for (const [tuple, expectedName] of Object.entries(WINDOWS_LIBRARY_NAMES)) { + const library = libraries[tuple] + assertRecord(library, `Windows import library ${tuple}`) + assertExactKeys(library, ['name', 'sha256'], `Windows import library ${tuple}`) + if (library.name !== expectedName) { + throw new Error(`Windows import library name does not match ${tuple}`) + } + assertSha256(library.sha256, `Windows import library ${tuple} SHA-256`) + } +} + export function sha256(bytes) { return createHash('sha256').update(bytes).digest('hex') } @@ -141,7 +174,8 @@ export function validateSshRelayNodeReleaseContract(release) { 'checksumDocument', 'signature', 'maximumArchiveBytes', - 'archives' + 'archives', + 'windowsBuildInputs' ], 'Node release contract' ) @@ -157,6 +191,7 @@ export function validateSshRelayNodeReleaseContract(release) { validateMetadataContract(release) validateArchives(release) + validateWindowsBuildInputs(release) return release } @@ -199,16 +234,28 @@ export function verifySshRelayNodeChecksumDocument(releaseInput, bytesInput) { throw new Error('Node checksum document SHA-256 does not match the pinned contract') } - return EXPECTED_TUPLES.map((tuple) => { - const archive = release.archives[tuple] - const documentDigest = entries.get(archive.name) + const expectedFiles = [ + ...EXPECTED_TUPLES.map((tuple) => ({ + kind: 'runtime-archive', + tuple, + ...release.archives[tuple] + })), + { kind: 'windows-headers', ...release.windowsBuildInputs.headersArchive }, + ...Object.entries(release.windowsBuildInputs.importLibraries).map(([tuple, file]) => ({ + kind: 'windows-import-library', + tuple, + ...file + })) + ] + return expectedFiles.map((file) => { + const documentDigest = entries.get(file.name) if (documentDigest === undefined) { - throw new Error(`Node checksum document is missing pinned archive: ${archive.name}`) + throw new Error(`Node checksum document is missing pinned input: ${file.name}`) } - if (documentDigest !== archive.sha256) { - throw new Error(`Node checksum document digest mismatch for pinned archive: ${archive.name}`) + if (documentDigest !== file.sha256) { + throw new Error(`Node checksum document digest mismatch for pinned input: ${file.name}`) } - return { tuple, name: archive.name, sha256: archive.sha256 } + return file }) } diff --git a/config/scripts/ssh-relay-node-release-file-verification.mjs b/config/scripts/ssh-relay-node-release-file-verification.mjs index 1c9b2834d1e..fc9d4920d07 100644 --- a/config/scripts/ssh-relay-node-release-file-verification.mjs +++ b/config/scripts/ssh-relay-node-release-file-verification.mjs @@ -74,3 +74,25 @@ export async function verifySshRelayNodeArchive(releaseInput, tuple, archivePath } return { tuple, name: archive.name, ...result } } + +export async function verifySshRelayNodeWindowsBuildInput(releaseInput, kind, tuple, inputPath) { + const release = validateSshRelayNodeReleaseContract(releaseInput) + const input = + kind === 'headers' + ? release.windowsBuildInputs.headersArchive + : kind === 'import-library' + ? release.windowsBuildInputs.importLibraries[tuple] + : undefined + if (!input) { + throw new Error(`Unknown Node Windows build input: ${kind}/${String(tuple)}`) + } + const result = await hashBoundedFile( + inputPath, + release.maximumArchiveBytes, + `Node Windows ${kind}` + ) + if (result.sha256 !== input.sha256) { + throw new Error(`Node Windows ${kind} SHA-256 mismatch for ${String(tuple)}`) + } + return { kind, tuple: kind === 'headers' ? undefined : tuple, name: input.name, ...result } +} diff --git a/config/scripts/ssh-relay-node-release-verification.mjs b/config/scripts/ssh-relay-node-release-verification.mjs index 11d7bd1abfd..53444810010 100644 --- a/config/scripts/ssh-relay-node-release-verification.mjs +++ b/config/scripts/ssh-relay-node-release-verification.mjs @@ -2,5 +2,8 @@ export { validateSshRelayNodeReleaseContract, verifySshRelayNodeChecksumDocument } from './ssh-relay-node-release-contract.mjs' -export { verifySshRelayNodeArchive } from './ssh-relay-node-release-file-verification.mjs' +export { + verifySshRelayNodeArchive, + verifySshRelayNodeWindowsBuildInput +} from './ssh-relay-node-release-file-verification.mjs' export { verifySshRelayNodeSignature } from './ssh-relay-node-release-signature.mjs' diff --git a/config/scripts/ssh-relay-node-release-verification.test.mjs b/config/scripts/ssh-relay-node-release-verification.test.mjs index 62bd04b109f..fbf2d1b8e4b 100644 --- a/config/scripts/ssh-relay-node-release-verification.test.mjs +++ b/config/scripts/ssh-relay-node-release-verification.test.mjs @@ -40,6 +40,24 @@ const archives = { sha256: '6'.repeat(64) } } +const windowsBuildInputs = { + headersArchive: { + name: 'node-v24.18.0-headers.tar.gz', + sha256: '7'.repeat(64) + }, + importLibraries: { + 'win32-arm64': { name: 'win-arm64/node.lib', sha256: '8'.repeat(64) }, + 'win32-x64': { name: 'win-x64/node.lib', sha256: '9'.repeat(64) } + } +} + +function pinnedFiles() { + return [ + ...Object.values(archives), + windowsBuildInputs.headersArchive, + ...Object.values(windowsBuildInputs.importLibraries) + ] +} function contract(overrides = {}) { return { @@ -67,6 +85,7 @@ function contract(overrides = {}) { }, maximumArchiveBytes: 100 * 1024 * 1024, archives, + windowsBuildInputs, ...overrides } } @@ -92,9 +111,12 @@ describe('SSH relay Node release verification', () => { expect(release.archives['win32-x64'].sha256).toBe( '0ae68406b42d7725661da979b1403ec9926da205c6770827f33aac9d8f26e821' ) + expect(release.windowsBuildInputs.headersArchive.sha256).toBe( + '6c7d41d83c3481d2301115b8ce4a44b7d4fbfa52859b1aac14f445d460137887' + ) }) - it('accepts only the exact immutable six-archive contract', () => { + it('accepts only the exact immutable runtime and Windows-build input contract', () => { expect(validateSshRelayNodeReleaseContract(contract()).nodeVersion).toBe('24.18.0') expect(() => @@ -112,17 +134,26 @@ describe('SSH relay Node release verification', () => { archives: { ...archives, 'win32-arm64': { ...archives['win32-arm64'], sha256: 'ABC' } } }) ).toThrow(/SHA-256/i) + expect(() => + validateSshRelayNodeReleaseContract({ + ...contract(), + windowsBuildInputs: { + ...windowsBuildInputs, + headersArchive: { ...windowsBuildInputs.headersArchive, name: 'latest-headers.tar.gz' } + } + }) + ).toThrow(/headers archive name/i) }) it('cross-checks every pinned archive against the authenticated checksum document', () => { - const body = Object.values(archives) - .map((archive) => `${archive.sha256} ${archive.name}`) + const body = pinnedFiles() + .map((file) => `${file.sha256} ${file.name}`) .join('\n') const release = contract({ checksumDocument: { ...contract().checksumDocument, sha256: digest(body) } }) - expect(verifySshRelayNodeChecksumDocument(release, Buffer.from(body))).toHaveLength(6) + expect(verifySshRelayNodeChecksumDocument(release, Buffer.from(body))).toHaveLength(9) expect(() => verifySshRelayNodeChecksumDocument(release, Buffer.from(`${body}\n${body.split('\n')[0]}`)) ).toThrow(/duplicate/i) @@ -211,17 +242,26 @@ describe('SSH relay Node release verification', () => { ).rejects.toThrow(/pinned Node release signer fingerprint/i) }) - it('verifies an explicitly scoped archive through the purpose-named CLI API', async () => { + it('verifies a Windows archive, headers, and import library through the CLI API', async () => { const directory = await mkdtemp(join(tmpdir(), 'orca-node-cli-test-')) temporaryDirectories.push(directory) const inputsDirectory = join(directory, 'inputs') await mkdir(inputsDirectory) const keyPath = join(directory, 'release-key.asc') const archiveBytes = Buffer.from('archive bytes') - const checksumBody = Object.entries(archives) - .map(([tuple, archive]) => { - const sha256 = tuple === 'linux-x64-glibc' ? digest(archiveBytes) : archive.sha256 - return `${sha256} ${archive.name}` + const headersBytes = Buffer.from('headers archive bytes') + const libraryBytes = Buffer.from('import library bytes') + const checksumBody = pinnedFiles() + .map((file) => { + const sha256 = + file.name === archives['win32-x64'].name + ? digest(archiveBytes) + : file.name === windowsBuildInputs.headersArchive.name + ? digest(headersBytes) + : file.name === windowsBuildInputs.importLibraries['win32-x64'].name + ? digest(libraryBytes) + : file.sha256 + return `${sha256} ${file.name}` }) .join('\n') const signatureBytes = Buffer.from('signature bytes') @@ -234,19 +274,38 @@ describe('SSH relay Node release verification', () => { }, archives: { ...archives, - 'linux-x64-glibc': { - ...archives['linux-x64-glibc'], + 'win32-x64': { + ...archives['win32-x64'], sha256: digest(archiveBytes) } + }, + windowsBuildInputs: { + headersArchive: { + ...windowsBuildInputs.headersArchive, + sha256: digest(headersBytes) + }, + importLibraries: { + ...windowsBuildInputs.importLibraries, + 'win32-x64': { + ...windowsBuildInputs.importLibraries['win32-x64'], + sha256: digest(libraryBytes) + } + } } }) const contractPath = join(directory, 'release.json') + await mkdir(join(inputsDirectory, 'win-x64')) await Promise.all([ writeFile(contractPath, JSON.stringify(release)), writeFile(keyPath, 'release key'), writeFile(join(inputsDirectory, release.checksumDocument.name), checksumBody), writeFile(join(inputsDirectory, release.signature.name), signatureBytes), - writeFile(join(inputsDirectory, release.archives['linux-x64-glibc'].name), archiveBytes) + writeFile(join(inputsDirectory, release.archives['win32-x64'].name), archiveBytes), + writeFile( + join(inputsDirectory, release.windowsBuildInputs.headersArchive.name), + headersBytes + ), + writeFile(join(inputsDirectory, 'win-x64', 'node.lib'), libraryBytes) ]) const commandRunner = vi.fn(async () => ({ exitCode: 0, @@ -255,15 +314,19 @@ describe('SSH relay Node release verification', () => { })) const result = await verifyNodeReleaseInputs( - { contractPath, inputsDirectory, archiveTuples: ['linux-x64-glibc'] }, + { contractPath, inputsDirectory, archiveTuples: ['win32-x64'] }, { commandRunner } ) expect(result).toMatchObject({ nodeVersion: release.nodeVersion, signerFingerprint: release.signature.signerFingerprint, - checksumEntriesVerified: 6, - archives: [{ tuple: 'linux-x64-glibc', bytes: archiveBytes.length }] + checksumEntriesVerified: 9, + archives: [{ tuple: 'win32-x64', bytes: archiveBytes.length }], + windowsBuildInputs: [ + { kind: 'headers', bytes: headersBytes.length }, + { kind: 'import-library', tuple: 'win32-x64', bytes: libraryBytes.length } + ] }) expect(commandRunner).toHaveBeenCalledTimes(2) }) diff --git a/config/scripts/ssh-relay-node-tar-inspection.test.mjs b/config/scripts/ssh-relay-node-tar-inspection.test.mjs index 9c31f757a02..389bcfb9a53 100644 --- a/config/scripts/ssh-relay-node-tar-inspection.test.mjs +++ b/config/scripts/ssh-relay-node-tar-inspection.test.mjs @@ -60,7 +60,14 @@ const release = { } }, maximumArchiveBytes: 1024 * 1024, - archives + archives, + windowsBuildInputs: { + headersArchive: { name: 'node-v24.18.0-headers.tar.gz', sha256: '7'.repeat(64) }, + importLibraries: { + 'win32-arm64': { name: 'win-arm64/node.lib', sha256: '8'.repeat(64) }, + 'win32-x64': { name: 'win-x64/node.lib', sha256: '9'.repeat(64) } + } + } } const temporaryDirectories = [] diff --git a/config/scripts/ssh-relay-node-zip-inspection.mjs b/config/scripts/ssh-relay-node-zip-inspection.mjs new file mode 100644 index 00000000000..71857b800de --- /dev/null +++ b/config/scripts/ssh-relay-node-zip-inspection.mjs @@ -0,0 +1,141 @@ +import { constants } from 'node:fs' +import { copyFile, mkdir, mkdtemp, rm, stat } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { verifySshRelayNodeArchive } from './ssh-relay-node-release-file-verification.mjs' +import { extractVerifiedSshRelayNodeHeaders } from './ssh-relay-node-headers-extraction.mjs' +import { verifySshRelayNodeWindowsBuildInput } from './ssh-relay-node-release-file-verification.mjs' +import { validateSshRelayNodeReleaseContract } from './ssh-relay-node-release-contract.mjs' +import { visitSshRelayZip } from './ssh-relay-zip-reader.mjs' + +const DEFAULT_LIMITS = Object.freeze({ + maximumArchiveBytes: 200 * 1024 * 1024, + maximumEntries: 100_000, + maximumExpandedBytes: 1024 * 1024 * 1024, + maximumFileBytes: 256 * 1024 * 1024, + maximumDepth: 32, + maximumPathBytes: 512 +}) +const ZIP_TIMEOUT_MS = 5 * 60 * 1000 + +function archiveRoot(release, tuple) { + const name = release.archives[tuple]?.name + if (typeof name !== 'string' || !name.endsWith('.zip')) { + throw new Error(`Node tuple ${String(tuple)} does not use a ZIP archive`) + } + return name.slice(0, -'.zip'.length) +} + +function selectedBuildInput(root, path) { + return path === `${root}/node.exe` || path === `${root}/LICENSE` +} + +async function scanNodeZip(archivePath, release, tuple, limits, { destination, signal } = {}) { + const root = archiveRoot(release, tuple) + const state = { + root, + hasNodeExecutable: false, + hasLicense: false, + largestFileBytes: 0 + } + const result = await visitSshRelayZip( + archivePath, + limits, + async (entry, consume) => { + if (entry.path !== root && !entry.path.startsWith(`${root}/`)) { + throw new Error('Node ZIP entry is outside its exact versioned root') + } + if (entry.type === 'file') { + state.largestFileBytes = Math.max(state.largestFileBytes, entry.size) + state.hasNodeExecutable ||= entry.path === `${root}/node.exe` + state.hasLicense ||= entry.path === `${root}/LICENSE` + const relativePath = entry.path.slice(root.length + 1) + const outputPath = + destination && selectedBuildInput(root, entry.path) + ? join(destination, root, ...relativePath.split('/')) + : undefined + await consume({ outputPath, mode: entry.path === `${root}/node.exe` ? 0o755 : 0o644 }) + } + }, + { signal } + ) + if (!state.hasNodeExecutable || !state.hasLicense) { + throw new Error('Node ZIP is missing node.exe or its license') + } + return { ...result, ...state } +} + +export async function inspectSshRelayNodeZip( + archivePath, + releaseInput, + tuple, + overrides = {}, + { signal } = {} +) { + const release = validateSshRelayNodeReleaseContract(releaseInput) + const limits = { + ...DEFAULT_LIMITS, + maximumArchiveBytes: Math.min(DEFAULT_LIMITS.maximumArchiveBytes, release.maximumArchiveBytes), + ...overrides + } + return scanNodeZip(archivePath, release, tuple, limits, { signal }) +} + +export async function extractVerifiedSshRelayNodeZipBuildInputs( + releaseInput, + tuple, + sourceArchivePath, + destination, + { headersArchivePath, importLibraryPath, signal } = {} +) { + const release = validateSshRelayNodeReleaseContract(releaseInput) + const effectiveSignal = signal ?? AbortSignal.timeout(ZIP_TIMEOUT_MS) + await verifySshRelayNodeArchive(release, tuple, sourceArchivePath) + const stagingDirectory = await mkdtemp(join(tmpdir(), 'orca-node-verified-zip-')) + const stagedArchivePath = join(stagingDirectory, release.archives[tuple].name) + try { + if (!headersArchivePath || !importLibraryPath) { + throw new Error('Windows Node extraction requires explicit headers and import-library inputs') + } + await copyFile(sourceArchivePath, stagedArchivePath, constants.COPYFILE_EXCL) + await verifySshRelayNodeArchive(release, tuple, stagedArchivePath) + await mkdir(destination) + const inspection = await scanNodeZip(stagedArchivePath, release, tuple, DEFAULT_LIMITS, { + destination, + signal: effectiveSignal + }) + const extractedRoot = join(destination, inspection.root) + const nodePath = join(extractedRoot, 'node.exe') + const headers = await extractVerifiedSshRelayNodeHeaders( + release, + headersArchivePath, + extractedRoot, + { + signal: effectiveSignal + } + ) + await verifySshRelayNodeWindowsBuildInput(release, 'import-library', tuple, importLibraryPath) + const releaseDirectory = join(extractedRoot, 'Release') + await mkdir(releaseDirectory) + const stagedLibraryPath = join(releaseDirectory, 'node.lib') + await copyFile(importLibraryPath, stagedLibraryPath, constants.COPYFILE_EXCL) + await verifySshRelayNodeWindowsBuildInput(release, 'import-library', tuple, stagedLibraryPath) + const [nodeMetadata, licenseMetadata, headerMetadata] = await Promise.all([ + stat(nodePath), + stat(join(extractedRoot, 'LICENSE')), + stat(join(extractedRoot, 'include', 'node', 'node.h')) + ]) + if (!nodeMetadata.isFile() || !licenseMetadata.isFile() || !headerMetadata.isFile()) { + throw new Error('Extracted Node ZIP build inputs do not match the inspected archive contract') + } + return { ...inspection, headers, extractedRoot, nodePath } + } catch (error) { + await rm(destination, { recursive: true, force: true }) + throw error + } finally { + await rm(stagingDirectory, { recursive: true, force: true }) + } +} + +export { DEFAULT_LIMITS } diff --git a/config/scripts/ssh-relay-node-zip-inspection.test.mjs b/config/scripts/ssh-relay-node-zip-inspection.test.mjs new file mode 100644 index 00000000000..e907fe83710 --- /dev/null +++ b/config/scripts/ssh-relay-node-zip-inspection.test.mjs @@ -0,0 +1,195 @@ +import { createWriteStream } from 'node:fs' +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pipeline } from 'node:stream/promises' + +import { afterEach, describe, expect, it } from 'vitest' +import { create } from 'tar' +import yazl from 'yazl' + +import { + extractVerifiedSshRelayNodeZipBuildInputs, + inspectSshRelayNodeZip +} from './ssh-relay-node-zip-inspection.mjs' +import { assertPortableSshRelayNodeHeaderPath } from './ssh-relay-node-headers-extraction.mjs' +import { sha256 } from './ssh-relay-node-release-contract.mjs' + +const tuple = 'win32-x64' +const root = 'node-v24.18.0-win-x64' +const temporaryDirectories = [] + +afterEach(async () => { + await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true }))) +}) + +function release(archiveSha256, headersSha256 = '7'.repeat(64), librarySha256 = '9'.repeat(64)) { + return { + schemaVersion: 1, + nodeVersion: '24.18.0', + baseUrl: 'https://nodejs.org/dist/v24.18.0', + checksumDocument: { + name: 'SHASUMS256.txt', + sha256: 'a'.repeat(64), + maximumBytes: 1024 + }, + signature: { + name: 'SHASUMS256.txt.sig', + sha256: 'b'.repeat(64), + maximumBytes: 1024, + signerFingerprint: 'C'.repeat(40), + key: { + path: 'release-key.asc', + sha256: 'c'.repeat(64), + sourceCommit: 'd'.repeat(40), + sourceUrl: + `https://raw.githubusercontent.com/nodejs/release-keys/${'d'.repeat(40)}` + + `/keys/${'C'.repeat(40)}.asc` + } + }, + maximumArchiveBytes: 1024 * 1024, + archives: { + 'darwin-arm64': { name: 'node-v24.18.0-darwin-arm64.tar.xz', sha256: '1'.repeat(64) }, + 'darwin-x64': { name: 'node-v24.18.0-darwin-x64.tar.xz', sha256: '2'.repeat(64) }, + 'linux-arm64-glibc': { + name: 'node-v24.18.0-linux-arm64.tar.xz', + sha256: '3'.repeat(64) + }, + 'linux-x64-glibc': { + name: 'node-v24.18.0-linux-x64.tar.xz', + sha256: '4'.repeat(64) + }, + 'win32-arm64': { name: 'node-v24.18.0-win-arm64.zip', sha256: '5'.repeat(64) }, + 'win32-x64': { name: `${root}.zip`, sha256: archiveSha256 } + }, + windowsBuildInputs: { + headersArchive: { name: 'node-v24.18.0-headers.tar.gz', sha256: headersSha256 }, + importLibraries: { + 'win32-arm64': { name: 'win-arm64/node.lib', sha256: '8'.repeat(64) }, + 'win32-x64': { name: 'win-x64/node.lib', sha256: librarySha256 } + } + } + } +} + +async function createZip(entries) { + const directory = await mkdtemp(join(tmpdir(), 'orca-node-zip-test-')) + temporaryDirectories.push(directory) + const archivePath = join(directory, `${root}.zip`) + const zip = new yazl.ZipFile() + for (const entry of entries) { + zip.addBuffer(Buffer.from(entry.bytes), entry.path, { + mtime: new Date('2025-07-17T00:00:00.000Z'), + mode: 0o644 + }) + } + zip.end() + await pipeline(zip.outputStream, createWriteStream(archivePath)) + return { archivePath, directory } +} + +async function validZip(extraEntries = []) { + return createZip([ + { path: `${root}/node.exe`, bytes: 'node executable' }, + { path: `${root}/LICENSE`, bytes: 'Node license' }, + ...extraEntries + ]) +} + +async function windowsBuildInputs(directory) { + const source = join(directory, 'headers-source') + const headersRoot = join(source, 'node-v24.18.0', 'include', 'node') + await mkdir(headersRoot, { recursive: true }) + await Promise.all([ + writeFile(join(headersRoot, 'node.h'), 'Node header'), + writeFile(join(headersRoot, 'common.gypi'), 'common gyp'), + writeFile(join(headersRoot, 'config.gypi'), 'config gyp') + ]) + const headersArchivePath = join(directory, 'node-v24.18.0-headers.tar.gz') + await create({ cwd: source, file: headersArchivePath, gzip: true, portable: true }, [ + 'node-v24.18.0' + ]) + const importLibraryPath = join(directory, 'node.lib') + await writeFile(importLibraryPath, 'node import library') + return { headersArchivePath, importLibraryPath } +} + +describe('SSH relay Node Windows ZIP inspection', () => { + it('rejects Windows-unsafe header paths before extraction', () => { + expect(() => assertPortableSshRelayNodeHeaderPath('node-v24.18.0/include/node/CON')).toThrow( + /unsafe path segment/i + ) + expect(() => + assertPortableSshRelayNodeHeaderPath('node-v24.18.0/include/node/header.h:stream') + ).toThrow(/unsafe path segment/i) + expect(() => + assertPortableSshRelayNodeHeaderPath('node-v24.18.0/include/node/header.h ') + ).toThrow(/unsafe path segment/i) + }) + + it('inspects and selectively extracts only executable, license, and build headers', async () => { + const value = await validZip([{ path: `${root}/npm.cmd`, bytes: 'excluded' }]) + const bytes = await readFile(value.archivePath) + const inputs = await windowsBuildInputs(value.directory) + const contract = release( + sha256(bytes), + sha256(await readFile(inputs.headersArchivePath)), + sha256(await readFile(inputs.importLibraryPath)) + ) + await expect(inspectSshRelayNodeZip(value.archivePath, contract, tuple)).resolves.toMatchObject( + { + root, + files: 3, + hasNodeExecutable: true, + hasLicense: true + } + ) + + const destination = join(value.directory, 'extracted') + const extracted = await extractVerifiedSshRelayNodeZipBuildInputs( + contract, + tuple, + value.archivePath, + destination, + inputs + ) + expect(await readFile(extracted.nodePath, 'utf8')).toBe('node executable') + expect(await readFile(join(extracted.extractedRoot, 'include', 'node', 'node.h'), 'utf8')).toBe( + 'Node header' + ) + expect(await readFile(join(extracted.extractedRoot, 'Release', 'node.lib'), 'utf8')).toBe( + 'node import library' + ) + await expect(readFile(join(extracted.extractedRoot, 'npm.cmd'))).rejects.toThrow() + }) + + it('rejects traversal, case-fold collisions, and bounded-size violations', async () => { + const traversal = await validZip([{ path: `${root}/aa/outside`, bytes: 'bad' }]) + const safeName = Buffer.from(`${root}/aa/outside`) + const unsafeName = Buffer.from(`${root}/../outside`) + const traversalBytes = await readFile(traversal.archivePath) + for (let offset = traversalBytes.indexOf(safeName); offset !== -1; ) { + unsafeName.copy(traversalBytes, offset) + offset = traversalBytes.indexOf(safeName, offset + unsafeName.length) + } + await writeFile(traversal.archivePath, traversalBytes) + const traversalContract = release(sha256(await readFile(traversal.archivePath))) + await expect( + inspectSshRelayNodeZip(traversal.archivePath, traversalContract, tuple) + ).rejects.toThrow(/unsafe path segment|invalid relative path/i) + + const collision = await validZip([{ path: `${root}/license`, bytes: 'duplicate spelling' }]) + const collisionContract = release(sha256(await readFile(collision.archivePath))) + await expect( + inspectSshRelayNodeZip(collision.archivePath, collisionContract, tuple) + ).rejects.toThrow(/case-fold collision/i) + + const oversized = await validZip() + const oversizedContract = release(sha256(await readFile(oversized.archivePath))) + await expect( + inspectSshRelayNodeZip(oversized.archivePath, oversizedContract, tuple, { + maximumExpandedBytes: 8 + }) + ).rejects.toThrow(/expanded-size limit/i) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-archive.mjs b/config/scripts/ssh-relay-runtime-archive.mjs index 526b5768f1f..67a0bd4f9d1 100644 --- a/config/scripts/ssh-relay-runtime-archive.mjs +++ b/config/scripts/ssh-relay-runtime-archive.mjs @@ -8,6 +8,8 @@ import { pipeline } from 'node:stream/promises' import { create, Parser } from 'tar' +import { createSshRelayRuntimeZip, inspectSshRelayRuntimeZip } from './ssh-relay-runtime-zip.mjs' + const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024 const MAX_DIAGNOSTIC_BYTES = 64 * 1024 const XZ_TIMEOUT_MS = 5 * 60 * 1000 @@ -17,7 +19,8 @@ function archiveName(tuple, contentId) { if (!match) { throw new Error('Runtime content identity is not a SHA-256 digest') } - return `orca-ssh-relay-runtime-v1-${tuple}-${match[1]}.tar.xz` + const suffix = tuple.startsWith('win32-') ? 'zip' : 'tar.xz' + return `orca-ssh-relay-runtime-v1-${tuple}-${match[1]}.${suffix}` } async function compressTar(tarPath, archivePath, signal) { @@ -96,6 +99,15 @@ export async function createSshRelayRuntimeArchive({ sourceDateEpoch, signal }) { + if (identity.tupleId.startsWith('win32-')) { + return createSshRelayRuntimeZip({ + runtimeRoot, + outputDirectory, + identity, + sourceDateEpoch, + signal + }) + } if (!Number.isSafeInteger(sourceDateEpoch) || sourceDateEpoch < 0) { throw new Error('Runtime archive SOURCE_DATE_EPOCH must be a non-negative safe integer') } @@ -141,6 +153,9 @@ export async function createSshRelayRuntimeArchive({ } export async function inspectSshRelayRuntimeArchive(archivePath, identity, { signal } = {}) { + if (identity.tupleId.startsWith('win32-')) { + return inspectSshRelayRuntimeZip(archivePath, identity, { signal }) + } const metadata = await stat(archivePath) if (!metadata.isFile() || metadata.size === 0 || metadata.size > MAX_ARCHIVE_BYTES) { throw new Error('Runtime archive exceeds the release-manifest compressed-size limit') diff --git a/config/scripts/ssh-relay-runtime-provenance.mjs b/config/scripts/ssh-relay-runtime-provenance.mjs index 9c017ab02b3..9582dd2256c 100644 --- a/config/scripts/ssh-relay-runtime-provenance.mjs +++ b/config/scripts/ssh-relay-runtime-provenance.mjs @@ -101,6 +101,27 @@ export async function writeSshRelayRuntimeMetadata({ ['node', 'node-pty-native', 'parcel-watcher-native', 'native-runtime'].includes(entry.role) ) .map((entry) => ({ path: entry.path, sha256: entry.sha256 })) + const resolvedDependencies = [ + { + uri: `${nodeRelease.baseUrl}/${nodeRelease.archives[identity.tupleId].name}`, + digest: { sha256: nodeRelease.archives[identity.tupleId].sha256 } + }, + { + uri: nodeRelease.signature.key.sourceUrl, + digest: { sha256: nodeRelease.signature.key.sha256 } + }, + { uri: `git+https://github.com/stablyai/orca@${gitCommit}`, digest: { gitCommit } } + ] + if (identity.tupleId.startsWith('win32-')) { + const headers = nodeRelease.windowsBuildInputs.headersArchive + const library = nodeRelease.windowsBuildInputs.importLibraries[identity.tupleId] + resolvedDependencies.splice( + 1, + 0, + { uri: `${nodeRelease.baseUrl}/${headers.name}`, digest: { sha256: headers.sha256 } }, + { uri: `${nodeRelease.baseUrl}/${library.name}`, digest: { sha256: library.sha256 } } + ) + } const provenance = { _type: 'https://in-toto.io/Statement/v1', subject: [{ name: archive.name, digest: { sha256: archive.sha256.slice('sha256:'.length) } }], @@ -114,17 +135,7 @@ export async function writeSshRelayRuntimeMetadata({ sourceDateEpoch }, internalParameters: { toolchain }, - resolvedDependencies: [ - { - uri: nodeRelease.baseUrl, - digest: { sha256: nodeRelease.archives[identity.tupleId].sha256 } - }, - { - uri: nodeRelease.signature.key.sourceUrl, - digest: { sha256: nodeRelease.signature.key.sha256 } - }, - { uri: `git+https://github.com/stablyai/orca@${gitCommit}`, digest: { gitCommit } } - ] + resolvedDependencies }, runDetails: { builder: { id: builder }, diff --git a/config/scripts/ssh-relay-runtime-smoke-child.cjs b/config/scripts/ssh-relay-runtime-smoke-child.cjs index f4432a6d236..75e20e61415 100644 --- a/config/scripts/ssh-relay-runtime-smoke-child.cjs +++ b/config/scripts/ssh-relay-runtime-smoke-child.cjs @@ -23,19 +23,40 @@ function deadline(label) { } async function ptySmoke() { - if (process.platform === 'win32') { - throw new Error('Windows PTY smoke is not implemented yet') - } - const shell = - process.env.SHELL && process.env.SHELL.startsWith('/') ? process.env.SHELL : '/bin/sh' - const terminal = nodePty.spawn( - shell, - [ - '-c', - 'printf "ORCA_PTY_READY\\n"; IFS= read -r line; stty size; printf "ORCA_PTY_INPUT:%s\\n" "$line"; exit 23' - ], - { name: 'xterm-256color', cols: 80, rows: 24, cwd: runtimeRoot, env: process.env } - ) + const windows = process.platform === 'win32' + const executable = windows + ? 'powershell.exe' + : process.env.SHELL && process.env.SHELL.startsWith('/') + ? process.env.SHELL + : '/bin/sh' + const arguments_ = windows + ? [ + '-NoLogo', + '-NoProfile', + '-Command', + "$ErrorActionPreference='Stop';" + + '[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false);' + + "Write-Output 'ORCA_PTY_READY';" + + '$line=[Console]::ReadLine();' + + '$size=$Host.UI.RawUI.WindowSize;' + + 'Write-Output ("ORCA_PTY_SIZE:{0}x{1}" -f $size.Width,$size.Height);' + + 'Write-Output ("ORCA_PTY_INPUT:{0}" -f $line);' + + 'exit 23' + ] + : [ + '-c', + 'printf "ORCA_PTY_READY\\n"; IFS= read -r line; stty size; printf "ORCA_PTY_INPUT:%s\\n" "$line"; exit 23' + ] + const terminal = nodePty.spawn(executable, arguments_, { + name: 'xterm-256color', + cols: 80, + rows: 24, + cwd: runtimeRoot, + env: process.env, + useConpty: windows, + // Why: the relay uses the bundled ConPTY runtime to preserve terminal rendering behavior. + useConptyDll: windows + }) let output = '' let wroteInput = false terminal.onData((data) => { @@ -67,20 +88,27 @@ async function ptySmoke() { if ( result.exitCode !== 23 || !output.includes('ORCA_PTY_INPUT:bounded-marker') || - !output.includes('37 101') + !output.includes(windows ? 'ORCA_PTY_SIZE:101x37' : '37 101') ) { throw new Error(`PTY smoke mismatch: exit=${result.exitCode} output=${JSON.stringify(output)}`) } const { loadNativeModule } = runtimeRequire('node-pty/lib/utils') - const native = loadNativeModule('pty') - if (!native.dir.replaceAll('\\', '/').includes('build/Release/')) { - throw new Error(`node-pty did not load the patched build/Release artifact: ${native.dir}`) + const nativeNames = windows ? ['conpty', 'conpty_console_list'] : ['pty'] + const nativeDirectories = nativeNames.map((name) => loadNativeModule(name).dir) + if ( + nativeDirectories.some( + (directory) => !directory.replaceAll('\\', '/').includes('build/Release/') + ) + ) { + throw new Error( + `node-pty did not load patched build/Release artifacts: ${nativeDirectories.join(', ')}` + ) } return { exitCode: result.exitCode, resizedRows: 37, resizedColumns: 101, - nativeDirectory: native.dir + nativeDirectory: nativeDirectories.join(', ') } } diff --git a/config/scripts/ssh-relay-runtime-tree.mjs b/config/scripts/ssh-relay-runtime-tree.mjs index 33785f8d933..e524e3ea199 100644 --- a/config/scripts/ssh-relay-runtime-tree.mjs +++ b/config/scripts/ssh-relay-runtime-tree.mjs @@ -36,14 +36,30 @@ const COMPATIBILITY = Object.freeze({ } }, 'darwin-x64': { kind: 'darwin', minimumVersion: '13.5' }, - 'darwin-arm64': { kind: 'darwin', minimumVersion: '13.5' } + 'darwin-arm64': { kind: 'darwin', minimumVersion: '13.5' }, + 'win32-x64': { + kind: 'win32', + minimumBuild: 20348, + minimumOpenSshVersion: '8.1p1', + minimumPowerShellVersion: '5.1', + minimumDotNetFrameworkRelease: 528040 + }, + 'win32-arm64': { + kind: 'win32', + minimumBuild: 20348, + minimumOpenSshVersion: '8.1p1', + minimumPowerShellVersion: '5.1', + minimumDotNetFrameworkRelease: 528040 + } }) const WATCHER_PACKAGES = Object.freeze({ 'linux-x64-glibc': '@parcel/watcher-linux-x64-glibc', 'linux-arm64-glibc': '@parcel/watcher-linux-arm64-glibc', 'darwin-x64': '@parcel/watcher-darwin-x64', - 'darwin-arm64': '@parcel/watcher-darwin-arm64' + 'darwin-arm64': '@parcel/watcher-darwin-arm64', + 'win32-x64': '@parcel/watcher-win32-x64', + 'win32-arm64': '@parcel/watcher-win32-arm64' }) function packageDirectory(name) { @@ -116,11 +132,25 @@ async function copyNodePty(buildDirectory, runtimeRoot, tuple) { await copyNormalized(join(buildDirectory, 'lib', path), join(destination, 'lib', path)) } await writeJson(join(destination, 'package.json'), await minimalPackage('node-pty')) - await copyNormalized( - join(buildDirectory, 'build', 'Release', 'pty.node'), - join(destination, 'build', 'Release', 'pty.node'), - 0o755 - ) + const nativeNames = tuple.startsWith('win32-') + ? ['conpty.node', 'conpty_console_list.node'] + : ['pty.node'] + for (const name of nativeNames) { + await copyNormalized( + join(buildDirectory, 'build', 'Release', name), + join(destination, 'build', 'Release', name), + 0o755 + ) + } + if (tuple.startsWith('win32-')) { + for (const name of ['conpty.dll', 'OpenConsole.exe']) { + await copyNormalized( + join(buildDirectory, 'build', 'Release', 'conpty', name), + join(destination, 'build', 'Release', 'conpty', name), + 0o755 + ) + } + } if (tuple.startsWith('darwin-')) { await copyNormalized( join(buildDirectory, 'build', 'Release', 'spawn-helper'), @@ -192,7 +222,7 @@ async function writeLicenses(runtimeRoot, nodeRoot, packages) { } function runtimeRole(path) { - if (path === 'bin/node') { + if (path === 'bin/node' || path === 'bin/node.exe') { return 'node' } if (path === 'relay.js') { @@ -201,13 +231,21 @@ function runtimeRole(path) { if (path === 'relay-watcher.js') { return 'relay-watcher' } - if (path.endsWith('/pty.node')) { + if ( + path.endsWith('/pty.node') || + path.endsWith('/conpty.node') || + path.endsWith('/conpty_console_list.node') + ) { return 'node-pty-native' } if (path.endsWith('/watcher.node')) { return 'parcel-watcher-native' } - if (path.endsWith('/spawn-helper')) { + if ( + path.endsWith('/spawn-helper') || + path.endsWith('/conpty/conpty.dll') || + path.endsWith('/conpty/OpenConsole.exe') + ) { return 'native-runtime' } if (path === 'THIRD_PARTY_LICENSES.txt') { @@ -263,7 +301,15 @@ async function collectEntries(runtimeRoot) { if (metadata.size > MAX_FILE_BYTES) { throw new Error(`Runtime file exceeds size limit: ${path}`) } - const mode = metadata.mode & 0o777 + const role = runtimeRole(path) + // Why: NTFS has no POSIX execute bits; ZIP metadata carries the canonical runtime mode. + const mode = + process.platform === 'win32' && + ['node', 'node-pty-native', 'parcel-watcher-native', 'native-runtime'].includes(role) + ? 0o755 + : process.platform === 'win32' + ? 0o644 + : metadata.mode & 0o777 if (mode !== 0o644 && mode !== 0o755) { throw new Error(`Runtime file has a non-canonical mode: ${path}`) } @@ -271,7 +317,7 @@ async function collectEntries(runtimeRoot) { entries.push({ path, type: 'file', - role: runtimeRole(path), + role, size: bytes.length, mode, sha256: `sha256:${createHash('sha256').update(bytes).digest('hex')}` @@ -303,7 +349,11 @@ export async function assembleSshRelayRuntimeTree({ throw new Error(`Runtime tree assembly is not implemented for ${tuple}`) } await mkdir(runtimeRoot) - await copyNormalized(join(nodeRoot, 'bin', 'node'), join(runtimeRoot, 'bin', 'node'), 0o755) + const nodeName = tuple.startsWith('win32-') ? 'node.exe' : 'node' + const nodeSource = tuple.startsWith('win32-') + ? join(nodeRoot, nodeName) + : join(nodeRoot, 'bin', nodeName) + await copyNormalized(join(nodeSource), join(runtimeRoot, 'bin', nodeName), 0o755) const version = await relayBuildVersion(relayDirectory) await Promise.all([ copyNormalized(join(relayDirectory, 'relay.js'), join(runtimeRoot, 'relay.js')), @@ -331,7 +381,7 @@ export async function assembleSshRelayRuntimeTree({ }) const tree = await collectEntries(runtimeRoot) - const os = tuple.startsWith('linux-') ? 'linux' : 'darwin' + const os = tuple.startsWith('linux-') ? 'linux' : tuple.startsWith('darwin-') ? 'darwin' : 'win32' const architecture = tuple.includes('arm64') ? 'arm64' : 'x64' const identity = { tupleId: tuple, diff --git a/config/scripts/ssh-relay-runtime-windows-tree.test.mjs b/config/scripts/ssh-relay-runtime-windows-tree.test.mjs new file mode 100644 index 00000000000..0575696ef56 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-windows-tree.test.mjs @@ -0,0 +1,92 @@ +import { createHash } from 'node:crypto' +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { assembleSshRelayRuntimeTree } from './ssh-relay-runtime-tree.mjs' + +const temporaryDirectories = [] +afterEach(async () => { + await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true }))) +}) + +async function writeFixture(path, bytes = path) { + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, bytes) +} + +async function createWindowsNodePtyBuild(root) { + const platformFiles = [ + 'conpty_console_list_agent.js', + 'eventEmitter2.js', + 'index.js', + 'terminal.js', + 'utils.js', + 'windowsConoutConnection.js', + 'windowsPtyAgent.js', + 'windowsTerminal.js', + 'shared/conout.js', + 'worker/conoutSocketWorker.js' + ] + await Promise.all( + platformFiles.map((path) => + writeFixture(join(root, 'lib', ...path.split('/')), `'use strict'\n`) + ) + ) + await Promise.all( + ['conpty.node', 'conpty_console_list.node', 'pty.node'].map((name) => + writeFixture(join(root, 'build', 'Release', name), name) + ) + ) + await Promise.all( + ['conpty.dll', 'OpenConsole.exe'].map((name) => + writeFixture(join(root, 'build', 'Release', 'conpty', name), name) + ) + ) +} + +describe('SSH relay Windows runtime tree', () => { + it('carries the production ConPTY runtime closure in the hashed identity', async () => { + const directory = await mkdtemp(join(tmpdir(), 'orca-runtime-windows-tree-')) + temporaryDirectories.push(directory) + const nodeRoot = join(directory, 'node') + const nodePtyBuildDirectory = join(directory, 'node-pty') + const relayDirectory = join(directory, 'relay') + const runtimeRoot = join(directory, 'runtime') + const relayBytes = 'relay bytes' + const watcherBytes = 'watcher bytes' + const relayHash = createHash('sha256') + .update(relayBytes) + .update(watcherBytes) + .digest('hex') + .slice(0, 12) + await Promise.all([ + writeFixture(join(nodeRoot, 'node.exe'), 'node executable'), + writeFixture(join(nodeRoot, 'LICENSE'), 'Node license'), + writeFixture(join(relayDirectory, 'relay.js'), relayBytes), + writeFixture(join(relayDirectory, 'relay-watcher.js'), watcherBytes), + writeFixture(join(relayDirectory, '.version'), `fixture+${relayHash}\n`), + createWindowsNodePtyBuild(nodePtyBuildDirectory) + ]) + + const identity = await assembleSshRelayRuntimeTree({ + tuple: 'win32-x64', + nodeRoot, + nodePtyBuildDirectory, + relayDirectory, + runtimeRoot, + nodeVersion: '24.18.0' + }) + + for (const name of ['conpty.dll', 'OpenConsole.exe']) { + const path = `node_modules/node-pty/build/Release/conpty/${name}` + expect(identity.entries).toContainEqual( + expect.objectContaining({ path, type: 'file', role: 'native-runtime', mode: 0o755 }) + ) + expect(await readFile(join(runtimeRoot, ...path.split('/')), 'utf8')).toBe(name) + } + expect(identity.entries.some((entry) => entry.path.endsWith('/pty.node'))).toBe(false) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 8200dfe73c8..4ef61105223 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -11,20 +11,27 @@ const workflowUrl = new URL( describe('SSH relay runtime artifact workflow', () => { it('uses exact native runner labels and SHA-pinned actions without publication authority', async () => { const workflow = parse(await readFile(workflowUrl, 'utf8')) - const job = workflow.jobs['build-posix-runtime'] - const matrix = job.strategy.matrix.include + const posixJob = workflow.jobs['build-posix-runtime'] + const windowsJob = workflow.jobs['build-windows-runtime'] - expect(matrix.map((entry) => [entry.runner, entry.tuple])).toEqual([ + expect(posixJob.strategy.matrix.include.map((entry) => [entry.runner, entry.tuple])).toEqual([ ['ubuntu-24.04', 'linux-x64-glibc'], ['ubuntu-24.04-arm', 'linux-arm64-glibc'], ['macos-15-intel', 'darwin-x64'], ['macos-15', 'darwin-arm64'] ]) + expect(windowsJob.strategy.matrix.include.map((entry) => [entry.runner, entry.tuple])).toEqual([ + ['windows-2022', 'win32-x64'], + ['windows-11-arm', 'win32-arm64'] + ]) expect(workflow.permissions).toEqual({ contents: 'read' }) - expect(job['timeout-minutes']).toBe(20) - expect(job.steps[0].with.ref).toBe('${{ github.event.pull_request.head.sha || github.sha }}') - for (const step of job.steps.filter((candidate) => candidate.uses)) { - expect(step.uses).toMatch(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+@[0-9a-f]{40}$/) + expect(posixJob['timeout-minutes']).toBe(20) + expect(windowsJob['timeout-minutes']).toBe(30) + for (const job of [posixJob, windowsJob]) { + expect(job.steps[0].with.ref).toBe('${{ github.event.pull_request.head.sha || github.sha }}') + for (const step of job.steps.filter((candidate) => candidate.uses)) { + expect(step.uses).toMatch(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+@[0-9a-f]{40}$/) + } } }) @@ -50,6 +57,12 @@ describe('SSH relay runtime artifact workflow', () => { expect(source).toContain('--git-commit "$source_commit"') expect(source).not.toContain('--git-commit "$GITHUB_SHA"') expect(source).toContain('cp "$output"/*.tar.xz') + expect(source).toContain("Get-ChildItem -LiteralPath $output -Filter '*.zip'") + expect(source).toContain('ssh-relay-node-zip-inspection.test.mjs') + expect(source).toContain('ssh-relay-runtime-zip.test.mjs') + expect(source).toContain('node-v24.18.0-headers.tar.gz') + expect(source).toContain('node_library: win-x64/node.lib') + expect(source).toContain("@('gpg.exe', 'gpgv.exe')") expect(steps[uploadIndex].with.path).toBe('runtime-evidence/${{ matrix.tuple }}/') expect(source).not.toMatch(/releases\/|gh release|contents:\s*write/i) }) diff --git a/config/scripts/ssh-relay-runtime-zip.mjs b/config/scripts/ssh-relay-runtime-zip.mjs new file mode 100644 index 00000000000..cb23bdeda18 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-zip.mjs @@ -0,0 +1,138 @@ +import { createHash } from 'node:crypto' +import { createReadStream, createWriteStream } from 'node:fs' +import { rm, stat } from 'node:fs/promises' +import { join } from 'node:path' +import { pipeline } from 'node:stream/promises' + +import yazl from 'yazl' + +import { visitSshRelayZip } from './ssh-relay-zip-reader.mjs' + +const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024 +const ZIP_LIMITS = Object.freeze({ + maximumArchiveBytes: MAX_ARCHIVE_BYTES, + maximumEntries: 5_000, + maximumExpandedBytes: 350 * 1024 * 1024, + maximumFileBytes: 250 * 1024 * 1024, + maximumDepth: 32, + maximumPathBytes: 240 +}) + +function archiveName(tuple, contentId) { + const match = /^sha256:([0-9a-f]{64})$/.exec(contentId) + if (!match) { + throw new Error('Runtime content identity is not a SHA-256 digest') + } + if (!tuple.startsWith('win32-')) { + throw new Error(`Runtime ZIP is only valid for a Windows tuple: ${tuple}`) + } + return `orca-ssh-relay-runtime-v1-${tuple}-${match[1]}.zip` +} + +function zipTimestamp(sourceDateEpoch) { + if (!Number.isSafeInteger(sourceDateEpoch) || sourceDateEpoch < 315_532_800) { + throw new Error('Runtime ZIP SOURCE_DATE_EPOCH must fit the ZIP timestamp range') + } + const value = new Date(sourceDateEpoch * 1000) + if (Number.isNaN(value.getTime()) || value.getUTCFullYear() > 2107) { + throw new Error('Runtime ZIP SOURCE_DATE_EPOCH must fit the ZIP timestamp range') + } + return value +} + +async function sha256File(path) { + const digest = createHash('sha256') + for await (const chunk of createReadStream(path)) { + digest.update(chunk) + } + return `sha256:${digest.digest('hex')}` +} + +export async function createSshRelayRuntimeZip({ + runtimeRoot, + outputDirectory, + identity, + sourceDateEpoch, + signal +}) { + const name = archiveName(identity.tupleId, identity.contentId) + const archivePath = join(outputDirectory, name) + const mtime = zipTimestamp(sourceDateEpoch) + const zip = new yazl.ZipFile() + const output = createWriteStream(archivePath, { flags: 'wx', mode: 0o600 }) + try { + for (const entry of [...identity.entries].sort((left, right) => + left.path < right.path ? -1 : left.path > right.path ? 1 : 0 + )) { + signal?.throwIfAborted() + const options = { mtime, mode: entry.mode, forceDosTimestamp: true } + if (entry.type === 'directory') { + zip.addEmptyDirectory(entry.path, options) + } else { + zip.addFile(join(runtimeRoot, ...entry.path.split('/')), entry.path, { + ...options, + compress: true, + compressionLevel: 9 + }) + } + } + zip.end({ forceZip64Format: false }) + await pipeline(zip.outputStream, output, { signal }) + const metadata = await stat(archivePath) + if (!metadata.isFile() || metadata.size === 0 || metadata.size > MAX_ARCHIVE_BYTES) { + throw new Error('Runtime ZIP exceeds the release-manifest compressed-size limit') + } + return { + name, + path: archivePath, + size: metadata.size, + sha256: await sha256File(archivePath) + } + } catch (error) { + zip.outputStream.destroy() + output.destroy() + await rm(archivePath, { force: true }) + throw error + } +} + +export async function inspectSshRelayRuntimeZip(archivePath, identity, { signal } = {}) { + const expected = new Map(identity.entries.map((entry) => [entry.path, entry])) + const seen = new Set() + const result = await visitSshRelayZip( + archivePath, + ZIP_LIMITS, + async (actual, consume) => { + const declared = expected.get(actual.path) + if (!declared || seen.has(actual.path)) { + throw new Error(`Runtime ZIP has extra or duplicate entry: ${actual.path}`) + } + seen.add(actual.path) + if ( + actual.type !== declared.type || + actual.unixMode === null || + (actual.unixMode & 0o777) !== declared.mode + ) { + throw new Error(`Runtime ZIP type or mode mismatch: ${actual.path}`) + } + if (declared.type === 'file') { + if (actual.size !== declared.size) { + throw new Error(`Runtime ZIP size mismatch: ${actual.path}`) + } + const verified = await consume() + if (verified.sha256 !== declared.sha256) { + throw new Error(`Runtime ZIP file integrity mismatch: ${actual.path}`) + } + } + }, + { signal } + ) + const missing = [...expected.keys()].find((path) => !seen.has(path)) + if (missing) { + throw new Error(`Runtime ZIP is missing declared entry: ${missing}`) + } + if (result.files !== identity.fileCount || result.expandedBytes !== identity.expandedSize) { + throw new Error('Runtime ZIP aggregate size or file-count mismatch') + } + return result +} diff --git a/config/scripts/ssh-relay-runtime-zip.test.mjs b/config/scripts/ssh-relay-runtime-zip.test.mjs new file mode 100644 index 00000000000..5aa3669bc49 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-zip.test.mjs @@ -0,0 +1,134 @@ +import { createHash } from 'node:crypto' +import { createWriteStream } from 'node:fs' +import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pipeline } from 'node:stream/promises' + +import { afterEach, describe, expect, it } from 'vitest' +import yazl from 'yazl' + +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' +import { createSshRelayRuntimeZip, inspectSshRelayRuntimeZip } from './ssh-relay-runtime-zip.mjs' + +const temporaryDirectories = [] +afterEach(async () => { + await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true }))) +}) + +const digest = (value) => `sha256:${createHash('sha256').update(value).digest('hex')}` + +async function fixture() { + const directory = await mkdtemp(join(tmpdir(), 'orca-runtime-zip-test-')) + temporaryDirectories.push(directory) + const runtimeRoot = join(directory, 'runtime') + await mkdir(join(runtimeRoot, 'bin'), { recursive: true }) + await Promise.all([ + writeFile(join(runtimeRoot, 'bin', 'node.exe'), 'node executable'), + writeFile(join(runtimeRoot, 'relay.js'), 'relay') + ]) + await chmod(join(runtimeRoot, 'bin', 'node.exe'), 0o755) + const base = { + tupleId: 'win32-x64', + os: 'win32', + architecture: 'x64', + compatibility: { + kind: 'win32', + minimumBuild: 20348, + minimumOpenSshVersion: '8.1p1', + minimumPowerShellVersion: '5.1', + minimumDotNetFrameworkRelease: 528040 + }, + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries: [ + { path: 'bin', type: 'directory', mode: 0o755 }, + { + path: 'bin/node.exe', + type: 'file', + role: 'node', + size: 15, + mode: 0o755, + sha256: digest('node executable') + }, + { + path: 'relay.js', + type: 'file', + role: 'relay', + size: 5, + mode: 0o644, + sha256: digest('relay') + } + ], + fileCount: 2, + expandedSize: 20 + } + return { + directory, + runtimeRoot, + identity: { ...base, contentId: computeSshRelayRuntimeContentId(base) } + } +} + +async function writeZip(path, entries) { + const zip = new yazl.ZipFile() + for (const entry of entries) { + zip.addBuffer(Buffer.from(entry.bytes ?? ''), entry.path, { + mtime: new Date('2025-07-17T00:00:00.000Z'), + mode: entry.mode ?? 0o644 + }) + } + zip.end() + await pipeline(zip.outputStream, createWriteStream(path)) +} + +describe('SSH relay runtime Windows ZIP', () => { + it('creates deterministic ZIP bytes and rehashes the complete declared tree', async () => { + const value = await fixture() + const firstDirectory = join(value.directory, 'first') + const secondDirectory = join(value.directory, 'second') + await Promise.all([mkdir(firstDirectory), mkdir(secondDirectory)]) + const first = await createSshRelayRuntimeZip({ + runtimeRoot: value.runtimeRoot, + outputDirectory: firstDirectory, + identity: value.identity, + sourceDateEpoch: 1_752_710_400 + }) + const second = await createSshRelayRuntimeZip({ + runtimeRoot: value.runtimeRoot, + outputDirectory: secondDirectory, + identity: value.identity, + sourceDateEpoch: 1_752_710_400 + }) + + expect(first.name).toMatch(/\.zip$/) + expect(await readFile(first.path)).toEqual(await readFile(second.path)) + await expect(inspectSshRelayRuntimeZip(first.path, value.identity)).resolves.toEqual({ + entries: 3, + files: 2, + expandedBytes: 20 + }) + }) + + it('rejects undeclared entries and declared-file integrity mismatches', async () => { + const value = await fixture() + const extra = join(value.directory, 'extra.zip') + const tampered = join(value.directory, 'tampered.zip') + await writeZip(extra, [ + { path: 'bin/node.exe', bytes: 'node executable', mode: 0o755 }, + { path: 'relay.js', bytes: 'relay' }, + { path: 'extra.js', bytes: 'extra' } + ]) + await writeZip(tampered, [ + { path: 'bin/node.exe', bytes: 'node executable', mode: 0o755 }, + { path: 'relay.js', bytes: 'wrong' } + ]) + + await expect(inspectSshRelayRuntimeZip(extra, value.identity)).rejects.toThrow( + /extra or duplicate/i + ) + await expect(inspectSshRelayRuntimeZip(tampered, value.identity)).rejects.toThrow( + /integrity mismatch/i + ) + }) +}) diff --git a/config/scripts/ssh-relay-zip-reader.mjs b/config/scripts/ssh-relay-zip-reader.mjs new file mode 100644 index 00000000000..7a406a958d9 --- /dev/null +++ b/config/scripts/ssh-relay-zip-reader.mjs @@ -0,0 +1,217 @@ +import { createHash } from 'node:crypto' +import { createWriteStream } from 'node:fs' +import { mkdir, stat } from 'node:fs/promises' +import { dirname } from 'node:path' +import { Transform, Writable } from 'node:stream' +import { pipeline } from 'node:stream/promises' +import { crc32 } from 'node:zlib' + +import yauzl from 'yauzl' + +const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true }) +const ENCRYPTION_FLAGS = 0x0001 | 0x0040 | 0x2000 +const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i + +function openZip(path) { + return new Promise((resolve, reject) => { + yauzl.open( + path, + { + autoClose: false, + lazyEntries: true, + decodeStrings: false, + strictFileNames: true, + validateEntrySizes: true + }, + (error, zip) => (error ? reject(error) : resolve(zip)) + ) + }) +} + +function nextEntry(zip) { + return new Promise((resolve, reject) => { + const cleanup = () => { + zip.off('entry', onEntry) + zip.off('end', onEnd) + zip.off('error', onError) + } + const onEntry = (entry) => { + cleanup() + resolve(entry) + } + const onEnd = () => { + cleanup() + resolve(null) + } + const onError = (error) => { + cleanup() + reject(error) + } + zip.once('entry', onEntry) + zip.once('end', onEnd) + zip.once('error', onError) + zip.readEntry() + }) +} + +function openEntryStream(zip, entry) { + return new Promise((resolve, reject) => { + zip.openReadStream(entry, (error, stream) => (error ? reject(error) : resolve(stream))) + }) +} + +function decodeEntryName(entry) { + if (!Buffer.isBuffer(entry.fileName)) { + throw new Error('ZIP reader did not return raw entry-name bytes') + } + try { + return UTF8_DECODER.decode(entry.fileName) + } catch { + throw new Error('ZIP entry name must be valid UTF-8') + } +} + +function portableEntryPath(name, limits) { + if ( + name.length === 0 || + Buffer.byteLength(name) > limits.maximumPathBytes || + name.startsWith('/') || + name.startsWith('\\') || + /^[A-Za-z]:/.test(name) + ) { + throw new Error('ZIP entry path is not a bounded portable relative path') + } + for (const character of name) { + const code = character.charCodeAt(0) + if (code <= 0x1f || code === 0x7f || character === '\\') { + throw new Error('ZIP entry path contains a control character or backslash') + } + } + const path = name.endsWith('/') ? name.slice(0, -1) : name + const segments = path.split('/') + if ( + segments.length > limits.maximumDepth || + segments.some( + (segment) => + segment.length === 0 || + segment === '.' || + segment === '..' || + segment.includes(':') || + segment.endsWith('.') || + segment.endsWith(' ') || + WINDOWS_DEVICE_NAME.test(segment) + ) + ) { + throw new Error('ZIP entry path contains an unsafe path segment') + } + return path +} + +function entryInfo(entry, path) { + const hostSystem = entry.versionMadeBy >>> 8 + const unixMode = hostSystem === 3 ? (entry.externalFileAttributes >>> 16) & 0xffff : null + if (unixMode !== null && (unixMode & 0o170000) === 0o120000) { + throw new Error(`ZIP archive contains a prohibited symbolic link: ${path}`) + } + if ((entry.generalPurposeBitFlag & ENCRYPTION_FLAGS) !== 0) { + throw new Error(`ZIP archive contains an encrypted entry: ${path}`) + } + if (entry.compressionMethod !== 0 && entry.compressionMethod !== 8) { + throw new Error(`ZIP archive uses an unsupported compression method: ${path}`) + } + const directory = entry.fileName.at(-1) === 0x2f + if (directory && (entry.uncompressedSize !== 0 || entry.compressedSize !== 0)) { + throw new Error(`ZIP directory entry contains file data: ${path}`) + } + return { + path, + type: directory ? 'directory' : 'file', + compressedSize: entry.compressedSize, + size: entry.uncompressedSize, + crc32: entry.crc32 >>> 0, + hostSystem, + unixMode, + compressionMethod: entry.compressionMethod + } +} + +async function consumeEntry(zip, entry, info, { outputPath, mode, signal } = {}) { + const input = await openEntryStream(zip, entry) + let bytes = 0 + let checksum = 0 + const digest = createHash('sha256') + const verifier = new Transform({ + transform(chunk, _encoding, callback) { + bytes += chunk.length + if (bytes > info.size) { + callback(new Error(`ZIP entry expanded beyond its declared size: ${info.path}`)) + return + } + checksum = crc32(chunk, checksum) + digest.update(chunk) + callback(null, chunk) + } + }) + const sink = outputPath + ? (await mkdir(dirname(outputPath), { recursive: true }), + createWriteStream(outputPath, { flags: 'wx', mode: mode ?? 0o600 })) + : new Writable({ write: (_chunk, _encoding, callback) => callback() }) + await pipeline(input, verifier, sink, { signal }) + if (bytes !== info.size || checksum >>> 0 !== info.crc32) { + throw new Error(`ZIP entry size or CRC-32 mismatch: ${info.path}`) + } + return { bytes, sha256: `sha256:${digest.digest('hex')}` } +} + +export async function visitSshRelayZip(archivePath, limits, visitor, { signal } = {}) { + const metadata = await stat(archivePath) + if (!metadata.isFile() || metadata.size === 0 || metadata.size > limits.maximumArchiveBytes) { + throw new Error('ZIP archive exceeds its compressed-size limit') + } + const zip = await openZip(archivePath) + const foldedPaths = new Set() + let entries = 0 + let files = 0 + let expandedBytes = 0 + try { + for (let entry = await nextEntry(zip); entry; entry = await nextEntry(zip)) { + signal?.throwIfAborted() + const path = portableEntryPath(decodeEntryName(entry), limits) + const folded = path.toLowerCase() + if (foldedPaths.has(folded)) { + throw new Error(`ZIP archive contains a duplicate or case-fold collision: ${path}`) + } + foldedPaths.add(folded) + const info = entryInfo(entry, path) + entries += 1 + if (entries > limits.maximumEntries) { + throw new Error('ZIP archive exceeds the entry-count limit') + } + if (info.type === 'file') { + files += 1 + if (!Number.isSafeInteger(info.size) || info.size > limits.maximumFileBytes) { + throw new Error(`ZIP archive file exceeds the per-file size limit: ${path}`) + } + expandedBytes += info.size + if (!Number.isSafeInteger(expandedBytes) || expandedBytes > limits.maximumExpandedBytes) { + throw new Error('ZIP archive exceeds the expanded-size limit') + } + } + let consumed = info.type === 'directory' + const consume = async (options) => { + if (consumed) { + throw new Error(`ZIP entry was consumed more than once: ${path}`) + } + consumed = true + return consumeEntry(zip, entry, info, { ...options, signal: options?.signal ?? signal }) + } + await visitor(info, consume) + if (!consumed) { + throw new Error(`ZIP visitor did not verify file bytes: ${path}`) + } + } + return { entries, files, expandedBytes } + } finally { + zip.close() + } +} diff --git a/config/scripts/verify-ssh-relay-node-release-inputs.mjs b/config/scripts/verify-ssh-relay-node-release-inputs.mjs index 120ab51c8fe..2ee689aae8e 100644 --- a/config/scripts/verify-ssh-relay-node-release-inputs.mjs +++ b/config/scripts/verify-ssh-relay-node-release-inputs.mjs @@ -10,7 +10,8 @@ import { validateSshRelayNodeReleaseContract, verifySshRelayNodeArchive, verifySshRelayNodeChecksumDocument, - verifySshRelayNodeSignature + verifySshRelayNodeSignature, + verifySshRelayNodeWindowsBuildInput } from './ssh-relay-node-release-verification.mjs' const scriptDirectory = import.meta.dirname @@ -104,11 +105,34 @@ export async function verifyNodeReleaseInputs( ) const checksums = verifySshRelayNodeChecksumDocument(release, checksumBytes) const archives = [] + const windowsBuildInputs = [] for (const tuple of archiveTuples) { const archive = release.archives[tuple] archives.push( await verifySshRelayNodeArchive(release, tuple, join(inputsDirectory, archive.name)) ) + if (tuple.startsWith('win32-')) { + const headers = release.windowsBuildInputs.headersArchive + const library = release.windowsBuildInputs.importLibraries[tuple] + if (!windowsBuildInputs.some((input) => input.kind === 'headers')) { + windowsBuildInputs.push( + await verifySshRelayNodeWindowsBuildInput( + release, + 'headers', + tuple, + join(inputsDirectory, headers.name) + ) + ) + } + windowsBuildInputs.push( + await verifySshRelayNodeWindowsBuildInput( + release, + 'import-library', + tuple, + join(inputsDirectory, ...library.name.split('/')) + ) + ) + } } return { @@ -118,7 +142,8 @@ export async function verifyNodeReleaseInputs( signerFingerprint: signature.signerFingerprint, checksumSha256: signature.checksumSha256, checksumEntriesVerified: checksums.length, - archives + archives, + windowsBuildInputs } } diff --git a/config/scripts/verify-ssh-relay-runtime.mjs b/config/scripts/verify-ssh-relay-runtime.mjs index 3b0cdc99f23..e1ee06d076c 100644 --- a/config/scripts/verify-ssh-relay-runtime.mjs +++ b/config/scripts/verify-ssh-relay-runtime.mjs @@ -52,7 +52,7 @@ async function actualTree(runtimeDirectory) { results.set(path.toLowerCase(), { path, type: 'directory', - mode: metadata.mode & 0o777 + mode: process.platform === 'win32' ? null : metadata.mode & 0o777 }) await visit(absolutePath) } else if (child.isFile()) { @@ -61,7 +61,7 @@ async function actualTree(runtimeDirectory) { path, type: 'file', size: bytes.length, - mode: metadata.mode & 0o777, + mode: process.platform === 'win32' ? null : metadata.mode & 0o777, sha256: `sha256:${createHash('sha256').update(bytes).digest('hex')}` }) } else { @@ -81,7 +81,8 @@ export async function verifyRuntimeTree(runtimeDirectory, identity) { !entry || entry.path !== expected.path || entry.type !== expected.type || - entry.mode !== expected.mode + // Why: NTFS has no execute bit; the verified ZIP carries canonical mode metadata instead. + (process.platform !== 'win32' && entry.mode !== expected.mode) ) { throw new Error(`Runtime tree entry mismatch: ${expected.path}`) } @@ -109,7 +110,7 @@ export async function verifyRuntimeTree(runtimeDirectory, identity) { } async function runBundledSmoke(runtimeDirectory, identity) { - const nodePath = join(runtimeDirectory, 'bin', 'node') + const nodePath = join(runtimeDirectory, 'bin', identity.os === 'win32' ? 'node.exe' : 'node') const childPath = resolve(scriptDirectory, 'ssh-relay-runtime-smoke-child.cjs') const result = await execFileAsync(nodePath, [childPath, runtimeDirectory], { cwd: runtimeDirectory, diff --git a/config/ssh-relay-node-release-v24.18.0.json b/config/ssh-relay-node-release-v24.18.0.json index 392205060b9..af985af30da 100644 --- a/config/ssh-relay-node-release-v24.18.0.json +++ b/config/ssh-relay-node-release-v24.18.0.json @@ -45,5 +45,21 @@ "name": "node-v24.18.0-win-x64.zip", "sha256": "0ae68406b42d7725661da979b1403ec9926da205c6770827f33aac9d8f26e821" } + }, + "windowsBuildInputs": { + "headersArchive": { + "name": "node-v24.18.0-headers.tar.gz", + "sha256": "6c7d41d83c3481d2301115b8ce4a44b7d4fbfa52859b1aac14f445d460137887" + }, + "importLibraries": { + "win32-arm64": { + "name": "win-arm64/node.lib", + "sha256": "7da03c5111815b69bbe63ffd2e51b28cd69eec9f545b7ccb8756efffdbb88dc2" + }, + "win32-x64": { + "name": "win-x64/node.lib", + "sha256": "589684168a73547ca47cd22d76a4e465ef561abe89fb1b2b23fe35bbe857d505" + } + } } } 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 e913d49f7f4..c4bf9ff99f4 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 @@ -58,9 +58,10 @@ same change as the work it records. [#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 - smoke, SBOM, and provenance only at exact implementation head `151628992` in stacked draft PR - [#8741](https://github.com/stablyai/orca/pull/8741). It may produce test artifacts but must not - publish, resolve, transfer, install, launch, or enable them. + smoke, SBOM, and provenance only. The Windows x64/arm64 artifact candidate is uncommitted on top + of `c308107a9` in stacked draft PR [#8741](https://github.com/stablyai/orca/pull/8741) while its + final diff and local gates are re-audited before native CI. It 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, 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 @@ -71,6 +72,15 @@ same change as the work it records. archive/tree verification, bundled Node, patched PTY, and watcher smoke for Linux x64/arm64 and macOS x64/arm64 under E-M3-CI-001. Same-runner clean-rebuild identity, oldest-baseline, native-trust, SSH, Windows, and musl cells remain open. +- Windows input correction: E-M3-WINDOWS-INPUT-GAP-001 proved the official Windows ZIP lacks headers + and `node.lib`. Both artifacts now require the exact signed headers archive and tuple import + library as explicit inputs. The schema, signed-checksum verifier, bounded ZIP/header extraction, + and import-library staging are locally green under E-M3-WINDOWS-INPUT-001; native Windows build + proof remains active and no implicit `node-gyp` download is allowed. +- Windows PTY-closure correction: E-M3-WINDOWS-CONPTY-GAP-001 proved the candidate omitted the + `conpty.dll` and `OpenConsole.exe` files required by Orca's production `useConptyDll: true` path + and therefore exercised a weaker system-ConPTY smoke. The artifact and smoke correction is active; + no Windows executable cell is complete. - Production behavior: unchanged; Orca embeds relay JavaScript and installs `node-pty` plus `@parcel/watcher` with remote npm. - New runtime assets published: none. @@ -83,11 +93,12 @@ same change as the work it records. - Rollout control: existing per-SSH-target configuration; legacy is the default and the bundled runtime is an explicit per-target Beta opt-in under E-M1-ROLLOUT-DECISION-001. - Legacy fallback removal: not authorized. -- Next required action: implement the bounded deterministic Windows runtime/ZIP builder and verifier - as the next artifact-only slice, add native Windows x64/arm64 runner jobs, and separately add a - same-head/same-runner clean-rebuild identity oracle for native build reproducibility. Keep - cross-family remote infrastructure, signing/trust, and measured legacy baseline gates open; do not - introduce publication, resolver, transfer, rollout, tuple enablement, or default behavior. +- Next required action: finish the final local audit of the bounded Windows runtime/ZIP builder and + verifier, commit the artifact-only implementation, and collect exact Windows x64/arm64 plus POSIX + regression evidence from draft-PR CI. A same-head/same-runner clean-rebuild identity oracle for + native build reproducibility remains a separate open gate. Keep cross-family remote + infrastructure, signing/trust, and measured legacy baseline gates open; do not introduce + publication, resolver, transfer, rollout, tuple enablement, or default behavior. ## Non-Negotiable Invariants @@ -350,6 +361,10 @@ cancellation; cleanup settlement is part of the timeout oracle. provenance, and preserve official executable bytes/signatures. Orca-built native executables and libraries still require the target-native signing policy decided below. (E-M1-NODE-PROVENANCE-001) +- [x] For Windows native-module builds, authenticate the exact-version `headers.tar.gz` and + architecture-specific `win-*/node.lib` through that same signed checksum document in addition + to the tuple ZIP. The official Windows ZIP does not contain either build input; implicit + `node-gyp` downloads are prohibited. (E-M3-WINDOWS-INPUT-GAP-001) ### Trust and signing @@ -558,9 +573,17 @@ Each runtime must contain only the executable closure required by the relay. (E-M3-RUNTIME-LOCAL-001) - [x] Pin Node and verify downloaded source/binary checksums and upstream signatures. (E-M3-NODE-PROVENANCE-001; real archive execution remains a separate per-tuple gate) +- [x] Extend the immutable Node input contract for Windows to include the signed exact-version + headers archive and tuple-specific `node.lib`; verify/copy them into an exclusive local build + root and configure `node-gyp` to fail rather than fetch an unstaged input. + (E-M3-WINDOWS-INPUT-001; successful target-native offline build remains a per-tuple gate) - [ ] Build Orca’s patched `node-pty@1.1.0` against the exact bundled Node runtime. - [ ] Assert Orca-required patched exports/diagnostics exist; do not silently use an upstream prebuild that omits the patch. +- [ ] For Windows, copy the tuple-architecture `conpty.dll` and `OpenConsole.exe` from the pinned + node-pty source into `build/Release/conpty`, hash them into the runtime identity as native + runtime files, and prove PTY spawn/resize/exit with `useConptyDll: true`. Do not substitute a + system-ConPTY smoke for the production path. (E-M3-WINDOWS-CONPTY-GAP-001) - [ ] Include exactly one compatible `@parcel/watcher@2.5.6` native optional package. - [ ] Include relay JavaScript, watcher child, required runtime JavaScript closure, licenses, SBOM, provenance, and runtime metadata. @@ -1192,6 +1215,8 @@ focused commands as their scripts/tests are introduced. - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-node-release-verification.test.mjs` (E-M3-NODE-RED-001, E-M3-NODE-PROVENANCE-001) - [x] `node config/scripts/verify-ssh-relay-node-release-inputs.mjs --inputs-directory --archive linux-x64-glibc` (E-M3-NODE-PROVENANCE-001; metadata and archive verification only) +- [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-node-zip-inspection.test.mjs config/scripts/ssh-relay-runtime-zip.test.mjs` (E-M3-WINDOWS-ZIP-RED-001, E-M3-WINDOWS-INPUT-001; synthetic ZIP/input contracts only) +- [x] `node config/scripts/verify-ssh-relay-node-release-inputs.mjs --inputs-directory --archive win32-x64` (E-M3-WINDOWS-INPUT-001; signed real Node ZIP, headers, and import library; no Windows execution) - [x] `node config/scripts/build-ssh-relay-runtime.mjs --tuple linux-arm64-glibc --inputs-directory --output-directory --source-date-epoch --git-commit ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) - [x] `node config/scripts/verify-ssh-relay-runtime.mjs --runtime-directory --identity --archive ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) @@ -2273,6 +2298,174 @@ fragmentLinks=9`. - Follow-up: assemble the first runtime on a target-native runner, inspect its archive, execute its bundled Node and native dependencies, then fill only the evidence cells actually proved. +### E-M3-WINDOWS-CONPTY-GAP-001 — Candidate omitted the production ConPTY runtime closure + +- Date: 2026-07-14 +- Commit SHA / PR: source audit of the uncommitted Windows artifact-only candidate on top of + `c308107a9`; draft PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: macOS 26.2 arm64; source and installed-package inspection only +- Remote and transport: none +- Exact commands: + + ```sh + rg -n "useConpty|nodePty\\.spawn|pty\\.spawn" src/main src/shared + sed -n '470,515p' src/main/daemon/pty-subprocess.ts + sed -n '1,130p' node_modules/node-pty/scripts/post-install.js + find node_modules/node-pty/third_party/conpty -maxdepth 3 -type f -print | sort + rg -n "conpty\\.dll|OpenConsole|useConptyDll" \ + config/scripts/ssh-relay-runtime-tree.mjs \ + config/scripts/ssh-relay-runtime-smoke-child.cjs + ``` + +- Result: discriminating discovery. Orca's daemon PTY spawn passes `useConptyDll: true` on Windows. + Pinned `node-pty@1.1.0` supplies architecture-specific `conpty.dll` and `OpenConsole.exe` under + `third_party/conpty/1.23.251008001/win10-{x64,arm64}` and its postinstall copies those files to + `build/Release/conpty`. The candidate directly invoked `node-gyp`, copied only `conpty.node` and + `conpty_console_list.node`, and smoked with `useConpty: true` but not `useConptyDll: true`. +- Oracle proved: the candidate artifact could pass its proposed smoke while omitting files required + by the production relay PTY configuration. Both plan artifacts now require the exact ConPTY + runtime closure and production-mode smoke before a Windows executable cell can pass. +- Does not prove: corrected staging, artifact-tree identity, ConPTY execution, Authenticode/native + trust, Windows build, SSH, transfer, install, or any enabled tuple. +- Checklist items satisfied: none; this is a red gap record and its implementation item remains open. +- Follow-up: stage the tuple-specific files without running an implicit downloader, copy and hash + them into the runtime, use `useConptyDll: true` in native smoke, and obtain Windows x64/arm64 CI + evidence before checking the implementation item. + +### E-M3-WINDOWS-INPUT-GAP-001 — Windows ZIP lacks native build inputs + +- Date: 2026-07-14 +- Commit SHA / PR: investigation while implementing the Windows artifact-only slice in draft PR + [#8741](https://github.com/stablyai/orca/pull/8741); no Windows artifact was produced or enabled +- Runner: macOS 26.2 arm64; `curl` plus system `unzip`; inspection only +- Remote and transport: exact HTTPS download from + `https://nodejs.org/dist/v24.18.0/node-v24.18.0-win-x64.zip`; no SSH or release upload +- Exact commands: + + ```sh + curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \ + --connect-timeout 20 --max-time 300 --retry 2 --retry-delay 2 --retry-all-errors \ + https://nodejs.org/dist/v24.18.0/node-v24.18.0-win-x64.zip \ + --output /tmp/orca-node-win-input-check/node-v24.18.0-win-x64.zip + unzip -l /tmp/orca-node-win-input-check/node-v24.18.0-win-x64.zip | \ + rg 'node\\.exe$|include/node/node\\.h$|node\\.lib$|LICENSE$' + curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \ + https://nodejs.org/dist/v24.18.0/SHASUMS256.txt | \ + rg 'headers|win-(x64|arm64)/node\\.lib|win-(x64|arm64)\\.zip' + ``` + +- Result: discriminating discovery. The authenticated x64 ZIP contains 2,449 files and 105,728,964 + expanded bytes, including the 92,534,088-byte `node.exe` and root `LICENSE`, but no + `include/node/node.h` and no `node.lib`. The same signed checksum document authenticates + `node-v24.18.0-headers.tar.gz` as + `6c7d41d83c3481d2301115b8ce4a44b7d4fbfa52859b1aac14f445d460137887`, + `win-x64/node.lib` as + `589684168a73547ca47cd22d76a4e465ef561abe89fb1b2b23fe35bbe857d505`, and + `win-arm64/node.lib` as + `7da03c5111815b69bbe63ffd2e51b28cd69eec9f545b7ccb8756efffdbb88dc2`. +- Oracle proved: the original six-binary-archive contract is insufficient to build `node-pty` on + Windows without an unrecorded `node-gyp` download. Both plan artifacts now require the signed + headers archive and per-architecture import library as explicit immutable build inputs. +- Does not prove: headers/import-library download or extraction, Windows build, ZIP determinism, + bundled Node/ConPTY/watcher execution, signing/trust, SSH, or any enabled tuple. +- Checklist items satisfied: Windows build-input policy correction only; implementation remains + unchecked. +- Follow-up: extend the signed input schema/verifier and hostile-input tests, stage only the required + headers and `node.lib`, then run the native Windows build with implicit downloads disabled. + +### E-M3-WINDOWS-ZIP-RED-001 — Windows ZIP/input contract red gate + +- Date: 2026-07-14 +- Commit SHA / PR: uncommitted Windows artifact-only tests on top of `c308107a9`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: macOS 26.2 arm64; Node v26.0.0 and pnpm 10.24.0 +- Remote and transport: none; synthetic local test discovery only +- Exact command: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-zip.test.mjs \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs + ``` + +- Result: expected FAIL before implementation. Both suites failed at import because the deliberately + missing `ssh-relay-runtime-zip.mjs` and `ssh-relay-node-zip-inspection.mjs` modules did not exist; + zero tests were collected. +- Duration and resource metrics: 196 ms Vitest duration; archive/runtime resources do not apply. +- Oracle proved: both Windows ZIP boundaries were discriminating before their implementations. +- Does not prove: any ZIP validation, real Node input, Windows build/execution, SSH, or tuple support. +- Checklist items satisfied: red half of the Windows ZIP/input contract gate only. +- Follow-up: E-M3-WINDOWS-INPUT-001 records the first green implementation and real signed inputs. + +### E-M3-WINDOWS-INPUT-001 — Signed Windows Node inputs and bounded selective extraction + +- Date: 2026-07-14 +- Commit SHA / PR: uncommitted Windows artifact-only implementation on top of `c308107a9`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741); native Windows CI pending +- Runners: focused tests and real selective extraction on macOS 26.2 arm64 with Node v26.0.0/pnpm + 10.24.0; signature/checksum/file verification in native Linux arm64 container + `node@sha256:032e78d7e54e352129831743737e3a83171d9cc5b5896f411649c597ce0b11ea` + with GnuPG/gpgv 2.2.40 +- Remote and transport: exact HTTPS inputs from `nodejs.org/dist/v24.18.0`; no SSH, release upload, + Windows execution, or remote-host egress +- Exact commands: + + ```sh + 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 \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + + docker run --rm \ + -v "$PWD:/workspace:ro" -v /tmp/orca-node-win-input-check:/inputs:ro \ + -w /workspace \ + node@sha256:032e78d7e54e352129831743737e3a83171d9cc5b5896f411649c597ce0b11ea \ + bash -lc 'set -euo pipefail + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq gnupg gpgv >/dev/null + node config/scripts/verify-ssh-relay-node-release-inputs.mjs \ + --inputs-directory /inputs --archive win32-x64' + + node --input-type=module -e \ + "import {readFile} from 'node:fs/promises'; + import {extractVerifiedSshRelayNodeZipBuildInputs} from + './config/scripts/ssh-relay-node-zip-inspection.mjs'; + const release=JSON.parse(await readFile( + './config/ssh-relay-node-release-v24.18.0.json','utf8')); + console.log(await extractVerifiedSshRelayNodeZipBuildInputs( + release,'win32-x64','','', + {headersArchivePath:'',importLibraryPath:''}));" + ``` + +- Result: PASS. Six focused suites passed 19/19 tests. The real verifier authenticated signer + `C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C`, the exact signed checksum document, all nine pinned + release inputs, the 37,176,245-byte x64 ZIP, 9,951,449-byte headers archive, and 2,986,260-byte x64 + import library. Bounded ZIP inspection consumed 2,449 entries/1,984 files/105,728,964 expanded + bytes and selectively staged only `node.exe` plus `LICENSE`. Bounded headers inspection consumed + 3,326 entries/2,726 files/58,969,198 expanded bytes and staged only `include`; `node.lib` was + rehashed before and after exclusive copy to `Release/node.lib`. +- Resource and integrity details: all input hashing and archive entry verification streamed; each + file was bounded by 256 MiB, aggregate input expansion by 1 GiB, path depth by 32, path bytes by + 512, and entries by 100,000. Synthetic runtime ZIP creation was byte-identical for the same tree + and epoch and rejected extra entries and content mismatch. Peak RSS, open files, and cancellation + settlement were not instrumented in this local slice. +- Oracle proved: exact schema and signed-checksum coverage for the Windows ZIP, common headers, and + tuple import library; missing/malformed input rejection; safe portable ZIP paths, traversal and + case-fold collision rejection; CRC-32 plus SHA-256 streaming; bounded selective extraction; exact + post-copy hashes; and deterministic synthetic runtime ZIP packing/inspection. +- Does not prove: native Windows `node-pty` build, that `node-gyp` succeeds offline, bundled + `node.exe`/ConPTY/watcher execution, full-runtime ZIP identity on Windows, Windows x64/arm64 + signing/trust, oldest baseline, SSH/SFTP/system-SSH, release publication, cache/fallback/UI, or any + enabled tuple. Local `gpg`/`gpgv` were unavailable on macOS, so the signature proof is the pinned + Linux container command above; Windows jobs explicitly select Git for Windows' bundled tools. +- Checklist items satisfied: explicit signed Windows build-input contract and bounded local staging + only; no Windows per-tuple build/executable cell is checked. +- Follow-up: run the exact builder on native `windows-2022` and `windows-11-arm`, require the + loopback-only `node-gyp` fallback URL to remain unused, and record only the cells actually passed. + ### E-M3-RUNTIME-LOCAL-001 — First target-native Linux arm64 runtime artifact - Date: 2026-07-14 diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index b3198b31b8b..09adbb1be72 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1135,15 +1135,37 @@

    Build for the runtime, architecture, and libc together.

    Rosetta SSH cell. These are selector eligibility constraints, not tuple enablement; every candidate still needs the complete two-layer evidence.

    +

    + Windows target-native native-module builds use three immutable inputs authenticated by + that same signed checksum document: the tuple's official Node ZIP, the exact-version + headers.tar.gz, and the tuple's win-*/node.lib. The Windows + ZIP does not contain build headers or the import library. Stage only the required + executable, license, headers, and import library into the local build root; disable + implicit node-gyp downloads so no unrecorded build input can enter. +

    Archive contents
      -
    • Unmodified official Node v24.18.0 executable pinned by source archive hash.
    • +
    • + Unmodified official Node v24.18.0 executable pinned by its signed binary-archive + hash. Windows native builds additionally authenticate the signed headers archive + and architecture-specific node.lib, neither of which is shipped in + the Windows runtime ZIP. +
    • relay.js and crash-isolated relay-watcher.js.
    • Orca-patched node-pty@1.1.0 built for the bundled runtime.
    • +
    • + Windows also carries the pinned node-pty ConPTY runtime closure (conpty.dll + and OpenConsole.exe) for the tuple architecture. Hash both files into + the tree identity, classify them as native runtime inputs for signing/trust, and + run PTY smoke with the production useConptyDll: true behavior rather + than silently exercising system ConPTY. +
    • @parcel/watcher@2.5.6 with exactly one native optional package.
    • diff --git a/package.json b/package.json index 12c0fe1b542..52c183391be 100644 --- a/package.json +++ b/package.json @@ -222,6 +222,8 @@ "vitest": "^4.1.5", "vscode-oniguruma": "^2.0.1", "vscode-textmate": "^9.3.2", + "yauzl": "3.4.0", + "yazl": "3.3.1", "zustand": "^5.0.13" }, "optionalDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d879a29059f..db785a10da3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -374,6 +374,12 @@ importers: vscode-textmate: specifier: ^9.3.2 version: 9.3.2 + yauzl: + specifier: 3.4.0 + version: 3.4.0 + yazl: + specifier: 3.3.1 + version: 3.3.1 zustand: specifier: ^5.0.13 version: 5.0.13(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)) @@ -3604,6 +3610,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -5664,6 +5674,9 @@ packages: resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} engines: {node: '>=12', npm: '>=6'} + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -6808,6 +6821,13 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yauzl@3.4.0: + resolution: {integrity: sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==} + engines: {node: '>=12'} + + yazl@3.3.1: + resolution: {integrity: sha512-BbETDVWG+VcMUle37k5Fqp//7SDOK2/1+T7X8TD96M3D9G8jK5VLUdQVdVjGi8im7FGkazX7kk5hkU8X4L5Bng==} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -9795,6 +9815,8 @@ snapshots: node-releases: 2.0.38 update-browserslist-db: 1.2.3(browserslist@4.28.2) + buffer-crc32@1.0.0: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -12242,6 +12264,8 @@ snapshots: pe-library@0.4.1: {} + pend@1.2.0: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -13525,6 +13549,14 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yauzl@3.4.0: + dependencies: + pend: 1.2.0 + + yazl@3.3.1: + dependencies: + buffer-crc32: 1.0.0 + yocto-queue@0.1.0: {} yocto-spinner@1.2.0: From 539cd060e74386ba33bf1291c940b68fba9af13b Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 09:59:58 -0400 Subject: [PATCH 006/267] docs(ssh): record Windows runtime artifact gates Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 74 ++++++++++++++++--- 1 file changed, 64 insertions(+), 10 deletions(-) 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 c4bf9ff99f4..b2cd77b26af 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 @@ -58,10 +58,10 @@ same change as the work it records. [#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 - smoke, SBOM, and provenance only. The Windows x64/arm64 artifact candidate is uncommitted on top - of `c308107a9` in stacked draft PR [#8741](https://github.com/stablyai/orca/pull/8741) while its - final diff and local gates are re-audited before native CI. It may produce test artifacts but must - not publish, resolve, transfer, install, launch, or enable them. + smoke, SBOM, and provenance only. The Windows x64/arm64 artifact candidate is implemented at exact + head `922edb6ff28199b394f508731fd18a635bec49a0` in stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) and awaits exact-head native CI. It 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, 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 @@ -79,8 +79,9 @@ same change as the work it records. proof remains active and no implicit `node-gyp` download is allowed. - Windows PTY-closure correction: E-M3-WINDOWS-CONPTY-GAP-001 proved the candidate omitted the `conpty.dll` and `OpenConsole.exe` files required by Orca's production `useConptyDll: true` path - and therefore exercised a weaker system-ConPTY smoke. The artifact and smoke correction is active; - no Windows executable cell is complete. + and therefore exercised a weaker system-ConPTY smoke. E-M3-WINDOWS-LOCAL-002 proves the corrected + local tree-closure and smoke configuration contracts; native execution remains open and no + Windows executable cell is complete. - Production behavior: unchanged; Orca embeds relay JavaScript and installs `node-pty` plus `@parcel/watcher` with remote npm. - New runtime assets published: none. @@ -1216,6 +1217,7 @@ focused commands as their scripts/tests are introduced. - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-node-release-verification.test.mjs` (E-M3-NODE-RED-001, E-M3-NODE-PROVENANCE-001) - [x] `node config/scripts/verify-ssh-relay-node-release-inputs.mjs --inputs-directory --archive linux-x64-glibc` (E-M3-NODE-PROVENANCE-001; metadata and archive verification only) - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-node-zip-inspection.test.mjs config/scripts/ssh-relay-runtime-zip.test.mjs` (E-M3-WINDOWS-ZIP-RED-001, E-M3-WINDOWS-INPUT-001; synthetic ZIP/input contracts only) +- [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-windows-tree.test.mjs` (E-M3-WINDOWS-LOCAL-002; structural Windows ConPTY closure only, no execution) - [x] `node config/scripts/verify-ssh-relay-node-release-inputs.mjs --inputs-directory --archive win32-x64` (E-M3-WINDOWS-INPUT-001; signed real Node ZIP, headers, and import library; no Windows execution) - [x] `node config/scripts/build-ssh-relay-runtime.mjs --tuple linux-arm64-glibc --inputs-directory --output-directory --source-date-epoch --git-commit ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) - [x] `node config/scripts/verify-ssh-relay-runtime.mjs --runtime-directory --identity --archive ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) @@ -2301,8 +2303,9 @@ fragmentLinks=9`. ### E-M3-WINDOWS-CONPTY-GAP-001 — Candidate omitted the production ConPTY runtime closure - Date: 2026-07-14 -- Commit SHA / PR: source audit of the uncommitted Windows artifact-only candidate on top of - `c308107a9`; draft PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Commit SHA / PR: source audit while preparing implementation commit + `922edb6ff28199b394f508731fd18a635bec49a0`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) - Runner: macOS 26.2 arm64; source and installed-package inspection only - Remote and transport: none - Exact commands: @@ -2400,8 +2403,8 @@ fragmentLinks=9`. ### E-M3-WINDOWS-INPUT-001 — Signed Windows Node inputs and bounded selective extraction - Date: 2026-07-14 -- Commit SHA / PR: uncommitted Windows artifact-only implementation on top of `c308107a9`; draft PR - [#8741](https://github.com/stablyai/orca/pull/8741); native Windows CI pending +- Commit SHA / PR: implementation commit `922edb6ff28199b394f508731fd18a635bec49a0`; + draft PR [#8741](https://github.com/stablyai/orca/pull/8741); native Windows CI pending - Runners: focused tests and real selective extraction on macOS 26.2 arm64 with Node v26.0.0/pnpm 10.24.0; signature/checksum/file verification in native Linux arm64 container `node@sha256:032e78d7e54e352129831743737e3a83171d9cc5b5896f411649c597ce0b11ea` @@ -2466,6 +2469,57 @@ fragmentLinks=9`. - Follow-up: run the exact builder on native `windows-2022` and `windows-11-arm`, require the loopback-only `node-gyp` fallback URL to remain unused, and record only the cells actually passed. +### E-M3-WINDOWS-LOCAL-002 — Corrected Windows artifact contracts and local static gates + +- Date: 2026-07-14 +- Commit SHA / PR: implementation commit `922edb6ff28199b394f508731fd18a635bec49a0`; + draft PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: macOS 26.2 arm64; Node v26.0.0 and pnpm 10.24.0. The repository requires Node 24, so + exact-head draft-PR CI is the authoritative supported-Node result. +- Remote and transport: none; synthetic contracts and repository static checks only +- Exact commands: + + ```sh + 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 \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm run typecheck + pnpm exec oxlint + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html + git diff --check + ``` + +- Result: PASS. Seven focused suites passed 21/21 tests in 290 ms. Typecheck, oxlint, max-lines, + full lint/reliability/localization gates, plan formatting, and diff whitespace passed. Oxlint + reported only pre-existing warnings outside this package. Full lint used `GOMAXPROCS=2` because + the external Go type-aware linter had previously segfaulted on an existing dependency declaration + at unconstrained concurrency. +- Oracle proved: deterministic synthetic Windows ZIP packing and complete ZIP reinspection; + signed-input schema and bounded ZIP/header staging contracts; Windows-unsafe header path + rejection; the Windows runtime-tree identity contains `conpty.dll` and `OpenConsole.exe` as + native runtime files; the smoke configuration selects `useConptyDll: true`; the workflow declares + exact x64/arm64 native runners and has read-only, unpublished artifact authority. +- Resource metrics: focused Vitest duration 290 ms. Runtime RSS, open files/channels, cancellation + settlement, artifact size, and native build/smoke duration do not apply to these synthetic checks + and remain open for target-native evidence. +- Does not prove: Windows native compilation, offline `node-gyp`, staged ConPTY execution, + bundled-Node/PTY/watcher smoke, full artifact bytes or reproducibility, native trust/signing, + oldest baseline, SSH transfer/install, publication, resolver/cache/fallback/UI, or any enabled + tuple. +- Checklist items satisfied: the added purpose-named local Windows-tree command only. Windows + per-tuple build/executable and ConPTY implementation boxes remain unchecked pending native CI. +- Follow-up: push this exact implementation head and record exact Windows x64/arm64 jobs, images, + artifact IDs/hashes/sizes, build/verify/smoke durations, RSS, and any discriminating failure. + ### E-M3-RUNTIME-LOCAL-001 — First target-native Linux arm64 runtime artifact - Date: 2026-07-14 From 55f7137c6e065481d2f58fed1353df9a75914fc8 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 10:07:55 -0400 Subject: [PATCH 007/267] test(ssh): make relay tar fixture portable Co-authored-by: Orca --- .../ssh-relay-node-tar-inspection.test.mjs | 17 +++++- ...github-release-implementation-checklist.md | 61 +++++++++++++++++-- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/config/scripts/ssh-relay-node-tar-inspection.test.mjs b/config/scripts/ssh-relay-node-tar-inspection.test.mjs index 389bcfb9a53..1a8aaca240d 100644 --- a/config/scripts/ssh-relay-node-tar-inspection.test.mjs +++ b/config/scripts/ssh-relay-node-tar-inspection.test.mjs @@ -1,4 +1,4 @@ -import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { Readable } from 'node:stream' @@ -87,8 +87,19 @@ async function validTarStream() { writeFile(join(directory, root, 'LICENSE'), 'Node license'), writeFile(join(directory, root, 'include', 'node', 'node.h'), 'Node header') ]) - await chmod(join(directory, root, 'bin', 'node'), 0o755) - return create({ cwd: directory, portable: true }, [root]) + return create( + { + cwd: directory, + portable: true, + // Why: NTFS cannot express POSIX execute bits, so the archive fixture must declare its mode. + onWriteEntry: (entry) => { + if (entry.path.replaceAll('\\', '/') === `${root}/bin/node` && entry.stat) { + entry.stat.mode = (entry.stat.mode & ~0o777) | 0o755 + } + } + }, + [root] + ) } function rawTar(entries) { 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 b2cd77b26af..21993023e25 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 @@ -82,6 +82,10 @@ same change as the work it records. and therefore exercised a weaker system-ConPTY smoke. E-M3-WINDOWS-LOCAL-002 proves the corrected local tree-closure and smoke configuration contracts; native execution remains open and no Windows executable cell is complete. +- First Windows native run: E-M3-WINDOWS-CI-RED-002 failed before download/build on both Windows + architectures. One failure is a confirmed NTFS fixture-mode assumption; two import-time + `SyntaxError` failures remain unexplained until the corrected exact-head rerun. No Windows + executable evidence is claimed from the run. - Production behavior: unchanged; Orca embeds relay JavaScript and installs `node-pty` plus `@parcel/watcher` with remote npm. - New runtime assets published: none. @@ -95,11 +99,12 @@ same change as the work it records. runtime is an explicit per-target Beta opt-in under E-M1-ROLLOUT-DECISION-001. - Legacy fallback removal: not authorized. - Next required action: finish the final local audit of the bounded Windows runtime/ZIP builder and - verifier, commit the artifact-only implementation, and collect exact Windows x64/arm64 plus POSIX - regression evidence from draft-PR CI. A same-head/same-runner clean-rebuild identity oracle for - native build reproducibility remains a separate open gate. Keep cross-family remote - infrastructure, signing/trust, and measured legacy baseline gates open; do not introduce - publication, resolver, transfer, rollout, tuple enablement, or default behavior. + verifier, correct the Windows-only test failures from E-M3-WINDOWS-CI-RED-002, and collect exact + Windows x64/arm64 plus POSIX regression evidence from the corrected draft-PR head. A + same-head/same-runner clean-rebuild identity oracle for native build reproducibility remains a + separate open gate. Keep cross-family remote infrastructure, signing/trust, and measured legacy + baseline gates open; do not introduce publication, resolver, transfer, rollout, tuple enablement, + or default behavior. ## Non-Negotiable Invariants @@ -2520,6 +2525,52 @@ fragmentLinks=9`. - Follow-up: push this exact implementation head and record exact Windows x64/arm64 jobs, images, artifact IDs/hashes/sizes, build/verify/smoke durations, RSS, and any discriminating failure. +### E-M3-WINDOWS-CI-RED-002 — First Windows native jobs stopped at contract tests + +- Date: 2026-07-14 +- Commit SHA / PR: exact workflow head `539cd060e74386ba33bf1291c940b68fba9af13b` containing + implementation commit `922edb6ff28199b394f508731fd18a635bec49a0`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Run: [SSH Relay Runtime Artifacts 29338911990](https://github.com/stablyai/orca/actions/runs/29338911990) +- Native jobs: + - x64 job `87105197603`: requested `windows-2022`; resolved `win22` image + `20260706.237.1`, GitHub-hosted X64, Windows Server 2022 Datacenter build 20348, Node v24.18.0; + failed after 1m17s. + - arm64 job `87105197605`: requested `windows-11-arm`; resolved `win11-arm64` image + `20260706.102.1`, GitHub-hosted ARM64, Windows 10 Enterprise build 26200, Node v24.18.0; failed + after 3m37s. +- Remote and transport: none. Both jobs stopped before Node input download, native build, runtime + smoke, or artifact upload. +- Exact commands: + + ```sh + gh api repos/stablyai/orca/actions/jobs/87105197603/logs > /tmp/orca-win-x64-red.log + gh api repos/stablyai/orca/actions/jobs/87105197605/logs > /tmp/orca-win-arm64-red.log + rg -n "requested_runner=|resolved_image_os=|resolved_image_version=|runner_arch=|WindowsProductName|WindowsVersion|OsBuildNumber|source_commit=|FAIL|SyntaxError|Node archive is missing" \ + /tmp/orca-win-x64-red.log /tmp/orca-win-arm64-red.log + ``` + +- Result: FAIL as a discriminating pre-build gate on both architectures. Each run reported three + failed and four passed suites, with one failed and ten passed collected tests. The x64 test phase + took 1.26s; arm64 took 1.33s. `ssh-relay-node-tar-inspection.test.mjs` created its POSIX tar + fixture from an NTFS file and therefore lost the declared `bin/node` execute bit; the production + inspector correctly rejected it. `ssh-relay-node-release-verification.test.mjs` and + `ssh-relay-runtime-artifact.test.mjs` separately failed at import with only `SyntaxError: Invalid +or unexpected token`; the first logs did not identify a source location. +- Oracle proved: both requested hosted runner labels resolve to native Node 24 environments, MSVC + setup succeeds, Git for Windows supplies both required GPG tools, frozen source installation + succeeds, and the test command stops the artifact build before any download or upload. It also + discriminated one cross-platform fixture assumption. +- Does not prove: that the explicit-mode fixture correction is green on Windows; the cause of the + two import-time syntax errors; Node input download/signature verification; native compilation; + offline `node-gyp`; bundled Node/ConPTY/watcher execution; ZIP output; native trust; SSH; or any + enabled tuple. +- Checklist items satisfied: exact Windows runner-label resolution only. No Windows build, + executable, archive, or artifact cell is checked. +- Follow-up: declare `0o755` in the tar fixture's archive metadata instead of relying on NTFS, + rerun the same seven suites on both native architectures, and diagnose the two import failures if + they recur before allowing either job to download inputs. + ### E-M3-RUNTIME-LOCAL-001 — First target-native Linux arm64 runtime artifact - Date: 2026-07-14 From ef94c90663740c657d44e93f758c99016e66b61a Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 10:11:21 -0400 Subject: [PATCH 008/267] test(ssh): keep relay CLI modules importable Co-authored-by: Orca --- .../verify-ssh-relay-node-release-inputs.mjs | 2 -- config/scripts/verify-ssh-relay-runtime.mjs | 2 -- ...relay-github-release-implementation-checklist.md | 13 ++++++++++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/config/scripts/verify-ssh-relay-node-release-inputs.mjs b/config/scripts/verify-ssh-relay-node-release-inputs.mjs index 2ee689aae8e..da2e8ef254e 100644 --- a/config/scripts/verify-ssh-relay-node-release-inputs.mjs +++ b/config/scripts/verify-ssh-relay-node-release-inputs.mjs @@ -1,5 +1,3 @@ -#!/usr/bin/env node - import { readFile } from 'node:fs/promises' import { dirname, isAbsolute, join, resolve } from 'node:path' import { pathToFileURL } from 'node:url' diff --git a/config/scripts/verify-ssh-relay-runtime.mjs b/config/scripts/verify-ssh-relay-runtime.mjs index e1ee06d076c..5179d0fc034 100644 --- a/config/scripts/verify-ssh-relay-runtime.mjs +++ b/config/scripts/verify-ssh-relay-runtime.mjs @@ -1,5 +1,3 @@ -#!/usr/bin/env node - import { execFile } from 'node:child_process' import { createHash } from 'node:crypto' import { readFile, readdir, stat } from 'node:fs/promises' 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 21993023e25..148424cf679 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 @@ -2557,6 +2557,13 @@ fragmentLinks=9`. inspector correctly rejected it. `ssh-relay-node-release-verification.test.mjs` and `ssh-relay-runtime-artifact.test.mjs` separately failed at import with only `SyntaxError: Invalid or unexpected token`; the first logs did not identify a source location. +- Discriminating follow-up: exact head `55f7137c6e065481d2f58fed1353df9a75914fc8` reran in + [run 29339487426](https://github.com/stablyai/orca/actions/runs/29339487426). X64 job + `87107170544` proved the explicit tar-mode correction: five suites and all 11 collected tests + passed in 1.24s. Only the same two import-time syntax errors remained. Both failing suites import + a command-line `.mjs` file with a Unix shebang, while every supported call site already invokes + those files through `node`; the next correction removes only those unused shebangs and preserves + the CLI main guards. - Oracle proved: both requested hosted runner labels resolve to native Node 24 environments, MSVC setup succeeds, Git for Windows supplies both required GPG tools, frozen source installation succeeds, and the test command stops the artifact build before any download or upload. It also @@ -2567,9 +2574,9 @@ or unexpected token`; the first logs did not identify a source location. enabled tuple. - Checklist items satisfied: exact Windows runner-label resolution only. No Windows build, executable, archive, or artifact cell is checked. -- Follow-up: declare `0o755` in the tar fixture's archive metadata instead of relying on NTFS, - rerun the same seven suites on both native architectures, and diagnose the two import failures if - they recur before allowing either job to download inputs. +- Follow-up: rerun the same seven suites on both native architectures without the two unused + shebangs. Treat a recurrence as evidence against the transform-boundary diagnosis and continue to + block input download/build. ### E-M3-RUNTIME-LOCAL-001 — First target-native Linux arm64 runtime artifact From b387ac48cda70b60a5a148c03ce740f9afedf9cb Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 10:14:48 -0400 Subject: [PATCH 009/267] test(ssh): preserve Windows verification fixtures Co-authored-by: Orca --- .gitattributes | 1 + .../scripts/ssh-relay-runtime-artifact.test.mjs | 11 +++++++---- ...ay-github-release-implementation-checklist.md | 16 +++++++++++++--- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.gitattributes b/.gitattributes index 399e8099114..42637cf496d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,6 +6,7 @@ /config/scripts/run-internal-dev-setup.mjs text eol=lf /config/scripts/verify-cli-bin.mjs text eol=lf /config/scripts/verify-release-required-assets.mjs text eol=lf +/config/ssh-relay-node-release-keys/*.asc -text /skill-guides/*.md text eol=lf /skills/*/SKILL.md text eol=lf /src/cli/bundled-skill-guides.ts text eol=lf diff --git a/config/scripts/ssh-relay-runtime-artifact.test.mjs b/config/scripts/ssh-relay-runtime-artifact.test.mjs index ed8c1e96d11..d09198ed799 100644 --- a/config/scripts/ssh-relay-runtime-artifact.test.mjs +++ b/config/scripts/ssh-relay-runtime-artifact.test.mjs @@ -170,10 +170,13 @@ describe('SSH relay runtime artifact', () => { ) await writeFile(join(fixture.runtimeRoot, 'relay.js'), 'relay') - await chmod(join(fixture.runtimeRoot, 'relay.js'), 0o600) - await expect(verifyRuntimeTree(fixture.runtimeRoot, fixture.identity)).rejects.toThrow( - /entry mismatch/i - ) + if (process.platform !== 'win32') { + // Why: NTFS cannot represent the POSIX mode that the verified ZIP carries for remote install. + await chmod(join(fixture.runtimeRoot, 'relay.js'), 0o600) + await expect(verifyRuntimeTree(fixture.runtimeRoot, fixture.identity)).rejects.toThrow( + /entry mismatch/i + ) + } }) it.skipIf(process.platform === 'win32')( 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 148424cf679..ea2d57297e4 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 @@ -2564,6 +2564,16 @@ or unexpected token`; the first logs did not identify a source location. a command-line `.mjs` file with a Unix shebang, while every supported call site already invokes those files through `node`; the next correction removes only those unused shebangs and preserves the CLI main guards. +- Second follow-up: exact head `ef94c90663740c657d44e93f758c99016e66b61a` reran in + [run 29339742904](https://github.com/stablyai/orca/actions/runs/29339742904). X64 job + `87108126824` collected all seven suites and 21 tests, proving the shebang correction. Eighteen + tests passed, one POSIX archive test skipped as designed, and two Windows-specific assertions + failed in 1.17s: Git checkout converted the pinned armored key from LF to CRLF, and a POSIX + `chmod(0o600)` rejection assertion ran even though the Windows verifier intentionally takes modes + from verified ZIP metadata. The received key hash + `25cc2da386fe54cfc6a3d683f1df2a5f636014a31be8efadd73a9ecc2208dbec` exactly matches + `sed 's/$/\r/' | shasum -a 256`; the committed LF bytes remain + `84b1ca614406f341cb86e72920f5a64687a13ab67ab84038bcf2abba97898a84`. - Oracle proved: both requested hosted runner labels resolve to native Node 24 environments, MSVC setup succeeds, Git for Windows supplies both required GPG tools, frozen source installation succeeds, and the test command stops the artifact build before any download or upload. It also @@ -2574,9 +2584,9 @@ or unexpected token`; the first logs did not identify a source location. enabled tuple. - Checklist items satisfied: exact Windows runner-label resolution only. No Windows build, executable, archive, or artifact cell is checked. -- Follow-up: rerun the same seven suites on both native architectures without the two unused - shebangs. Treat a recurrence as evidence against the transform-boundary diagnosis and continue to - block input download/build. +- Follow-up: mark the pinned armored key `-text` so checkout preserves its authenticated bytes, + keep the filesystem-mode mutation assertion POSIX-only, and rerun both native architectures. + Continue to block input download/build until all seven suites pass. ### E-M3-RUNTIME-LOCAL-001 — First target-native Linux arm64 runtime artifact From b6903b2205b52472e64f241fd0f2117196602deb Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 10:23:38 -0400 Subject: [PATCH 010/267] fix(ssh): keep GPG verification paths portable Co-authored-by: Orca --- .../ssh-relay-node-release-signature.mjs | 40 ++--- ...h-relay-node-release-verification.test.mjs | 60 ++++++++ ...github-release-implementation-checklist.md | 143 ++++++++++++++++-- 3 files changed, 213 insertions(+), 30 deletions(-) diff --git a/config/scripts/ssh-relay-node-release-signature.mjs b/config/scripts/ssh-relay-node-release-signature.mjs index 173d5288666..04b96ed45e1 100644 --- a/config/scripts/ssh-relay-node-release-signature.mjs +++ b/config/scripts/ssh-relay-node-release-signature.mjs @@ -42,9 +42,10 @@ async function readVerifiedFile(filePath, maximumBytes, expectedSha256, label) { return Buffer.concat(chunks, bytes) } -async function defaultCommandRunner(command, args) { +async function defaultCommandRunner(command, args, { cwd } = {}) { try { const result = await execFileAsync(command, args, { + cwd, encoding: 'utf8', maxBuffer: MAX_COMMAND_OUTPUT_BYTES, timeout: COMMAND_TIMEOUT_MS, @@ -104,7 +105,6 @@ export async function verifySshRelayNodeSignature( const directory = await mkdtemp(join(tmpdir(), 'orca-node-signature-')) try { const armoredKeyPath = join(directory, 'release-key.asc') - const keyringPath = join(directory, 'release-key.gpg') const verifiedChecksumPath = join(directory, release.checksumDocument.name) const verifiedSignaturePath = join(directory, release.signature.name) await Promise.all([ @@ -113,28 +113,32 @@ export async function verifySshRelayNodeSignature( writeFile(verifiedSignaturePath, signatureBytes, { mode: 0o600 }) ]) - const dearmor = await commandRunner('gpg', [ - '--batch', - '--yes', - '--dearmor', - '--output', - keyringPath, - armoredKeyPath - ]) + // Git for Windows GPG treats drive-letter keyring paths as resource URLs. + // Relative names stay inside the exclusive verified-copy directory on every platform. + const commandOptions = { cwd: directory } + const dearmor = await commandRunner( + 'gpg', + ['--batch', '--yes', '--dearmor', '--output', './release-key.gpg', './release-key.asc'], + commandOptions + ) if (dearmor.exitCode !== 0) { throw new Error( `gpgv key preparation failed: ${dearmor.stderr || 'gpg exited unsuccessfully'}` ) } - const verified = await commandRunner('gpgv', [ - '--status-fd', - '1', - '--keyring', - keyringPath, - verifiedSignaturePath, - verifiedChecksumPath - ]) + const verified = await commandRunner( + 'gpgv', + [ + '--status-fd', + '1', + '--keyring', + './release-key.gpg', + `./${release.signature.name}`, + `./${release.checksumDocument.name}` + ], + commandOptions + ) if (verified.exitCode !== 0) { throw new Error( `gpgv rejected the Node checksum signature: ${verified.stderr || 'unknown error'}` diff --git a/config/scripts/ssh-relay-node-release-verification.test.mjs b/config/scripts/ssh-relay-node-release-verification.test.mjs index fbf2d1b8e4b..c9bdecba07d 100644 --- a/config/scripts/ssh-relay-node-release-verification.test.mjs +++ b/config/scripts/ssh-relay-node-release-verification.test.mjs @@ -242,6 +242,66 @@ describe('SSH relay Node release verification', () => { ).rejects.toThrow(/pinned Node release signer fingerprint/i) }) + it('keeps GPG verified-copy paths relative to the exclusive working directory', async () => { + const directory = await mkdtemp(join(tmpdir(), 'orca-node-signature-path-test-')) + temporaryDirectories.push(directory) + const keyPath = join(directory, 'release-key.asc') + const checksumPath = join(directory, 'SHASUMS256.txt') + const signaturePath = join(directory, 'SHASUMS256.txt.sig') + await writeFile(keyPath, 'key') + await writeFile(checksumPath, 'checksums') + await writeFile(signaturePath, 'signature') + const release = contract({ + checksumDocument: { ...contract().checksumDocument, sha256: digest('checksums') }, + signature: { + ...contract().signature, + sha256: digest('signature'), + key: { ...contract().signature.key, path: keyPath, sha256: digest('key') } + } + }) + const calls = [] + const commandRunner = async (command, args, options) => { + calls.push({ command, args, options }) + return { + exitCode: 0, + stdout: + command === 'gpgv' + ? `[GNUPG:] VALIDSIG ${release.signature.signerFingerprint} signed fixture\n` + : '', + stderr: '' + } + } + + await verifySshRelayNodeSignature(release, { checksumPath, signaturePath, commandRunner }) + + expect(calls.map(({ command, args }) => ({ command, args }))).toEqual([ + { + command: 'gpg', + args: [ + '--batch', + '--yes', + '--dearmor', + '--output', + './release-key.gpg', + './release-key.asc' + ] + }, + { + command: 'gpgv', + args: [ + '--status-fd', + '1', + '--keyring', + './release-key.gpg', + './SHASUMS256.txt.sig', + './SHASUMS256.txt' + ] + } + ]) + expect(calls[0].options.cwd).toMatch(/orca-node-signature-/) + expect(calls[1].options.cwd).toBe(calls[0].options.cwd) + }) + it('verifies a Windows archive, headers, and import library through the CLI API', async () => { const directory = await mkdtemp(join(tmpdir(), 'orca-node-cli-test-')) temporaryDirectories.push(directory) 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 ea2d57297e4..c892768c251 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — four POSIX native-runner artifact builds are CI-green; Windows runtime/ZIP, reproducibility, oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — four POSIX native-runner artifact builds are CI-green; Windows contract tests are native-runner green but signature-tool path portability, runtime/ZIP, reproducibility, oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -58,10 +58,12 @@ same change as the work it records. [#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 - smoke, SBOM, and provenance only. The Windows x64/arm64 artifact candidate is implemented at exact - head `922edb6ff28199b394f508731fd18a635bec49a0` in stacked draft PR - [#8741](https://github.com/stablyai/orca/pull/8741) and awaits exact-head native CI. It may produce - test artifacts but must not publish, resolve, transfer, install, launch, or enable them. + smoke, SBOM, and provenance only. The Windows x64/arm64 artifact candidate and portability + corrections are at exact head `b387ac48cda70b60a5a148c03ce740f9afedf9cb` in stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). Both native architectures pass all seven + contract suites, then fail closed before extraction/build because Git for Windows `gpgv` rejects + an absolute drive-letter keyring path (E-M3-WINDOWS-CI-RED-003). It 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, 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 @@ -82,10 +84,11 @@ same change as the work it records. and therefore exercised a weaker system-ConPTY smoke. E-M3-WINDOWS-LOCAL-002 proves the corrected local tree-closure and smoke configuration contracts; native execution remains open and no Windows executable cell is complete. -- First Windows native run: E-M3-WINDOWS-CI-RED-002 failed before download/build on both Windows - architectures. One failure is a confirmed NTFS fixture-mode assumption; two import-time - `SyntaxError` failures remain unexplained until the corrected exact-head rerun. No Windows - executable evidence is claimed from the run. +- Windows native progression: E-M3-WINDOWS-CI-RED-002 captured and corrected NTFS fixture modes, + Windows shebang parsing, authenticated-key checkout bytes, and POSIX-only mode assertions. At the + corrected exact head, E-M3-WINDOWS-CI-RED-003 proves 20 passing and one intentionally skipped test + on each native architecture, exact input download, and a common fail-closed Git-for-Windows + `gpgv` drive-letter keyring incompatibility. No Windows executable evidence is claimed. - Production behavior: unchanged; Orca embeds relay JavaScript and installs `node-pty` plus `@parcel/watcher` with remote npm. - New runtime assets published: none. @@ -98,9 +101,9 @@ same change as the work it records. - Rollout control: existing per-SSH-target configuration; legacy is the default and the bundled runtime is an explicit per-target Beta opt-in under E-M1-ROLLOUT-DECISION-001. - Legacy fallback removal: not authorized. -- Next required action: finish the final local audit of the bounded Windows runtime/ZIP builder and - verifier, correct the Windows-only test failures from E-M3-WINDOWS-CI-RED-002, and collect exact - Windows x64/arm64 plus POSIX regression evidence from the corrected draft-PR head. A +- Next required action: pass only cwd-relative verified-copy paths to `gpg`/`gpgv`, add a + discriminating command-boundary regression test, and collect exact Windows x64/arm64 plus POSIX + regression evidence from the corrected draft-PR head. A same-head/same-runner clean-rebuild identity oracle for native build reproducibility remains a separate open gate. Keep cross-family remote infrastructure, signing/trust, and measured legacy baseline gates open; do not introduce publication, resolver, transfer, rollout, tuple enablement, @@ -2588,6 +2591,122 @@ or unexpected token`; the first logs did not identify a source location. keep the filesystem-mode mutation assertion POSIX-only, and rerun both native architectures. Continue to block input download/build until all seven suites pass. +### E-M3-WINDOWS-CI-RED-003 — Native Windows input verification exposed GPG path incompatibility + +- Date: 2026-07-14 +- Commit SHA / PR: exact workflow head `b387ac48cda70b60a5a148c03ce740f9afedf9cb`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Run: [SSH Relay Runtime Artifacts 29339998273](https://github.com/stablyai/orca/actions/runs/29339998273) +- Native jobs: + - x64 job `87109014067`: requested `windows-2022`; resolved `win22` image + `20260706.237.1`, GitHub-hosted X64, Windows Server 2022 Datacenter build 20348, Node v24.18.0; + failed after 1m21s. + - arm64 job `87109014073`: requested `windows-11-arm`; resolved `win11-arm64` image + `20260706.102.1`, GitHub-hosted ARM64, Windows 10 Enterprise build 26200, Node v24.18.0; failed + after approximately 3m. +- Remote and transport: none. Each job downloaded exact HTTPS inputs from + `nodejs.org/dist/v24.18.0`; neither reached archive extraction, native compilation, smoke, ZIP + creation, or artifact upload. +- Exact commands: + + ```sh + gh run view 29339998273 --repo stablyai/orca \ + --json databaseId,headSha,status,conclusion,url,createdAt,updatedAt,jobs + gh api repos/stablyai/orca/actions/jobs/87109014067/logs | \ + rg -n -C 12 "SSH relay runtime build failed|gpgv rejected|invalid key resource|Process completed with exit code" + gh api repos/stablyai/orca/actions/jobs/87109014073/logs | \ + rg -n -C 12 "SSH relay runtime build failed|gpgv rejected|invalid key resource|Process completed with exit code" + ``` + +- Result: FAIL as a discriminating signed-input gate on both architectures. All seven focused suites + collected on each runner; 20 tests passed and one POSIX-only test skipped as designed in 1.17s on + x64 and 1.47s on arm64. Exact Node inputs downloaded successfully. Git for Windows `gpgv` then + rejected the exclusive verified-copy keyring path before signature acceptance: + `invalid key resource URL 'C:\\...\\release-key.gpg'`. GnuPG interprets the drive-letter colon + as a resource-URL scheme at this option boundary. Both runners failed closed with `No public key`; + no unverified archive was extracted or executed. +- Oracle proved: the preceding fixture, shebang, checkout-byte, and filesystem-mode corrections are + green on both native Windows architectures; exact release inputs are downloadable; the production + signature gate executes and prevents unverified bytes from progressing. The identical x64/arm64 + failure isolates command-path representation rather than architecture-specific cryptography. +- Does not prove: successful Windows signature verification, archive/header/import-library + extraction, offline `node-gyp`, native compilation, bundled Node/ConPTY/watcher execution, ZIP + output or determinism, native trust/signing, oldest baseline, SSH, or any enabled tuple. +- Checklist items satisfied: Windows contract-test portability and fail-closed signed-input behavior + only. No Windows build, executable, archive, or artifact cell is checked. +- Follow-up: run both GPG commands in the exclusive verified-copy directory with cwd-relative paths, + assert the command boundary never receives an absolute drive-letter path, and rerun both native + architectures before any broader Windows correction. + +### E-M3-WINDOWS-GPG-PATH-RED-001 — Relative verified-copy command boundary red gate + +- Date: 2026-07-14 +- Commit SHA / PR: uncommitted purpose-named regression test on exact head + `b387ac48cda70b60a5a148c03ce740f9afedf9cb`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: macOS 26.2 arm64; Node v26.0.0 and pnpm 10.24.0 +- Remote and transport: none; mocked child-process command boundary only +- Exact command: + `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-node-release-verification.test.mjs` +- Result: expected FAIL; one new test failed and seven existing tests passed in 163 ms. The failure + diff showed absolute temporary paths passed to both `gpg --output` and `gpgv --keyring`, while the + required oracle expected cwd-relative verified-copy names and a common exclusive working directory. +- Oracle proved: the regression test discriminates the exact command representation rejected by Git + for Windows in E-M3-WINDOWS-CI-RED-003 without weakening signature/fingerprint validation. +- Does not prove: real GPG execution, Windows compatibility, successful signature verification, + archive extraction/build/smoke, or any tuple support. +- Checklist items satisfied: red half of the Windows GPG path-portability correction only. +- Follow-up: add cwd support to the bounded command runner, pass only explicit relative verified-copy + names, rerun this focused suite, and require native x64/arm64 CI before claiming the correction. + +### E-M3-WINDOWS-GPG-PATH-LOCAL-001 — Relative verified-copy command boundary correction + +- Date: 2026-07-14 +- Commit SHA / PR: uncommitted correction on exact base + `b387ac48cda70b60a5a148c03ce740f9afedf9cb`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: macOS 26.2 arm64; Node v26.0.0 and pnpm 10.24.0. The repository requires Node 24, so + exact-head draft-PR CI remains authoritative. +- Remote and transport: none; mocked GPG command-boundary regression plus local contract/static + checks +- Exact commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-release-verification.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 \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm run typecheck + pnpm exec oxlint + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html + git diff --check + ``` + +- Result: PASS. The focused signature suite passed 8/8 tests in 151 ms; all seven artifact suites + passed 22/22 tests in 313 ms. Typecheck, full oxlint, max-lines ratchet, full + lint/reliability/localization gates, plan formatting, and diff whitespace passed. Oxlint reported + only existing warnings outside this package; full lint used `GOMAXPROCS=2` for the previously + recorded external Go linter stability constraint. +- Oracle proved: both GPG invocations share the exclusive verified-copy directory as their cwd and + receive only explicit `./` paths for the key, keyring, checksum, and signature; existing hash, + command-failure, and exact-fingerprint tests remain green. +- Does not prove: real GPG execution, Git for Windows compatibility, Node 24 support, Windows native + build/smoke, archive output, or any tuple support. +- Checklist items satisfied: local green half of the path-portability correction only. Native CI is + still mandatory before the E-M3-WINDOWS-CI-RED-003 failure is considered corrected. +- Follow-up: run the exact correction on Windows x64/arm64 and all four POSIX artifact jobs, then + record any next discriminating native boundary without broadening this work package. + ### E-M3-RUNTIME-LOCAL-001 — First target-native Linux arm64 runtime artifact - Date: 2026-07-14 From 42aa02fa9eccf81e40d80f87461ddd232a1cda1f Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 10:24:00 -0400 Subject: [PATCH 011/267] docs(ssh): record Windows GPG path gate Co-authored-by: Orca --- ...-github-release-implementation-checklist.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) 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 c892768c251..35c08e48eef 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 @@ -59,11 +59,13 @@ same change as the work it records. deploy/resolver call site is connected and no tuple is enabled. - Active package: Work Package 2 target-native runtime assembly, archive inspection, executable smoke, SBOM, and provenance only. The Windows x64/arm64 artifact candidate and portability - corrections are at exact head `b387ac48cda70b60a5a148c03ce740f9afedf9cb` in stacked draft PR - [#8741](https://github.com/stablyai/orca/pull/8741). Both native architectures pass all seven - contract suites, then fail closed before extraction/build because Git for Windows `gpgv` rejects - an absolute drive-letter keyring path (E-M3-WINDOWS-CI-RED-003). It may produce test artifacts but - must not publish, resolve, transfer, install, launch, or enable them. + corrections are implemented through `b6903b220` in stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). Both native architectures passed all seven + contract suites at the preceding head, then failed closed before extraction/build because Git for + Windows `gpgv` rejected an absolute drive-letter keyring path (E-M3-WINDOWS-CI-RED-003). The + cwd-relative correction is locally green under E-M3-WINDOWS-GPG-PATH-LOCAL-001 and awaits native + CI. 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, 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 @@ -2641,8 +2643,8 @@ or unexpected token`; the first logs did not identify a source location. ### E-M3-WINDOWS-GPG-PATH-RED-001 — Relative verified-copy command boundary red gate - Date: 2026-07-14 -- Commit SHA / PR: uncommitted purpose-named regression test on exact head - `b387ac48cda70b60a5a148c03ce740f9afedf9cb`; draft PR +- Commit SHA / PR: red test introduced before its implementation in correction commit + `b6903b220`; exact base `b387ac48cda70b60a5a148c03ce740f9afedf9cb`; draft PR [#8741](https://github.com/stablyai/orca/pull/8741) - Runner: macOS 26.2 arm64; Node v26.0.0 and pnpm 10.24.0 - Remote and transport: none; mocked child-process command boundary only @@ -2662,7 +2664,7 @@ or unexpected token`; the first logs did not identify a source location. ### E-M3-WINDOWS-GPG-PATH-LOCAL-001 — Relative verified-copy command boundary correction - Date: 2026-07-14 -- Commit SHA / PR: uncommitted correction on exact base +- Commit SHA / PR: implementation commit `b6903b220` on exact base `b387ac48cda70b60a5a148c03ce740f9afedf9cb`; draft PR [#8741](https://github.com/stablyai/orca/pull/8741) - Runner: macOS 26.2 arm64; Node v26.0.0 and pnpm 10.24.0. The repository requires Node 24, so From 3aab5aff8cc8fe83788ed9d12755af72ea44d437 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 10:36:28 -0400 Subject: [PATCH 012/267] test(ssh): preserve runtime smoke failure details Co-authored-by: Orca --- .../ssh-relay-runtime-artifact.test.mjs | 24 ++- config/scripts/verify-ssh-relay-runtime.mjs | 45 ++++- ...github-release-implementation-checklist.md | 184 +++++++++++++++--- 3 files changed, 219 insertions(+), 34 deletions(-) diff --git a/config/scripts/ssh-relay-runtime-artifact.test.mjs b/config/scripts/ssh-relay-runtime-artifact.test.mjs index d09198ed799..e13ae1be529 100644 --- a/config/scripts/ssh-relay-runtime-artifact.test.mjs +++ b/config/scripts/ssh-relay-runtime-artifact.test.mjs @@ -10,7 +10,10 @@ import { inspectSshRelayRuntimeArchive } from './ssh-relay-runtime-archive.mjs' import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' -import { verifyRuntimeTree } from './verify-ssh-relay-runtime.mjs' +import { + formatSshRelayRuntimeSmokeFailure, + verifyRuntimeTree +} from './verify-ssh-relay-runtime.mjs' const temporaryDirectories = [] afterEach(async () => { @@ -179,6 +182,25 @@ describe('SSH relay runtime artifact', () => { } }) + it('preserves bounded bundled-smoke child failure diagnostics', () => { + const message = formatSshRelayRuntimeSmokeFailure({ + code: 'ETIMEDOUT', + killed: true, + signal: 'SIGTERM', + message: 'Command timed out', + stdout: 'partial smoke output', + stderr: `${'x'.repeat(70 * 1024)}Bundled runtime smoke failed: watcher create exceeded 15000 ms` + }) + + expect(message).toContain('timeoutMs=45000') + expect(message).toContain('code="ETIMEDOUT"') + expect(message).toContain('killed=true') + expect(message).toContain('signal="SIGTERM"') + expect(message).toContain('partial smoke output') + expect(message).toContain('truncated') + expect(message).toContain('watcher create exceeded 15000 ms') + }) + it.skipIf(process.platform === 'win32')( 'creates deterministic archives and rehashes every entry', async () => { diff --git a/config/scripts/verify-ssh-relay-runtime.mjs b/config/scripts/verify-ssh-relay-runtime.mjs index 5179d0fc034..f849367cbba 100644 --- a/config/scripts/verify-ssh-relay-runtime.mjs +++ b/config/scripts/verify-ssh-relay-runtime.mjs @@ -10,6 +10,31 @@ import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mj const execFileAsync = promisify(execFile) const scriptDirectory = import.meta.dirname const MAX_SMOKE_OUTPUT_BYTES = 4 * 1024 * 1024 +const MAX_SMOKE_FAILURE_DETAIL_BYTES = 64 * 1024 +const SMOKE_TIMEOUT_MS = 45_000 + +function boundedSshRelayRuntimeSmokeDetail(value) { + const text = typeof value === 'string' ? value : Buffer.isBuffer(value) ? value.toString() : '' + const bytes = Buffer.from(text) + if (bytes.length <= MAX_SMOKE_FAILURE_DETAIL_BYTES) { + return text + } + const retained = bytes.subarray(bytes.length - MAX_SMOKE_FAILURE_DETAIL_BYTES).toString() + return `[truncated ${bytes.length - MAX_SMOKE_FAILURE_DETAIL_BYTES} bytes]\n${retained}` +} + +export function formatSshRelayRuntimeSmokeFailure(error) { + return [ + 'Bundled runtime smoke command failed:', + `timeoutMs=${SMOKE_TIMEOUT_MS}`, + `code=${JSON.stringify(error?.code ?? null)}`, + `killed=${JSON.stringify(error?.killed ?? false)}`, + `signal=${JSON.stringify(error?.signal ?? null)}`, + `message=${JSON.stringify(error?.message ?? String(error))}`, + `stdout=${JSON.stringify(boundedSshRelayRuntimeSmokeDetail(error?.stdout))}`, + `stderr=${JSON.stringify(boundedSshRelayRuntimeSmokeDetail(error?.stderr))}` + ].join(' ') +} function parseArguments(argv) { const result = {} @@ -110,13 +135,19 @@ export async function verifyRuntimeTree(runtimeDirectory, identity) { async function runBundledSmoke(runtimeDirectory, identity) { const nodePath = join(runtimeDirectory, 'bin', identity.os === 'win32' ? 'node.exe' : 'node') const childPath = resolve(scriptDirectory, 'ssh-relay-runtime-smoke-child.cjs') - const result = await execFileAsync(nodePath, [childPath, runtimeDirectory], { - cwd: runtimeDirectory, - encoding: 'utf8', - maxBuffer: MAX_SMOKE_OUTPUT_BYTES, - timeout: 45_000, - windowsHide: true - }) + let result + try { + result = await execFileAsync(nodePath, [childPath, runtimeDirectory], { + cwd: runtimeDirectory, + encoding: 'utf8', + maxBuffer: MAX_SMOKE_OUTPUT_BYTES, + timeout: SMOKE_TIMEOUT_MS, + windowsHide: true + }) + } catch (error) { + // The child classifies PTY/watcher failures; retain its bounded tail to avoid blind CI retries. + throw new Error(formatSshRelayRuntimeSmokeFailure(error), { cause: error }) + } const smoke = JSON.parse(result.stdout) if (smoke.nodeVersion !== `v${identity.nodeVersion}`) { throw new Error(`Bundled smoke used unexpected Node version: ${smoke.nodeVersion}`) 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 35c08e48eef..37907dcd5e9 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — four POSIX native-runner artifact builds are CI-green; Windows contract tests are native-runner green but signature-tool path portability, runtime/ZIP, reproducibility, oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — four POSIX native-runner artifact builds are CI-green; Windows signed-input verification and runtime/ZIP assembly reach native smoke on both architectures, but bounded smoke diagnostics/execution, reproducibility, oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -61,11 +61,11 @@ same change as the work it records. smoke, SBOM, and provenance only. The Windows x64/arm64 artifact candidate and portability corrections are implemented through `b6903b220` in stacked draft PR [#8741](https://github.com/stablyai/orca/pull/8741). Both native architectures passed all seven - contract suites at the preceding head, then failed closed before extraction/build because Git for - Windows `gpgv` rejected an absolute drive-letter keyring path (E-M3-WINDOWS-CI-RED-003). The - cwd-relative correction is locally green under E-M3-WINDOWS-GPG-PATH-LOCAL-001 and awaits native - CI. This package may produce test artifacts but must not publish, resolve, transfer, install, - launch, or enable them. + contract suites, exact signed-input verification, native compilation, and bounded runtime/ZIP + assembly at exact CI head `42aa02fa9` (E-M3-WINDOWS-CI-RED-004). Both then time out during the + bundled smoke, while the parent currently suppresses bounded child stderr needed to classify the + failing PTY or watcher substage. 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, 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 @@ -90,7 +90,10 @@ same change as the work it records. Windows shebang parsing, authenticated-key checkout bytes, and POSIX-only mode assertions. At the corrected exact head, E-M3-WINDOWS-CI-RED-003 proves 20 passing and one intentionally skipped test on each native architecture, exact input download, and a common fail-closed Git-for-Windows - `gpgv` drive-letter keyring incompatibility. No Windows executable evidence is claimed. + `gpgv` drive-letter keyring incompatibility. E-M3-WINDOWS-CI-RED-004 proves that correction with + 21 passing and one intentionally skipped test, signed input acceptance, native compilation, and + runtime/ZIP assembly on each architecture; both 45-second bundled smoke commands then time out + without propagating their already bounded child stderr. No Windows executable cell is claimed. - Production behavior: unchanged; Orca embeds relay JavaScript and installs `node-pty` plus `@parcel/watcher` with remote npm. - New runtime assets published: none. @@ -103,13 +106,12 @@ same change as the work it records. - Rollout control: existing per-SSH-target configuration; legacy is the default and the bundled runtime is an explicit per-target Beta opt-in under E-M1-ROLLOUT-DECISION-001. - Legacy fallback removal: not authorized. -- Next required action: pass only cwd-relative verified-copy paths to `gpg`/`gpgv`, add a - discriminating command-boundary regression test, and collect exact Windows x64/arm64 plus POSIX - regression evidence from the corrected draft-PR head. A - same-head/same-runner clean-rebuild identity oracle for native build reproducibility remains a - separate open gate. Keep cross-family remote infrastructure, signing/trust, and measured legacy - baseline gates open; do not introduce publication, resolver, transfer, rollout, tuple enablement, - or default behavior. +- Next required action: preserve the smoke command's bounded stdout/stderr and timeout metadata in + the parent failure, add a discriminating diagnostic test, and rerun Windows x64 first to classify + the PTY/watcher failure before changing smoke behavior. A same-head/same-runner clean-rebuild + identity oracle for native build reproducibility remains a separate open gate. Keep cross-family + remote infrastructure, signing/trust, and measured legacy baseline gates open; do not introduce + publication, resolver, transfer, rollout, tuple enablement, or default behavior. ## Non-Negotiable Invariants @@ -2709,6 +2711,136 @@ or unexpected token`; the first logs did not identify a source location. - Follow-up: run the exact correction on Windows x64/arm64 and all four POSIX artifact jobs, then record any next discriminating native boundary without broadening this work package. +### E-M3-WINDOWS-CI-RED-004 — Native Windows assembly reached an opaque bounded smoke timeout + +- Date: 2026-07-14 +- Commit SHA / PR: exact workflow head `42aa02fa9eccf81e40d80f87461ddd232a1cda1f`, containing + implementation commit `b6903b220`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Run: [SSH Relay Runtime Artifacts 29340686444](https://github.com/stablyai/orca/actions/runs/29340686444), + conclusion `failure`; all four POSIX regression jobs passed and uploaded only unpublished + seven-day Actions artifacts. +- Native Windows jobs: + - x64 job `87111302431`: `windows-2022`, resolved `win22` image `20260706.237.1`, GitHub-hosted + X64, Windows Server 2022 Datacenter build 20348, Node v24.18.0; failed after 4m37s. + - arm64 job `87111302486`: `windows-11-arm`, resolved `win11-arm64` image `20260706.102.1`, + GitHub-hosted ARM64, Windows 10 Enterprise build 26200, Node v24.18.0; failed after 7m9s. +- Remote and transport: no SSH remote. Exact inputs came from `nodejs.org/dist/v24.18.0`; no release + publication or Windows artifact upload occurred. +- Exact commands: + + ```sh + gh run view 29340686444 --repo stablyai/orca \ + --json databaseId,headSha,status,conclusion,url,createdAt,updatedAt,jobs + gh api repos/stablyai/orca/actions/jobs/87111302431/logs | \ + rg -n -C 25 "SSH relay runtime verification failed|durationMs|contentId|Process completed with exit code" + gh api repos/stablyai/orca/actions/jobs/87111302486/logs | \ + rg -n -C 25 "SSH relay runtime verification failed|durationMs|contentId|Process completed with exit code" + gh api --paginate repos/stablyai/orca/actions/runs/29340686444/artifacts + ``` + +- Result: FAIL at the native bundled smoke on both architectures. Each runner passed all seven + contract suites with 21 passing and one intentionally skipped test, authenticated the exact Node + checksum/signature plus ZIP/headers/import library, compiled the patched native modules, assembled + and reinspected a 60-entry/42-file deterministic-format ZIP, and entered verification's smoke only + after archive/tree integrity checks. X64 produced a 37,212,065-byte ZIP expanding to 97,248,414 + bytes, content ID `90823b2c6bf7dad748a4399fe74fdfa8be82ef4a00d727e3a6be173f274fd43f`, + archive SHA-256 `ee7634dea179026b3ba8232294016d939e32edc34e1fd3697dd9e11722285d6a`, + and 146,511.617 ms build duration. Arm64 produced a 33,261,531-byte ZIP expanding to 86,189,740 + bytes, content ID `09ec2772bb8c47ee240fd226963b7e251a8060e7f84321cb24d2668bf993e2ca`, + archive SHA-256 `5cb50521ec88d491f1a4183fc13f0b0f688e242e290038c4d34e0125d8e8b2c2`, + and 176,108.728 ms build duration. +- Failure boundary: each bundled-Node smoke exceeded the parent command's 45-second timeout. The + child already bounds PTY and watcher waits at 15 seconds and writes its classified failure to + stderr, but `verify-ssh-relay-runtime.mjs` currently reports only the generic `execFile` stack. + The logs therefore cannot distinguish PTY, watcher, cleanup-handle, or combined failure. Upload + remained skipped. Smoke RSS, channel/file counts, and cancellation settlement were not emitted. +- Oracle proved: the GPG relative-path correction works through real Git-for-Windows GPG on native + x64 and arm64; signed-input gating, offline native build inputs, patched native compilation, + bundled Node v24.18.0 staging, ZIP construction/inspection, and pre-execution archive/tree gates + all precede the failing smoke. The four POSIX jobs remain green on the exact same source head. +- Does not prove: successful Windows PTY/ConPTY/watcher execution, which smoke substage failed, + prompt child-process settlement, ZIP clean-rebuild identity, durable Windows artifact bytes, + native trust/signing, oldest baseline, SSH, or any enabled tuple. +- Checklist items satisfied: native GPG path portability and pre-smoke Windows assembly progression + only. Windows per-tuple cells remain unchecked because executable smoke and artifact retention did + not pass. +- Follow-up: preserve bounded child stdout, stderr, timeout, exit, and signal details in the parent + diagnostic; add a failure-path unit test; rerun x64 first and change no PTY/watcher behavior until + the classified child failure is visible. + +### E-M3-WINDOWS-SMOKE-DIAGNOSTIC-RED-001 — Bounded child-detail propagation red gate + +- Date: 2026-07-14 +- Commit SHA / PR: uncommitted purpose-named regression test on exact head + `42aa02fa9eccf81e40d80f87461ddd232a1cda1f`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: macOS 26.2 arm64; Node v26.0.0 and pnpm 10.24.0 +- Remote and transport: none; synthetic child-process error object only +- Exact command: + `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-artifact.test.mjs` +- Result: expected FAIL; the new diagnostic test failed and three existing tests passed in 239 ms + because `formatSshRelayRuntimeSmokeFailure` did not exist. The required oracle supplies timeout, + exit/signal state, partial stdout, and a greater-than-limit stderr whose final classified failure + must survive explicit truncation. +- Oracle proved: existing parent diagnostics cannot satisfy the bounded failure-detail contract and + the new test discriminates both missing metadata and unbounded/error-tail-losing implementations. +- Does not prove: child execution, Windows behavior, the actual PTY/watcher failure, cleanup + settlement, or any tuple support. +- Checklist items satisfied: red half of the smoke-diagnostic correction only. +- Follow-up: implement a 64-KiB tail-preserving formatter around the already 4-MiB-bounded child + process, wrap `execFile` failure with it, and rerun focused/static gates before native x64 CI. + +### E-M3-WINDOWS-SMOKE-DIAGNOSTIC-LOCAL-001 — Bounded child-detail propagation correction + +- Date: 2026-07-14 +- Commit SHA / PR: uncommitted correction on exact base + `42aa02fa9eccf81e40d80f87461ddd232a1cda1f`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: macOS 26.2 arm64; Node v26.0.0 and pnpm 10.24.0. Exact-head Node 24 CI remains + authoritative. +- Remote and transport: none; synthetic child-process error plus local artifact contracts +- Exact commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-artifact.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 \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm run typecheck + pnpm exec oxlint \ + config/scripts/verify-ssh-relay-runtime.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs + pnpm exec oxlint + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html + git diff --check + ``` + +- Result: PASS. The focused artifact suite passed 4/4 tests in 194 ms; all seven artifact suites + passed 23/23 tests in 293 ms. Typecheck, focused and full oxlint, max-lines ratchet, full + lint/reliability/localization gates, plan formatting, and diff whitespace passed. Full lint + reported only existing warnings outside this package and used the recorded `GOMAXPROCS=2` + stability constraint. +- Oracle proved: any failed bundled-smoke child now propagates the declared 45-second parent timeout, + exit code, kill state, signal, message, stdout, and stderr; each stream retains at most its final + 64 KiB with an explicit omitted-byte count. The real command remains capped at 4 MiB and no + success-path parsing or smoke behavior changed. +- Does not prove: real child failure propagation on Windows, which PTY/watcher stage fails, + process-tree settlement after timeout, successful native smoke, or any tuple support. +- Checklist items satisfied: local green half of the bounded smoke-diagnostic correction only. +- Follow-up: rerun native Windows x64, capture the classified child tail, and make no execution-path + change until that evidence identifies the actual failure. + ### E-M3-RUNTIME-LOCAL-001 — First target-native Linux arm64 runtime artifact - Date: 2026-07-14 @@ -2944,18 +3076,18 @@ or unexpected token`; the first logs did not identify a source location. No product gap is accepted merely because it appears in this list. Each entry requires explicit owner and promotion condition. -| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | -| ------------------------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------ | -| Bundled runtime only partially implemented | Four unpublished POSIX native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | -| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | -| Windows runtime/zip builder absent | Windows targets remain entirely on the legacy path | POSIX proof cannot establish Windows behavior | Codex implementation owner | Bounded deterministic zip, Node/native build, signing, smoke, and live gates | Open | -| Native clean-rebuild identity unproved | One successful artifact per corrected-head hosted runner | Toolchain drift may change native content IDs | Codex implementation owner | Same-head, same-runner clean builds match or a reviewed reproducibility policy | Open | -| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | -| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | -| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | -| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | -| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | -| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | +| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | +| ------------------------------------------ | ----------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------ | +| Bundled runtime only partially implemented | Four unpublished POSIX native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | +| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | +| Windows runtime smoke incomplete | Native candidate ZIPs build, but smoke fails before artifact retention | Pre-smoke proof cannot establish relay behavior | Codex implementation owner | Classified passing PTY/watcher smoke, reproducibility, trust, and live gates | Open | +| Native clean-rebuild identity unproved | One successful artifact per corrected-head hosted runner | Toolchain drift may change native content IDs | Codex implementation owner | Same-head, same-runner clean builds match or a reviewed reproducibility policy | Open | +| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | +| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | +| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | +| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | +| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | +| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | ## Final Definition of Done From df661e370102b9b66c26f4c708e641abef0aa1ae Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 10:36:47 -0400 Subject: [PATCH 013/267] docs(ssh): record Windows smoke timeout gate Co-authored-by: Orca --- ...14-ssh-relay-github-release-implementation-checklist.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 37907dcd5e9..5be5d9a25cc 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 @@ -59,7 +59,8 @@ same change as the work it records. deploy/resolver call site is connected and no tuple is enabled. - Active package: Work Package 2 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` in stacked draft PR + corrections are implemented through `b6903b220`, with bounded smoke diagnostics in `3aab5aff8`, + in stacked draft PR [#8741](https://github.com/stablyai/orca/pull/8741). Both native architectures passed all seven contract suites, exact signed-input verification, native compilation, and bounded runtime/ZIP assembly at exact CI head `42aa02fa9` (E-M3-WINDOWS-CI-RED-004). Both then time out during the @@ -2772,7 +2773,7 @@ or unexpected token`; the first logs did not identify a source location. ### E-M3-WINDOWS-SMOKE-DIAGNOSTIC-RED-001 — Bounded child-detail propagation red gate - Date: 2026-07-14 -- Commit SHA / PR: uncommitted purpose-named regression test on exact head +- Commit SHA / PR: red test introduced before its implementation in `3aab5aff8`; exact base `42aa02fa9eccf81e40d80f87461ddd232a1cda1f`; draft PR [#8741](https://github.com/stablyai/orca/pull/8741) - Runner: macOS 26.2 arm64; Node v26.0.0 and pnpm 10.24.0 @@ -2794,7 +2795,7 @@ or unexpected token`; the first logs did not identify a source location. ### E-M3-WINDOWS-SMOKE-DIAGNOSTIC-LOCAL-001 — Bounded child-detail propagation correction - Date: 2026-07-14 -- Commit SHA / PR: uncommitted correction on exact base +- Commit SHA / PR: implementation commit `3aab5aff8` on exact base `42aa02fa9eccf81e40d80f87461ddd232a1cda1f`; draft PR [#8741](https://github.com/stablyai/orca/pull/8741) - Runner: macOS 26.2 arm64; Node v26.0.0 and pnpm 10.24.0. Exact-head Node 24 CI remains From ddf28eb8dba3ade4806082cfc5edd6526389cf94 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 10:52:54 -0400 Subject: [PATCH 014/267] test(ssh): settle Windows runtime PTY smoke Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../scripts/ssh-relay-runtime-pty-smoke.cjs | 127 +++++++++++++ .../ssh-relay-runtime-pty-smoke.test.mjs | 91 ++++++++++ .../scripts/ssh-relay-runtime-smoke-child.cjs | 97 +--------- .../ssh-relay-runtime-workflow.test.mjs | 1 + ...github-release-implementation-checklist.md | 168 ++++++++++++++++-- 6 files changed, 381 insertions(+), 105 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-pty-smoke.cjs create mode 100644 config/scripts/ssh-relay-runtime-pty-smoke.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 1c0b2b51349..731fe04f550 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -108,6 +108,7 @@ jobs: config/scripts/ssh-relay-node-tar-inspection.test.mjs \ config/scripts/ssh-relay-node-zip-inspection.test.mjs \ config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ config/scripts/ssh-relay-runtime-zip.test.mjs \ config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -249,6 +250,7 @@ jobs: config/scripts/ssh-relay-node-tar-inspection.test.mjs ` config/scripts/ssh-relay-node-zip-inspection.test.mjs ` config/scripts/ssh-relay-runtime-artifact.test.mjs ` + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs ` config/scripts/ssh-relay-runtime-windows-tree.test.mjs ` config/scripts/ssh-relay-runtime-zip.test.mjs ` config/scripts/ssh-relay-runtime-workflow.test.mjs diff --git a/config/scripts/ssh-relay-runtime-pty-smoke.cjs b/config/scripts/ssh-relay-runtime-pty-smoke.cjs new file mode 100644 index 00000000000..411e66242e7 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-pty-smoke.cjs @@ -0,0 +1,127 @@ +'use strict' + +function deadline(label, timeoutMs) { + let timeout + const promise = new Promise((_, reject) => { + timeout = setTimeout(() => reject(new Error(`${label} exceeded ${timeoutMs} ms`)), timeoutMs) + }) + return { promise, cancel: () => clearTimeout(timeout) } +} + +async function runSshRelayRuntimePtySmoke({ + nodePty, + runtimeRequire, + runtimeRoot, + platform = process.platform, + environment = process.env, + timeoutMs = 15_000 +}) { + const windows = platform === 'win32' + const executable = windows + ? 'powershell.exe' + : environment.SHELL && environment.SHELL.startsWith('/') + ? environment.SHELL + : '/bin/sh' + const arguments_ = windows + ? [ + '-NoLogo', + '-NoProfile', + '-Command', + "$ErrorActionPreference='Stop';" + + '[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false);' + + "Write-Output 'ORCA_PTY_READY';" + + '$line=[Console]::ReadLine();' + + '$size=$Host.UI.RawUI.WindowSize;' + + 'Write-Output ("ORCA_PTY_SIZE:{0}x{1}" -f $size.Width,$size.Height);' + + 'Write-Output ("ORCA_PTY_INPUT:{0}" -f $line);' + + 'exit 23' + ] + : [ + '-c', + 'printf "ORCA_PTY_READY\\n"; IFS= read -r line; stty size; printf "ORCA_PTY_INPUT:%s\\n" "$line"; exit 23' + ] + const terminal = nodePty.spawn(executable, arguments_, { + name: 'xterm-256color', + cols: 80, + rows: 24, + cwd: runtimeRoot, + env: environment, + useConpty: windows, + // Why: the relay uses the bundled ConPTY runtime to preserve terminal rendering behavior. + useConptyDll: windows + }) + let output = '' + let wroteInput = false + const outputSubscription = terminal.onData((data) => { + output += data + if (!wroteInput && output.includes('ORCA_PTY_READY')) { + wroteInput = true + terminal.resize(101, 37) + terminal.write('bounded-marker\r') + } + }) + let exitSubscription + let limitSubscription + const exited = new Promise((resolve, reject) => { + exitSubscription = terminal.onExit(resolve) + limitSubscription = terminal.onData(() => { + if (output.length > 1024 * 1024) { + reject(new Error('PTY smoke output exceeded 1 MiB')) + } + }) + }) + const timer = deadline('PTY smoke', timeoutMs) + let result + let cleanupStarted = false + function settlePtyResources() { + if (cleanupStarted) { + return + } + cleanupStarted = true + try { + outputSubscription.dispose() + exitSubscription.dispose() + limitSubscription.dispose() + } finally { + if (!result || windows) { + // Why: node-pty retains its Windows ConPTY worker handles after process exit until kill(). + terminal.kill() + } + } + } + try { + result = await Promise.race([exited, timer.promise]) + if ( + result.exitCode !== 23 || + !output.includes('ORCA_PTY_INPUT:bounded-marker') || + !output.includes(windows ? 'ORCA_PTY_SIZE:101x37' : '37 101') + ) { + throw new Error( + `PTY smoke mismatch: exit=${result.exitCode} output=${JSON.stringify(output)}` + ) + } + const { loadNativeModule } = runtimeRequire('node-pty/lib/utils') + const nativeNames = windows ? ['conpty', 'conpty_console_list'] : ['pty'] + const nativeDirectories = nativeNames.map((name) => loadNativeModule(name).dir) + if ( + nativeDirectories.some( + (directory) => !directory.replaceAll('\\', '/').includes('build/Release/') + ) + ) { + throw new Error( + `node-pty did not load patched build/Release artifacts: ${nativeDirectories.join(', ')}` + ) + } + return { + exitCode: result.exitCode, + resizedRows: 37, + resizedColumns: 101, + nativeDirectory: nativeDirectories.join(', ') + } + } finally { + timer.cancel() + settlePtyResources() + } +} + +module.exports = { runSshRelayRuntimePtySmoke } diff --git a/config/scripts/ssh-relay-runtime-pty-smoke.test.mjs b/config/scripts/ssh-relay-runtime-pty-smoke.test.mjs new file mode 100644 index 00000000000..bd0d375f14f --- /dev/null +++ b/config/scripts/ssh-relay-runtime-pty-smoke.test.mjs @@ -0,0 +1,91 @@ +import { createRequire } from 'node:module' + +import { describe, expect, it, vi } from 'vitest' + +const require = createRequire(import.meta.url) +const { runSshRelayRuntimePtySmoke } = require('./ssh-relay-runtime-pty-smoke.cjs') + +function successfulTerminalFixture({ windows }) { + const dataListeners = [] + const exitListeners = [] + const subscriptions = [] + const terminal = { + kill: vi.fn(), + resize: vi.fn(), + write: vi.fn(), + onData: vi.fn((listener) => { + dataListeners.push(listener) + const subscription = { dispose: vi.fn() } + subscriptions.push(subscription) + return subscription + }), + onExit: vi.fn((listener) => { + exitListeners.push(listener) + const subscription = { dispose: vi.fn() } + subscriptions.push(subscription) + return subscription + }) + } + const nodePty = { + spawn: vi.fn(() => { + queueMicrotask(() => { + const output = windows + ? 'ORCA_PTY_READY\r\nORCA_PTY_SIZE:101x37\r\nORCA_PTY_INPUT:bounded-marker\r\n' + : 'ORCA_PTY_READY\n37 101\nORCA_PTY_INPUT:bounded-marker\n' + dataListeners.forEach((listener) => listener(output)) + exitListeners.forEach((listener) => listener({ exitCode: 23, signal: 0 })) + }) + return terminal + }) + } + return { nodePty, subscriptions, terminal } +} + +function runtimeRequire() { + return { + loadNativeModule: () => ({ dir: '../build/Release/' }) + } +} + +describe('SSH relay runtime PTY smoke lifecycle', () => { + it('releases successful Windows ConPTY resources after validating the exit', async () => { + const fixture = successfulTerminalFixture({ windows: true }) + + await expect( + runSshRelayRuntimePtySmoke({ + nodePty: fixture.nodePty, + runtimeRequire, + runtimeRoot: 'C:\\runtime', + platform: 'win32', + environment: {}, + timeoutMs: 100 + }) + ).resolves.toMatchObject({ exitCode: 23, resizedColumns: 101, resizedRows: 37 }) + + expect(fixture.terminal.kill).toHaveBeenCalledOnce() + expect(fixture.subscriptions).toHaveLength(3) + fixture.subscriptions.forEach((subscription) => { + expect(subscription.dispose).toHaveBeenCalledOnce() + }) + }) + + it('disposes successful POSIX listeners without killing an exited terminal', async () => { + const fixture = successfulTerminalFixture({ windows: false }) + + await expect( + runSshRelayRuntimePtySmoke({ + nodePty: fixture.nodePty, + runtimeRequire, + runtimeRoot: '/runtime', + platform: 'linux', + environment: { SHELL: '/bin/sh' }, + timeoutMs: 100 + }) + ).resolves.toMatchObject({ exitCode: 23, resizedColumns: 101, resizedRows: 37 }) + + expect(fixture.terminal.kill).not.toHaveBeenCalled() + fixture.subscriptions.forEach((subscription) => { + expect(subscription.dispose).toHaveBeenCalledOnce() + }) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-smoke-child.cjs b/config/scripts/ssh-relay-runtime-smoke-child.cjs index 75e20e61415..b092b793905 100644 --- a/config/scripts/ssh-relay-runtime-smoke-child.cjs +++ b/config/scripts/ssh-relay-runtime-smoke-child.cjs @@ -5,6 +5,8 @@ const { tmpdir } = require('node:os') const { join } = require('node:path') const { createRequire } = require('node:module') +const { runSshRelayRuntimePtySmoke } = require('./ssh-relay-runtime-pty-smoke.cjs') + const runtimeRoot = process.argv[2] if (!runtimeRoot) { throw new Error('runtime root argument is required') @@ -22,96 +24,6 @@ function deadline(label) { return { promise, cancel: () => clearTimeout(timeout) } } -async function ptySmoke() { - const windows = process.platform === 'win32' - const executable = windows - ? 'powershell.exe' - : process.env.SHELL && process.env.SHELL.startsWith('/') - ? process.env.SHELL - : '/bin/sh' - const arguments_ = windows - ? [ - '-NoLogo', - '-NoProfile', - '-Command', - "$ErrorActionPreference='Stop';" + - '[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false);' + - "Write-Output 'ORCA_PTY_READY';" + - '$line=[Console]::ReadLine();' + - '$size=$Host.UI.RawUI.WindowSize;' + - 'Write-Output ("ORCA_PTY_SIZE:{0}x{1}" -f $size.Width,$size.Height);' + - 'Write-Output ("ORCA_PTY_INPUT:{0}" -f $line);' + - 'exit 23' - ] - : [ - '-c', - 'printf "ORCA_PTY_READY\\n"; IFS= read -r line; stty size; printf "ORCA_PTY_INPUT:%s\\n" "$line"; exit 23' - ] - const terminal = nodePty.spawn(executable, arguments_, { - name: 'xterm-256color', - cols: 80, - rows: 24, - cwd: runtimeRoot, - env: process.env, - useConpty: windows, - // Why: the relay uses the bundled ConPTY runtime to preserve terminal rendering behavior. - useConptyDll: windows - }) - let output = '' - let wroteInput = false - terminal.onData((data) => { - output += data - if (!wroteInput && output.includes('ORCA_PTY_READY')) { - wroteInput = true - terminal.resize(101, 37) - terminal.write('bounded-marker\r') - } - }) - const exited = new Promise((resolve, reject) => { - terminal.onExit(resolve) - terminal.onData(() => { - if (output.length > 1024 * 1024) { - reject(new Error('PTY smoke output exceeded 1 MiB')) - } - }) - }) - const timer = deadline('PTY smoke') - let result - try { - result = await Promise.race([exited, timer.promise]) - } finally { - timer.cancel() - if (!result) { - terminal.kill() - } - } - if ( - result.exitCode !== 23 || - !output.includes('ORCA_PTY_INPUT:bounded-marker') || - !output.includes(windows ? 'ORCA_PTY_SIZE:101x37' : '37 101') - ) { - throw new Error(`PTY smoke mismatch: exit=${result.exitCode} output=${JSON.stringify(output)}`) - } - const { loadNativeModule } = runtimeRequire('node-pty/lib/utils') - const nativeNames = windows ? ['conpty', 'conpty_console_list'] : ['pty'] - const nativeDirectories = nativeNames.map((name) => loadNativeModule(name).dir) - if ( - nativeDirectories.some( - (directory) => !directory.replaceAll('\\', '/').includes('build/Release/') - ) - ) { - throw new Error( - `node-pty did not load patched build/Release artifacts: ${nativeDirectories.join(', ')}` - ) - } - return { - exitCode: result.exitCode, - resizedRows: 37, - resizedColumns: 101, - nativeDirectory: nativeDirectories.join(', ') - } -} - async function waitFor(events, predicate, label) { const timer = deadline(label) try { @@ -177,7 +89,10 @@ async function watcherSmoke() { async function main() { const started = process.hrtime.bigint() - const [pty, watched] = await Promise.all([ptySmoke(), watcherSmoke()]) + const [pty, watched] = await Promise.all([ + runSshRelayRuntimePtySmoke({ nodePty, runtimeRequire, runtimeRoot }), + watcherSmoke() + ]) const durationMs = Number(process.hrtime.bigint() - started) / 1e6 process.stdout.write( `${JSON.stringify({ nodeVersion: process.version, modulesAbi: process.versions.modules, pty, watcher: watched, durationMs, rssBytes: process.memoryUsage().rss })}\n` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 4ef61105223..53e3362df21 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -59,6 +59,7 @@ describe('SSH relay runtime artifact workflow', () => { expect(source).toContain('cp "$output"/*.tar.xz') expect(source).toContain("Get-ChildItem -LiteralPath $output -Filter '*.zip'") expect(source).toContain('ssh-relay-node-zip-inspection.test.mjs') + expect(source).toContain('ssh-relay-runtime-pty-smoke.test.mjs') expect(source).toContain('ssh-relay-runtime-zip.test.mjs') expect(source).toContain('node-v24.18.0-headers.tar.gz') expect(source).toContain('node_library: win-x64/node.lib') 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 5be5d9a25cc..d83e11de41e 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — four POSIX native-runner artifact builds are CI-green; Windows signed-input verification and runtime/ZIP assembly reach native smoke on both architectures, but bounded smoke diagnostics/execution, reproducibility, oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — correct prompt Windows PTY smoke-process settlement after functionally successful native x64 and arm64 execution; exact-head green CI, reproducibility, oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -61,12 +61,15 @@ same change as the work it records. smoke, SBOM, and provenance only. The Windows x64/arm64 artifact candidate and portability corrections are implemented through `b6903b220`, with bounded smoke diagnostics in `3aab5aff8`, in stacked draft PR - [#8741](https://github.com/stablyai/orca/pull/8741). Both native architectures passed all seven - contract suites, exact signed-input verification, native compilation, and bounded runtime/ZIP - assembly at exact CI head `42aa02fa9` (E-M3-WINDOWS-CI-RED-004). Both then time out during the - bundled smoke, while the parent currently suppresses bounded child stderr needed to classify the - failing PTY or watcher substage. This package may produce test artifacts but must not publish, - resolve, transfer, install, launch, or enable them. + [#8741](https://github.com/stablyai/orca/pull/8741). Exact-head run + [29341643555](https://github.com/stablyai/orca/actions/runs/29341643555) proves the bundled Node, + patched PTY input/resize/exit, and watcher lifecycle complete on x64 in 3,357.413 ms with + 56,905,728-byte RSS and arm64 in 6,593.663 ms with 54,505,472-byte RSS, but each child retains a + Windows handle until the parent kills it at its 45-second bound. The active slice is a + discriminating PTY-listener/terminal-cleanup contract followed by explicit prompt ConPTY + settlement; the injected lifecycle contract and repository static gates are locally green, while + exact-head native execution remains mandatory. 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, 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 @@ -578,6 +581,11 @@ archive inspection, bundled-Node/native-module/PTY/watcher smoke, SBOM, and prov behind purpose-named scripts and CI artifacts. No release publication, desktop resolver, SSH transfer, install, fallback, rollout setting, or tuple enablement belongs in this package. +**Active sub-gate — 2026-07-14, Codex implementation owner:** the discriminating lifecycle test and +explicit listener/ConPTY cleanup are locally green without `process.exit()` or a weakened 45-second +bound. Exact-head native x64/arm64 CI must now prove normal child settlement and artifact retention; +the prior common failure is run 29341643555/jobs 87114639412 and 87114639449. + Each runtime must contain only the executable closure required by the relay. - [ ] Replace or extend `config/scripts/build-relay.mjs` without weakening its existing relay and @@ -1231,6 +1239,7 @@ focused commands as their scripts/tests are introduced. - [x] `node config/scripts/verify-ssh-relay-node-release-inputs.mjs --inputs-directory --archive linux-x64-glibc` (E-M3-NODE-PROVENANCE-001; metadata and archive verification only) - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-node-zip-inspection.test.mjs config/scripts/ssh-relay-runtime-zip.test.mjs` (E-M3-WINDOWS-ZIP-RED-001, E-M3-WINDOWS-INPUT-001; synthetic ZIP/input contracts only) - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-windows-tree.test.mjs` (E-M3-WINDOWS-LOCAL-002; structural Windows ConPTY closure only, no execution) +- [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-pty-smoke.test.mjs` (E-M3-WINDOWS-SMOKE-SETTLEMENT-LOCAL-RED-001, E-M3-WINDOWS-SMOKE-SETTLEMENT-LOCAL-001; native evidence remains pending) - [x] `node config/scripts/verify-ssh-relay-node-release-inputs.mjs --inputs-directory --archive win32-x64` (E-M3-WINDOWS-INPUT-001; signed real Node ZIP, headers, and import library; no Windows execution) - [x] `node config/scripts/build-ssh-relay-runtime.mjs --tuple linux-arm64-glibc --inputs-directory --output-directory --source-date-epoch --git-commit ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) - [x] `node config/scripts/verify-ssh-relay-runtime.mjs --runtime-directory --identity --archive ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) @@ -2842,6 +2851,136 @@ or unexpected token`; the first logs did not identify a source location. - Follow-up: rerun native Windows x64, capture the classified child tail, and make no execution-path change until that evidence identifies the actual failure. +### E-M3-WINDOWS-SMOKE-SETTLEMENT-CI-RED-001 — Native Windows smoke succeeds but does not settle + +- Date: 2026-07-14 +- Commit SHA / PR: exact workflow head `df661e370102b9b66c26f4c708e641abef0aa1ae`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runners and jobs: GitHub Actions run + [29341643555](https://github.com/stablyai/orca/actions/runs/29341643555); Windows x64 job + [87114639412](https://github.com/stablyai/orca/actions/runs/29341643555/job/87114639412), + `windows-2022` image `20260706.237.1`, Windows Server 2022 10.0.20348, native x64, Node + v24.18.0; Windows arm64 job + [87114639449](https://github.com/stablyai/orca/actions/runs/29341643555/job/87114639449), + `windows-11-arm64` image `20260706.102.1`, Windows 11 Enterprise 10.0.26200, native arm64, + Node v24.18.0 +- Remote and transport: no SSH remote; target-native artifact build and bundled-runtime execution + on the hosted runner; exact signed inputs downloaded from pinned nodejs.org URLs +- Exact command: + + ```sh + gh api repos/stablyai/orca/actions/jobs/87114639412/logs | \ + rg -n -C 16 'Bundled runtime smoke command failed|timeoutMs=|Image:|Version:' + gh api repos/stablyai/orca/actions/jobs/87114639449/logs | \ + rg -n -C 16 'Bundled runtime smoke command failed|timeoutMs=|Image:|Version:' + ``` + +- Result: expected discriminating FAIL. The exact signed inputs, offline native build, archive and + complete-tree verification all passed. Bundled Node v24.18.0/ABI 137 then completed the patched + ConPTY smoke with exit 23, 101×37 resize, input marker, and both native modules loaded from + `build/Release`; watcher create, update, rename-as-delete/create, and delete also completed. The + x64 child emitted valid JSON after 3,357.413 ms with 56,905,728-byte RSS; arm64 emitted the same + functional result after 6,593.663 ms with 54,505,472-byte RSS. Both remained alive until the + parent killed them at 45 seconds (`code=null`, `killed=true`, `signal="SIGTERM"`, empty stderr). +- Artifact metrics: x64 content ID + `sha256:8cf9eb4e04459e25d710a28235a57e777d39f65168841c2408ab658243a21d94`; ZIP + 37,212,068 bytes with SHA-256 + `sha256:11f83971ed20cde7be41aa2f753cfd4350cd4a70a3fe46a27540ebc872c4ca56`; build + 151,226.208 ms. Arm64 content ID + `sha256:dda60b6237b320c74236f57bece072d0f847479eb322f43f22c829daf2ce9bf6`; ZIP + 33,261,533 bytes with SHA-256 + `sha256:f3fefed7b959fb6c3945170e8f0f7d2766cf26240aef9928b0e25770e5b19a26`; 86,189,740 + expanded bytes; build 248,307.259 ms. Uploads remained skipped because prompt process settlement + is part of executable proof. +- Oracle proved: Windows x64 and arm64 PTY/ConPTY and watcher functionality succeeds through each + exact bundled runtime, and the common remaining failure occurs after successful JSON emission in + child-process handle settlement rather than in PTY, watcher, archive, or tree validation. +- Does not prove: prompt cleanup, durable artifact upload, clean-rebuild identity, native + trust/signing, oldest baseline, SSH, or any enabled tuple. Both per-tuple rows remain unchecked + until the child exits normally and exact-head CI retains each artifact. +- Follow-up: retain and dispose the PTY listener subscriptions, call the supported Windows terminal + cleanup after a validated successful exit, and prove normal child settlement without + `process.exit()` on both native architectures. + +### E-M3-WINDOWS-SMOKE-SETTLEMENT-LOCAL-RED-001 — PTY lifecycle cleanup contract red gate + +- Date: 2026-07-14 +- Commit SHA / PR: uncommitted contract test and behavior-preserving PTY-smoke module extraction on + exact base `df661e370102b9b66c26f4c708e641abef0aa1ae`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: macOS 26.2 arm64; Node v26.0.0 and pnpm 10.24.0 +- Remote and transport: none; injected terminal/listener lifecycle only +- Exact command: + `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-pty-smoke.test.mjs` +- Result: expected FAIL in 122 ms. The Windows success case returned a valid exit result but called + `terminal.kill()` zero times; the POSIX success case and Windows case both disposed zero of their + three `onData`/`onExit` subscriptions. Two tests failed, with no timeout or synthetic forced exit. +- Oracle proved: the current success path lacks the exact listener and ConPTY cleanup operations + required by the native x64 diagnostic, and the purpose-named suite distinguishes Windows-only + terminal cleanup from platform-neutral listener disposal. +- Does not prove: the green implementation, actual Windows handle release, timeout/error cleanup, + full child-process settlement, arm64 behavior, or any tuple support. +- Checklist items satisfied: red half of the Windows PTY smoke-settlement correction only. +- Follow-up: add idempotent finally cleanup, preserve successful POSIX no-kill behavior, rerun this + suite and all artifact/static gates, then require exact-head native Windows CI. + +### E-M3-WINDOWS-SMOKE-SETTLEMENT-LOCAL-001 — Explicit PTY resource settlement + +- Date: 2026-07-14 +- Commit SHA / PR: uncommitted implementation on exact base + `df661e370102b9b66c26f4c708e641abef0aa1ae`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: macOS 26.2 arm64; Node v26.0.0 and pnpm 10.24.0. Exact-head Node 24 native CI remains + authoritative. +- Remote and transport: none; injected terminal lifecycle plus local source/static verification +- Exact commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-pty-smoke.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 \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm run typecheck + pnpm exec oxlint \ + config/scripts/ssh-relay-runtime-smoke-child.cjs \ + config/scripts/ssh-relay-runtime-pty-smoke.cjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check .github/workflows/ssh-relay-runtime-artifacts.yml \ + config/scripts/ssh-relay-runtime-smoke-child.cjs \ + config/scripts/ssh-relay-runtime-pty-smoke.cjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md + git diff --check + ``` + +- Result: PASS. The lifecycle suite passed 2/2 tests in 117 ms; the eight artifact suites passed + 25/25 tests in 417 ms. Typecheck, focused oxlint, max-lines ratchet, full + lint/reliability/localization checks, formatting, and diff whitespace passed. Full lint reported + only pre-existing warnings outside this package. +- Oracle proved: the success path retains and disposes both data subscriptions and the exit + subscription exactly once; a validated Windows exit additionally calls the supported node-pty + terminal cleanup exactly once, while a successful POSIX exit does not kill its terminal. Cleanup + is in `finally`, the existing timeout/error path still kills the terminal, and no `process.exit()` + was introduced. Both native workflow families now run the lifecycle contract. +- Does not prove: actual Windows ConPTY handle release, prompt bundled child-process exit, exact-head + Node 24 behavior, artifact retention, clean-rebuild identity, native trust, oldest baseline, SSH, + or any enabled tuple. Both Windows per-tuple cells remain unchecked. +- Checklist items satisfied: local green half of the PTY smoke-settlement correction and its + purpose-named verification-command inventory only. +- Follow-up: commit and push the isolated correction, run both target-native Windows jobs at the + exact head, and require normal smoke completion plus artifact upload before advancing. + ### E-M3-RUNTIME-LOCAL-001 — First target-native Linux arm64 runtime artifact - Date: 2026-07-14 @@ -3081,7 +3220,7 @@ owner and promotion condition. | ------------------------------------------ | ----------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------ | | Bundled runtime only partially implemented | Four unpublished POSIX native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | | No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | -| Windows runtime smoke incomplete | Native candidate ZIPs build, but smoke fails before artifact retention | Pre-smoke proof cannot establish relay behavior | Codex implementation owner | Classified passing PTY/watcher smoke, reproducibility, trust, and live gates | Open | +| Windows runtime smoke incomplete | Both native smokes work but prior children hung; local cleanup awaits CI | No prompt settlement or retained artifact proof | Codex implementation owner | Exact-head clean exit/upload, reproducibility, trust, and live gates | Open | | Native clean-rebuild identity unproved | One successful artifact per corrected-head hosted runner | Toolchain drift may change native content IDs | Codex implementation owner | Same-head, same-runner clean builds match or a reviewed reproducibility policy | Open | | Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | | Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | @@ -3129,9 +3268,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Implement bounded deterministic Windows ZIP/native assembly as its own evidence-gated slice, add -native Windows x64/arm64 jobs without publishing assets, and add a same-head/same-runner clean-build -identity comparison for native reproducibility. The cross-family Layer B target pool, protected -manifest-signing environment, and paired legacy performance baseline remain release/default-path -blockers; no publication, desktop resolver, SSH transfer/install, per-target Beta, fallback, tuple -enablement, or default behavior may be connected by this package. +Commit and push the isolated PTY smoke-settlement correction, then require exact-head Windows +x64/arm64 jobs to exit normally and upload their unpublished evidence artifacts. If both pass, record +their exact hashes, sizes, smoke/build durations, RSS, runner images, and residual gaps before the +separate same-head/same-runner clean-build identity gate. The cross-family Layer B target pool, +protected manifest-signing environment, and paired legacy performance baseline remain +release/default-path blockers; no publication, desktop resolver, SSH transfer/install, per-target +Beta, fallback, tuple enablement, or default behavior may be connected by this package. From b6291e55b1781b4c4e6f46a4965152f4a0d2334f Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 10:53:50 -0400 Subject: [PATCH 015/267] docs(ssh): record Windows smoke settlement gate Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) 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 d83e11de41e..511dc8fa70b 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 @@ -60,7 +60,7 @@ same change as the work it records. - Active package: Work Package 2 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`, - in stacked draft PR + and explicit PTY smoke settlement in `ddf28eb8d`, in stacked draft PR [#8741](https://github.com/stablyai/orca/pull/8741). Exact-head run [29341643555](https://github.com/stablyai/orca/actions/runs/29341643555) proves the bundled Node, patched PTY input/resize/exit, and watcher lifecycle complete on x64 in 3,357.413 ms with @@ -1182,16 +1182,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; four POSIX jobs green; Windows active | `151628992`; E-M3-NODE-RED-001, E-M3-NODE-PROVENANCE-001, E-M3-RUNTIME-LOCAL-001, E-M3-STATIC-001, E-M3-CI-RED-001, E-M3-CI-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; Windows settlement CI pending | `ddf28eb8d`; E-M3-NODE-RED-001, E-M3-NODE-PROVENANCE-001, E-M3-RUNTIME-LOCAL-001, E-M3-STATIC-001, E-M3-CI-001, E-M3-WINDOWS-SMOKE-SETTLEMENT-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 | — | Every PR must document: @@ -2927,8 +2927,8 @@ or unexpected token`; the first logs did not identify a source location. ### E-M3-WINDOWS-SMOKE-SETTLEMENT-LOCAL-001 — Explicit PTY resource settlement - Date: 2026-07-14 -- Commit SHA / PR: uncommitted implementation on exact base - `df661e370102b9b66c26f4c708e641abef0aa1ae`; draft PR +- Commit SHA / PR: implementation commit + `ddf28eb8dba3ade4806082cfc5edd6526389cf94`; draft PR [#8741](https://github.com/stablyai/orca/pull/8741) - Runner: macOS 26.2 arm64; Node v26.0.0 and pnpm 10.24.0. Exact-head Node 24 native CI remains authoritative. @@ -3216,18 +3216,18 @@ or unexpected token`; the first logs did not identify a source location. No product gap is accepted merely because it appears in this list. Each entry requires explicit owner and promotion condition. -| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | -| ------------------------------------------ | ----------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------ | -| Bundled runtime only partially implemented | Four unpublished POSIX native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | -| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | +| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | +| ------------------------------------------ | ------------------------------------------------------------------------ | ----------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------ | +| Bundled runtime only partially implemented | Four unpublished POSIX native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | +| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | | Windows runtime smoke incomplete | Both native smokes work but prior children hung; local cleanup awaits CI | No prompt settlement or retained artifact proof | Codex implementation owner | Exact-head clean exit/upload, reproducibility, trust, and live gates | Open | -| Native clean-rebuild identity unproved | One successful artifact per corrected-head hosted runner | Toolchain drift may change native content IDs | Codex implementation owner | Same-head, same-runner clean builds match or a reviewed reproducibility policy | Open | -| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | -| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | -| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | -| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | -| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | -| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | +| Native clean-rebuild identity unproved | One successful artifact per corrected-head hosted runner | Toolchain drift may change native content IDs | Codex implementation owner | Same-head, same-runner clean builds match or a reviewed reproducibility policy | Open | +| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | +| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | +| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | +| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | +| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | +| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | ## Final Definition of Done From 6bba900209543a53b7673ca12173159d40d9fc87 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 11:05:09 -0400 Subject: [PATCH 016/267] test(ssh): classify lingering Windows smoke resources Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + ...ssh-relay-runtime-resource-diagnostics.cjs | 33 ++++ ...elay-runtime-resource-diagnostics.test.mjs | 45 +++++ .../scripts/ssh-relay-runtime-smoke-child.cjs | 4 +- .../ssh-relay-runtime-workflow.test.mjs | 1 + ...github-release-implementation-checklist.md | 173 ++++++++++++++---- 6 files changed, 220 insertions(+), 38 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-resource-diagnostics.cjs create mode 100644 config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 731fe04f550..f310da29fd5 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -109,6 +109,7 @@ jobs: config/scripts/ssh-relay-node-zip-inspection.test.mjs \ config/scripts/ssh-relay-runtime-artifact.test.mjs \ config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ config/scripts/ssh-relay-runtime-zip.test.mjs \ config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -251,6 +252,7 @@ jobs: config/scripts/ssh-relay-node-zip-inspection.test.mjs ` config/scripts/ssh-relay-runtime-artifact.test.mjs ` config/scripts/ssh-relay-runtime-pty-smoke.test.mjs ` + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs ` config/scripts/ssh-relay-runtime-windows-tree.test.mjs ` config/scripts/ssh-relay-runtime-zip.test.mjs ` config/scripts/ssh-relay-runtime-workflow.test.mjs diff --git a/config/scripts/ssh-relay-runtime-resource-diagnostics.cjs b/config/scripts/ssh-relay-runtime-resource-diagnostics.cjs new file mode 100644 index 00000000000..2b23d03ff5a --- /dev/null +++ b/config/scripts/ssh-relay-runtime-resource-diagnostics.cjs @@ -0,0 +1,33 @@ +'use strict' + +const DEFAULT_OBSERVATION_MS = 2_000 +const MAX_RESOURCE_TYPES = 256 + +function boundedActiveResources(getActiveResourcesInfo) { + const resources = getActiveResourcesInfo().map((resource) => String(resource).slice(0, 128)) + return { + types: resources.slice(0, MAX_RESOURCE_TYPES), + omitted: Math.max(0, resources.length - MAX_RESOURCE_TYPES) + } +} + +async function observeWindowsResourceSettlement({ + platform = process.platform, + getActiveResourcesInfo = () => process.getActiveResourcesInfo(), + delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)), + observationMs = DEFAULT_OBSERVATION_MS +} = {}) { + if (platform !== 'win32') { + return null + } + const immediatelyAfterSmoke = boundedActiveResources(getActiveResourcesInfo) + // Why: ConPTY cleanup is delayed for output draining; observe only resources that survive it. + await delay(observationMs) + return { + observationMs, + immediatelyAfterSmoke, + afterObservation: boundedActiveResources(getActiveResourcesInfo) + } +} + +module.exports = { observeWindowsResourceSettlement } diff --git a/config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs b/config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs new file mode 100644 index 00000000000..cc19e35b508 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs @@ -0,0 +1,45 @@ +import { createRequire } from 'node:module' + +import { describe, expect, it, vi } from 'vitest' + +const require = createRequire(import.meta.url) +const { observeWindowsResourceSettlement } = require('./ssh-relay-runtime-resource-diagnostics.cjs') + +describe('SSH relay runtime resource diagnostics', () => { + it('observes bounded Windows resources after the cleanup drain interval', async () => { + const delay = vi.fn(async () => {}) + const getActiveResourcesInfo = vi + .fn() + .mockReturnValueOnce(['PipeWrap', 'Worker', ...Array.from({ length: 300 }, () => 'Timeout')]) + .mockReturnValueOnce(['PipeWrap', 'Worker']) + + await expect( + observeWindowsResourceSettlement({ + platform: 'win32', + getActiveResourcesInfo, + delay, + observationMs: 2_000 + }) + ).resolves.toEqual({ + observationMs: 2_000, + immediatelyAfterSmoke: { + types: ['PipeWrap', 'Worker', ...Array.from({ length: 254 }, () => 'Timeout')], + omitted: 46 + }, + afterObservation: { types: ['PipeWrap', 'Worker'], omitted: 0 } + }) + expect(delay).toHaveBeenCalledOnce() + expect(delay).toHaveBeenCalledWith(2_000) + }) + + it('does not add an observation delay to successful POSIX smoke', async () => { + const delay = vi.fn(async () => {}) + const getActiveResourcesInfo = vi.fn(() => ['PipeWrap']) + + await expect( + observeWindowsResourceSettlement({ platform: 'linux', getActiveResourcesInfo, delay }) + ).resolves.toBeNull() + expect(delay).not.toHaveBeenCalled() + expect(getActiveResourcesInfo).not.toHaveBeenCalled() + }) +}) diff --git a/config/scripts/ssh-relay-runtime-smoke-child.cjs b/config/scripts/ssh-relay-runtime-smoke-child.cjs index b092b793905..77ad0247169 100644 --- a/config/scripts/ssh-relay-runtime-smoke-child.cjs +++ b/config/scripts/ssh-relay-runtime-smoke-child.cjs @@ -6,6 +6,7 @@ const { join } = require('node:path') const { createRequire } = require('node:module') const { runSshRelayRuntimePtySmoke } = require('./ssh-relay-runtime-pty-smoke.cjs') +const { observeWindowsResourceSettlement } = require('./ssh-relay-runtime-resource-diagnostics.cjs') const runtimeRoot = process.argv[2] if (!runtimeRoot) { @@ -93,9 +94,10 @@ async function main() { runSshRelayRuntimePtySmoke({ nodePty, runtimeRequire, runtimeRoot }), watcherSmoke() ]) + const resourceSettlement = await observeWindowsResourceSettlement() const durationMs = Number(process.hrtime.bigint() - started) / 1e6 process.stdout.write( - `${JSON.stringify({ nodeVersion: process.version, modulesAbi: process.versions.modules, pty, watcher: watched, durationMs, rssBytes: process.memoryUsage().rss })}\n` + `${JSON.stringify({ nodeVersion: process.version, modulesAbi: process.versions.modules, pty, watcher: watched, resourceSettlement, durationMs, rssBytes: process.memoryUsage().rss })}\n` ) } diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 53e3362df21..3fc07e0ff8d 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -60,6 +60,7 @@ describe('SSH relay runtime artifact workflow', () => { expect(source).toContain("Get-ChildItem -LiteralPath $output -Filter '*.zip'") expect(source).toContain('ssh-relay-node-zip-inspection.test.mjs') expect(source).toContain('ssh-relay-runtime-pty-smoke.test.mjs') + expect(source).toContain('ssh-relay-runtime-resource-diagnostics.test.mjs') expect(source).toContain('ssh-relay-runtime-zip.test.mjs') expect(source).toContain('node-v24.18.0-headers.tar.gz') expect(source).toContain('node_library: win-x64/node.lib') 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 511dc8fa70b..9bb1c183b94 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — correct prompt Windows PTY smoke-process settlement after functionally successful native x64 and arm64 execution; exact-head green CI, reproducibility, oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — classify the Windows resource retained after explicit PTY listener/terminal cleanup still failed to settle functionally successful native x64 and arm64 children; exact-head green CI, reproducibility, oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -67,9 +67,10 @@ same change as the work it records. 56,905,728-byte RSS and arm64 in 6,593.663 ms with 54,505,472-byte RSS, but each child retains a Windows handle until the parent kills it at its 45-second bound. The active slice is a discriminating PTY-listener/terminal-cleanup contract followed by explicit prompt ConPTY - settlement; the injected lifecycle contract and repository static gates are locally green, while - exact-head native execution remains mandatory. This package may produce test artifacts but must - not publish, resolve, transfer, install, launch, or enable them. + settlement; the injected lifecycle contract and repository static gates are locally green, but + exact-head run 29342918504 proves the native x64 child still retains an unclassified resource + after valid JSON. 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, 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 @@ -581,10 +582,11 @@ archive inspection, bundled-Node/native-module/PTY/watcher smoke, SBOM, and prov behind purpose-named scripts and CI artifacts. No release publication, desktop resolver, SSH transfer, install, fallback, rollout setting, or tuple enablement belongs in this package. -**Active sub-gate — 2026-07-14, Codex implementation owner:** the discriminating lifecycle test and -explicit listener/ConPTY cleanup are locally green without `process.exit()` or a weakened 45-second -bound. Exact-head native x64/arm64 CI must now prove normal child settlement and artifact retention; -the prior common failure is run 29341643555/jobs 87114639412 and 87114639449. +**Active sub-gate — 2026-07-14, Codex implementation owner:** exact-head x64 run +29342918504/job 87119092333 still timed out after functional smoke despite explicit PTY cleanup; +arm64 job 87119092293 reproduced it. Add bounded post-smoke active-resource diagnostics before any +second behavioral change, preserving the existing 45-second parent bound and avoiding +`process.exit()`. Each runtime must contain only the executable closure required by the relay. @@ -1182,16 +1184,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; Windows settlement CI pending | `ddf28eb8d`; E-M3-NODE-RED-001, E-M3-NODE-PROVENANCE-001, E-M3-RUNTIME-LOCAL-001, E-M3-STATIC-001, E-M3-CI-001, E-M3-WINDOWS-SMOKE-SETTLEMENT-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; resource diagnostic pending | `ddf28eb8d`; E-M3-NODE-RED-001, E-M3-NODE-PROVENANCE-001, E-M3-RUNTIME-LOCAL-001, E-M3-CI-001, E-M3-WINDOWS-SMOKE-SETTLEMENT-CI-RED-002 | +| 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 | — | Every PR must document: @@ -1240,6 +1242,7 @@ focused commands as their scripts/tests are introduced. - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-node-zip-inspection.test.mjs config/scripts/ssh-relay-runtime-zip.test.mjs` (E-M3-WINDOWS-ZIP-RED-001, E-M3-WINDOWS-INPUT-001; synthetic ZIP/input contracts only) - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-windows-tree.test.mjs` (E-M3-WINDOWS-LOCAL-002; structural Windows ConPTY closure only, no execution) - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-pty-smoke.test.mjs` (E-M3-WINDOWS-SMOKE-SETTLEMENT-LOCAL-RED-001, E-M3-WINDOWS-SMOKE-SETTLEMENT-LOCAL-001; native evidence remains pending) +- [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs` (E-M3-WINDOWS-RESOURCE-DIAGNOSTIC-LOCAL-001; native classification pending) - [x] `node config/scripts/verify-ssh-relay-node-release-inputs.mjs --inputs-directory --archive win32-x64` (E-M3-WINDOWS-INPUT-001; signed real Node ZIP, headers, and import library; no Windows execution) - [x] `node config/scripts/build-ssh-relay-runtime.mjs --tuple linux-arm64-glibc --inputs-directory --output-directory --source-date-epoch --git-commit ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) - [x] `node config/scripts/verify-ssh-relay-runtime.mjs --runtime-directory --identity --archive ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) @@ -2981,6 +2984,102 @@ or unexpected token`; the first logs did not identify a source location. - Follow-up: commit and push the isolated correction, run both target-native Windows jobs at the exact head, and require normal smoke completion plus artifact upload before advancing. +### E-M3-WINDOWS-SMOKE-SETTLEMENT-CI-RED-002 — Explicit PTY cleanup is insufficient on native x64 + +- Date: 2026-07-14 +- Commit SHA / PR: exact workflow head `b6291e55b1781b4c4e6f46a4965152f4a0d2334f`, containing + PTY settlement implementation `ddf28eb8dba3ade4806082cfc5edd6526389cf94`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runners and jobs: GitHub Actions run + [29342918504](https://github.com/stablyai/orca/actions/runs/29342918504); Windows x64 job + [87119092333](https://github.com/stablyai/orca/actions/runs/29342918504/job/87119092333), + `windows-2022` native x64 with Node v24.18.0; Windows arm64 job + [87119092293](https://github.com/stablyai/orca/actions/runs/29342918504/job/87119092293), + `windows-11-arm64` native arm64 with Node v24.18.0 +- Remote and transport: no SSH remote; target-native artifact build and bundled-runtime execution +- Exact commands: + `gh api repos/stablyai/orca/actions/jobs/87119092333/logs | rg -n -C 20 'SSH relay runtime verification failed|timeoutMs=|durationMs|rssBytes|contentId'` and the same command for job `87119092293` +- Result: expected discriminating FAIL. The new 25/25 contract suite passed, followed by signed + inputs, offline native compilation, archive/tree inspection, and functional bundled smoke. The + child emitted PTY exit 23, 101×37 resize, input, patched native paths, all watcher events, and + valid JSON in 3,344.690 ms with 59,699,200-byte RSS, then again remained alive until the parent + killed it at 45 seconds with empty stderr. ZIP size was 37,212,067 bytes, content ID + `sha256:68a49782761c411494e677be9c2dc5038fb4f85f066316edc7b92ee25a3c3fbd`, archive SHA-256 + `sha256:11688158da540cdfa1e10e5f632dac7487adf61e600e46d3fb0303fa42fc15b6`, and build duration + 139,167.307 ms. Artifact upload remained skipped. +- Arm64 result: the same valid PTY/watcher JSON arrived in 3,772.018 ms with 54,095,872-byte RSS, + followed by the identical 45-second timeout and empty stderr. ZIP size was 33,261,534 bytes, + content ID `sha256:cff683eba32500f95d4a1a87d8f8a8eaf06613753b75d8eff55ccce23a0160c6`, archive + SHA-256 `sha256:12640e6003547cf94f6ab13c6fdecb6373254b724fb561aa38023a3d33b5fa17`, and build + duration 155,781.418 ms. Upload remained skipped. +- Oracle proved: disposing all public PTY subscriptions and calling public Windows terminal cleanup + after validated exit does not by itself settle the bundled child. The failure remains post-smoke + and is not a PTY/watcher functional failure, but the retained resource type is still unknown. +- Does not prove: which PTY worker/socket, watcher, stdio, timer, or other resource remains; safe + dependency-level remediation; normal child exit; artifact retention; or any tuple. +- Checklist items satisfied: none beyond a new native red boundary; Windows rows remain unchecked. +- Follow-up: emit bounded `process.getActiveResourcesInfo()` data after all smoke cleanup, retain it + through the existing parent diagnostic, and make no further cleanup change until the resource + class is visible on native Windows. + +### E-M3-WINDOWS-RESOURCE-DIAGNOSTIC-LOCAL-001 — Bounded post-cleanup resource observation + +- Date: 2026-07-14 +- Commit SHA / PR: uncommitted diagnostic implementation on exact base + `b6291e55b1781b4c4e6f46a4965152f4a0d2334f`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: macOS 26.2 arm64; Node v26.0.0 and pnpm 10.24.0. Native Windows classification remains + authoritative. +- Remote and transport: none; injected public active-resource lists plus local source/static gates +- Exact commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.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 \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm run typecheck + pnpm exec oxlint config/scripts/ssh-relay-runtime-resource-diagnostics.cjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-smoke-child.cjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check .github/workflows/ssh-relay-runtime-artifacts.yml \ + config/scripts/ssh-relay-runtime-resource-diagnostics.cjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-smoke-child.cjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md + git diff --check + ``` + +- Result: PASS. The resource/PTY/workflow slice passed 6/6 tests in 173 ms; all nine artifact + suites passed 27/27 tests in 297 ms. Typecheck, focused oxlint, max-lines, full + lint/reliability/localization, formatting, and whitespace gates passed with only existing + out-of-package lint warnings. +- Oracle proved: Windows diagnostics use the public `process.getActiveResourcesInfo()` API, retain + only at most 256 resource type strings capped at 128 characters, report omitted counts, and take + snapshots immediately after all functional cleanup and after a two-second drain window. POSIX + smoke receives neither the delay nor a resource probe. Both native workflow families run the + contract. +- Does not prove: the actual retained native Windows resource types, whether the culprit is PTY or + watcher state, safe cleanup, normal child exit, artifact retention, or any tuple. +- Checklist items satisfied: bounded local diagnostic contract and verification-command inventory + only; no behavior, matrix, or tuple item. +- Follow-up: commit and push the diagnostic, capture both native Windows snapshots through the + existing 45-second failure formatter, then change only the resource owner identified by evidence. + ### E-M3-RUNTIME-LOCAL-001 — First target-native Linux arm64 runtime artifact - Date: 2026-07-14 @@ -3216,18 +3315,18 @@ or unexpected token`; the first logs did not identify a source location. No product gap is accepted merely because it appears in this list. Each entry requires explicit owner and promotion condition. -| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | -| ------------------------------------------ | ------------------------------------------------------------------------ | ----------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------ | -| Bundled runtime only partially implemented | Four unpublished POSIX native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | -| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | -| Windows runtime smoke incomplete | Both native smokes work but prior children hung; local cleanup awaits CI | No prompt settlement or retained artifact proof | Codex implementation owner | Exact-head clean exit/upload, reproducibility, trust, and live gates | Open | -| Native clean-rebuild identity unproved | One successful artifact per corrected-head hosted runner | Toolchain drift may change native content IDs | Codex implementation owner | Same-head, same-runner clean builds match or a reviewed reproducibility policy | Open | -| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | -| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | -| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | -| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | -| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | -| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | +| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | +| ------------------------------------------ | ----------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------ | +| Bundled runtime only partially implemented | Four unpublished POSIX native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | +| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | +| Windows runtime smoke incomplete | Native smokes work; public PTY cleanup still hangs; diagnostic is local | No prompt settlement or retained artifact proof | Codex implementation owner | Classify resource, then clean exit/upload, reproducibility/trust/live gates | Open | +| Native clean-rebuild identity unproved | One successful artifact per corrected-head hosted runner | Toolchain drift may change native content IDs | Codex implementation owner | Same-head, same-runner clean builds match or a reviewed reproducibility policy | Open | +| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | +| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | +| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | +| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | +| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | +| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | ## Final Definition of Done @@ -3268,10 +3367,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the isolated PTY smoke-settlement correction, then require exact-head Windows -x64/arm64 jobs to exit normally and upload their unpublished evidence artifacts. If both pass, record -their exact hashes, sizes, smoke/build durations, RSS, runner images, and residual gaps before the -separate same-head/same-runner clean-build identity gate. The cross-family Layer B target pool, -protected manifest-signing environment, and paired legacy performance baseline remain -release/default-path blockers; no publication, desktop resolver, SSH transfer/install, per-target -Beta, fallback, tuple enablement, or default behavior may be connected by this package. +Commit and push the bounded post-smoke resource diagnostic, then capture both native Windows +resource snapshots through the existing parent timeout without making another cleanup change. Use +that evidence to isolate the owning subsystem before requiring exact-head normal exit and artifact +upload. The separate clean-build identity gate, cross-family Layer B target pool, protected +manifest-signing environment, and paired legacy performance baseline remain release/default-path +blockers; no publication, desktop resolver, SSH transfer/install, per-target Beta, fallback, tuple +enablement, or default behavior may be connected by this package. From 2aaea70465dc80ced2ba4662b5bc1475a9c2f8c5 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 11:05:43 -0400 Subject: [PATCH 017/267] docs(ssh): record Windows resource diagnostic gate Co-authored-by: Orca --- ...14-ssh-relay-github-release-implementation-checklist.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 9bb1c183b94..43b05c3aeee 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 @@ -60,7 +60,8 @@ same change as the work it records. - Active package: Work Package 2 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`, - and explicit PTY smoke settlement in `ddf28eb8d`, in stacked draft PR + explicit PTY smoke settlement in `ddf28eb8d`, and bounded active-resource diagnostics in + `6bba90020`, in stacked draft PR [#8741](https://github.com/stablyai/orca/pull/8741). Exact-head run [29341643555](https://github.com/stablyai/orca/actions/runs/29341643555) proves the bundled Node, patched PTY input/resize/exit, and watcher lifecycle complete on x64 in 3,357.413 ms with @@ -3025,8 +3026,8 @@ or unexpected token`; the first logs did not identify a source location. ### E-M3-WINDOWS-RESOURCE-DIAGNOSTIC-LOCAL-001 — Bounded post-cleanup resource observation - Date: 2026-07-14 -- Commit SHA / PR: uncommitted diagnostic implementation on exact base - `b6291e55b1781b4c4e6f46a4965152f4a0d2334f`; draft PR +- Commit SHA / PR: diagnostic implementation commit + `6bba900209543a53b7673ca12173159d40d9fc87`; draft PR [#8741](https://github.com/stablyai/orca/pull/8741) - Runner: macOS 26.2 arm64; Node v26.0.0 and pnpm 10.24.0. Native Windows classification remains authoritative. From c04b4f63002a75b969d9d0317f563693cf6ab4f8 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 11:20:42 -0400 Subject: [PATCH 018/267] fix(ssh): settle copied Windows ConPTY worker Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + config/scripts/ssh-relay-node-pty-build.mjs | 6 + .../ssh-relay-node-pty-windows-settlement.mjs | 32 +++++ ...relay-node-pty-windows-settlement.test.mjs | 101 +++++++++++++++ ...github-release-implementation-checklist.md | 121 +++++++++++++----- 5 files changed, 233 insertions(+), 29 deletions(-) create mode 100644 config/scripts/ssh-relay-node-pty-windows-settlement.mjs create mode 100644 config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index f310da29fd5..f0d4dc9aee2 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -106,6 +106,7 @@ jobs: 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 \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ config/scripts/ssh-relay-node-zip-inspection.test.mjs \ config/scripts/ssh-relay-runtime-artifact.test.mjs \ config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ @@ -249,6 +250,7 @@ jobs: 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 ` + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs ` config/scripts/ssh-relay-node-zip-inspection.test.mjs ` config/scripts/ssh-relay-runtime-artifact.test.mjs ` config/scripts/ssh-relay-runtime-pty-smoke.test.mjs ` diff --git a/config/scripts/ssh-relay-node-pty-build.mjs b/config/scripts/ssh-relay-node-pty-build.mjs index a405d0618dd..2d8bd2b3bfb 100644 --- a/config/scripts/ssh-relay-node-pty-build.mjs +++ b/config/scripts/ssh-relay-node-pty-build.mjs @@ -4,6 +4,8 @@ import { createRequire } from 'node:module' import { dirname, join, resolve } from 'node:path' import { promisify } from 'node:util' +import { applyWindowsNodePtySettlement } from './ssh-relay-node-pty-windows-settlement.mjs' + const require = createRequire(import.meta.url) const execFileAsync = promisify(execFile) const NODE_GYP_PATH = require.resolve('node-gyp/bin/node-gyp.js') @@ -132,6 +134,10 @@ export async function buildPatchedSshRelayNodePty({ dereference: true, filter: sourceFilter }) + await applyWindowsNodePtySettlement({ + nodePtyLibraryDirectory: join(buildDirectory, 'lib'), + tuple + }) await mkdir(join(buildDirectory, 'node_modules'), { recursive: true }) await cp(NODE_ADDON_API_DIRECTORY, join(buildDirectory, 'node_modules', 'node-addon-api'), { recursive: true, diff --git a/config/scripts/ssh-relay-node-pty-windows-settlement.mjs b/config/scripts/ssh-relay-node-pty-windows-settlement.mjs new file mode 100644 index 00000000000..cf3a7751545 --- /dev/null +++ b/config/scripts/ssh-relay-node-pty-windows-settlement.mjs @@ -0,0 +1,32 @@ +import { readFile, writeFile } from 'node:fs/promises' +import { join } from 'node:path' + +const conptyDllKillSequence = ` this._ptyNative.kill(this._pty, this._useConptyDll); + this._outSocket.on('data', function () { + _this._conoutSocketWorker.dispose(); + });` + +const settledConptyDllKillSequence = ` this._ptyNative.kill(this._pty, this._useConptyDll); + // Why: quiet ConPTY output cannot retrigger disposal, so start its bounded drain now. + this._conoutSocketWorker.dispose(); + this._outSocket.on('data', function () { + _this._conoutSocketWorker.dispose(); + });` + +export async function applyWindowsNodePtySettlement({ nodePtyLibraryDirectory, tuple }) { + if (!tuple.startsWith('win32-')) { + return { applied: false } + } + + const agentPath = join(nodePtyLibraryDirectory, 'windowsPtyAgent.js') + const source = await readFile(agentPath, 'utf8') + const matchCount = source.split(conptyDllKillSequence).length - 1 + if (matchCount !== 1) { + throw new Error( + `expected exactly one DLL-mode ConPTY worker settlement sequence; found ${matchCount}` + ) + } + + await writeFile(agentPath, source.replace(conptyDllKillSequence, settledConptyDllKillSequence)) + return { applied: true } +} diff --git a/config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs b/config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs new file mode 100644 index 00000000000..5dfb808668f --- /dev/null +++ b/config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs @@ -0,0 +1,101 @@ +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { createRequire } from 'node:module' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { applyWindowsNodePtySettlement } from './ssh-relay-node-pty-windows-settlement.mjs' + +const require = createRequire(import.meta.url) +const temporaryDirectories = [] +const conptyDllKillSequence = ` this._ptyNative.kill(this._pty, this._useConptyDll); + this._outSocket.on('data', function () { + _this._conoutSocketWorker.dispose(); + });` + +afterEach(async () => { + await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true }))) +}) + +async function createNodePtyFixture(source = conptyDllKillSequence) { + const directory = await mkdtemp(join(tmpdir(), 'orca-node-pty-settlement-')) + temporaryDirectories.push(directory) + const agentPath = join(directory, 'windowsPtyAgent.js') + await writeFile(agentPath, source) + return { agentPath, directory } +} + +describe('SSH relay copied node-pty Windows settlement', () => { + it('starts ConPTY worker disposal before waiting for more output', async () => { + const nodePtyDirectory = dirname(require.resolve('node-pty/package.json')) + const fixture = await createNodePtyFixture( + await readFile(join(nodePtyDirectory, 'lib', 'windowsPtyAgent.js'), 'utf8') + ) + + await applyWindowsNodePtySettlement({ + nodePtyLibraryDirectory: fixture.directory, + tuple: 'win32-x64' + }) + + expect(await readFile(fixture.agentPath, 'utf8')).toContain( + ` this._ptyNative.kill(this._pty, this._useConptyDll); + // Why: quiet ConPTY output cannot retrigger disposal, so start its bounded drain now. + this._conoutSocketWorker.dispose(); + this._outSocket.on('data', function () {` + ) + }) + + it('does not inspect or modify a POSIX node-pty copy', async () => { + const missingDirectory = join(tmpdir(), 'orca-node-pty-posix-does-not-exist') + + await expect( + applyWindowsNodePtySettlement({ + nodePtyLibraryDirectory: missingDirectory, + tuple: 'linux-x64-glibc' + }) + ).resolves.toEqual({ applied: false }) + }) + + it('rejects source drift without modifying the copied agent', async () => { + const source = conptyDllKillSequence.replace( + "this._outSocket.on('data'", + "this._outSocket.once('data'" + ) + const fixture = await createNodePtyFixture(source) + + await expect( + applyWindowsNodePtySettlement({ + nodePtyLibraryDirectory: fixture.directory, + tuple: 'win32-arm64' + }) + ).rejects.toThrow('expected exactly one DLL-mode ConPTY worker settlement sequence; found 0') + await expect(readFile(fixture.agentPath, 'utf8')).resolves.toBe(source) + }) + + it('rejects duplicate source matches without modifying the copied agent', async () => { + const source = `${conptyDllKillSequence}\n${conptyDllKillSequence}` + const fixture = await createNodePtyFixture(source) + + await expect( + applyWindowsNodePtySettlement({ + nodePtyLibraryDirectory: fixture.directory, + tuple: 'win32-x64' + }) + ).rejects.toThrow('expected exactly one DLL-mode ConPTY worker settlement sequence; found 2') + await expect(readFile(fixture.agentPath, 'utf8')).resolves.toBe(source) + }) + + it('is wired after node-pty is copied into exclusive artifact staging', async () => { + const buildSource = await readFile( + new URL('./ssh-relay-node-pty-build.mjs', import.meta.url), + 'utf8' + ) + const copyIndex = buildSource.indexOf('await cp(sourceDirectory, buildDirectory') + const settlementIndex = buildSource.indexOf('await applyWindowsNodePtySettlement({') + + expect(copyIndex).toBeGreaterThan(-1) + expect(settlementIndex).toBeGreaterThan(copyIndex) + expect(buildSource.slice(copyIndex, settlementIndex)).not.toContain('node-gyp') + }) +}) 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 43b05c3aeee..340d44093b0 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — classify the Windows resource retained after explicit PTY listener/terminal cleanup still failed to settle functionally successful native x64 and arm64 children; exact-head green CI, reproducibility, oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — add a fail-closed, runtime-artifact-only correction for the ConPTY worker `MessagePort` retained after functionally successful native Windows x64 and arm64 smoke; exact-head green CI, reproducibility, oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -66,12 +66,15 @@ same change as the work it records. [29341643555](https://github.com/stablyai/orca/actions/runs/29341643555) proves the bundled Node, patched PTY input/resize/exit, and watcher lifecycle complete on x64 in 3,357.413 ms with 56,905,728-byte RSS and arm64 in 6,593.663 ms with 54,505,472-byte RSS, but each child retains a - Windows handle until the parent kills it at its 45-second bound. The active slice is a - discriminating PTY-listener/terminal-cleanup contract followed by explicit prompt ConPTY - settlement; the injected lifecycle contract and repository static gates are locally green, but - exact-head run 29342918504 proves the native x64 child still retains an unclassified resource - after valid JSON. This package may produce test artifacts but must not publish, resolve, transfer, - install, launch, or enable them. + Windows handle until the parent kills it at its 45-second bound. Exact-head run + [29343816558](https://github.com/stablyai/orca/actions/runs/29343816558) now classifies the resource + on both native architectures: after all public PTY cleanup and a two-second observation window, + one `MessagePort` remains alongside only the expected parent stdio `PipeWrap` resources + (E-M3-WINDOWS-RESOURCE-DIAGNOSTIC-CI-RED-001). The active slice is an exact-source, fail-closed + correction applied only to the copied node-pty Windows JavaScript inside runtime artifact + staging; the repository-wide node-pty patch and legacy/default desktop path must remain unchanged. + 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, 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 @@ -99,7 +102,11 @@ same change as the work it records. `gpgv` drive-letter keyring incompatibility. E-M3-WINDOWS-CI-RED-004 proves that correction with 21 passing and one intentionally skipped test, signed input acceptance, native compilation, and runtime/ZIP assembly on each architecture; both 45-second bundled smoke commands then time out - without propagating their already bounded child stderr. No Windows executable cell is claimed. + without propagating their already bounded child stderr. E-M3-WINDOWS-SMOKE-SETTLEMENT-CI-RED-001 + and E-M3-WINDOWS-SMOKE-SETTLEMENT-CI-RED-002 then proved functional PTY/watcher smoke still did not + settle after public listener and terminal cleanup. E-M3-WINDOWS-RESOURCE-DIAGNOSTIC-CI-RED-001 + identifies the persistent native resource as one `MessagePort` on both x64 and arm64 after a + two-second drain window. No Windows executable cell is claimed. - Production behavior: unchanged; Orca embeds relay JavaScript and installs `node-pty` plus `@parcel/watcher` with remote npm. - New runtime assets published: none. @@ -112,11 +119,13 @@ same change as the work it records. - Rollout control: existing per-SSH-target configuration; legacy is the default and the bundled runtime is an explicit per-target Beta opt-in under E-M1-ROLLOUT-DECISION-001. - Legacy fallback removal: not authorized. -- Next required action: preserve the smoke command's bounded stdout/stderr and timeout metadata in - the parent failure, add a discriminating diagnostic test, and rerun Windows x64 first to classify - the PTY/watcher failure before changing smoke behavior. A same-head/same-runner clean-rebuild - identity oracle for native build reproducibility remains a separate open gate. Keep cross-family - remote infrastructure, signing/trust, and measured legacy baseline gates open; do not introduce +- Next required action: prove the copied node-pty Windows runtime closure owns the persistent + `MessagePort`, add a discriminating source-drift-rejecting test, and apply the narrow correction + only after node-pty is copied into exclusive artifact staging. Rerun both exact-head native + Windows jobs and require normal child exit, no persistent `MessagePort` after observation, and + unpublished artifact upload before claiming either executable cell. A same-head/same-runner + clean-rebuild identity oracle remains a separate next gate. Keep cross-family remote + infrastructure, signing/trust, and measured legacy baseline gates open; do not introduce publication, resolver, transfer, rollout, tuple enablement, or default behavior. ## Non-Negotiable Invariants @@ -583,11 +592,12 @@ archive inspection, bundled-Node/native-module/PTY/watcher smoke, SBOM, and prov behind purpose-named scripts and CI artifacts. No release publication, desktop resolver, SSH transfer, install, fallback, rollout setting, or tuple enablement belongs in this package. -**Active sub-gate — 2026-07-14, Codex implementation owner:** exact-head x64 run -29342918504/job 87119092333 still timed out after functional smoke despite explicit PTY cleanup; -arm64 job 87119092293 reproduced it. Add bounded post-smoke active-resource diagnostics before any -second behavioral change, preserving the existing 45-second parent bound and avoiding -`process.exit()`. +**Active sub-gate — 2026-07-14, Codex implementation owner:** exact-head run 29343816558 identifies +one persistent `MessagePort` on native Windows x64 and arm64 after functional PTY/watcher smoke, +public PTY cleanup, and a two-second observation window. Prove and correct the copied ConPTY worker +lifecycle inside artifact staging only, rejecting missing, duplicated, or drifted source; preserve +the repository-wide node-pty patch, legacy/default desktop path, 45-second parent bound, and the +prohibition on `process.exit()`. Each runtime must contain only the executable closure required by the relay. @@ -1185,16 +1195,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; resource diagnostic pending | `ddf28eb8d`; E-M3-NODE-RED-001, E-M3-NODE-PROVENANCE-001, E-M3-RUNTIME-LOCAL-001, E-M3-CI-001, E-M3-WINDOWS-SMOKE-SETTLEMENT-CI-RED-002 | -| 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; ConPTY settlement active | `2aaea7046`; E-M3-NODE-RED-001, E-M3-NODE-PROVENANCE-001, E-M3-RUNTIME-LOCAL-001, E-M3-CI-001, E-M3-WINDOWS-RESOURCE-DIAGNOSTIC-CI-RED-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 | — | Every PR must document: @@ -3081,6 +3091,59 @@ or unexpected token`; the first logs did not identify a source location. - Follow-up: commit and push the diagnostic, capture both native Windows snapshots through the existing 45-second failure formatter, then change only the resource owner identified by evidence. +### E-M3-WINDOWS-RESOURCE-DIAGNOSTIC-CI-RED-001 — Native Windows retains one `MessagePort` + +- Date: 2026-07-14 +- Commit SHA / PR: exact workflow head `2aaea70465dc80ced2ba4662b5bc1475a9c2f8c5`, containing + bounded diagnostic implementation `6bba900209543a53b7673ca12173159d40d9fc87`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runners and jobs: GitHub Actions run + [29343816558](https://github.com/stablyai/orca/actions/runs/29343816558); Windows x64 job + [87122172498](https://github.com/stablyai/orca/actions/runs/29343816558/job/87122172498), + `windows-2022` image `20260706.237.1`, native x64, runner `2.335.1`; Windows arm64 job + [87122172553](https://github.com/stablyai/orca/actions/runs/29343816558/job/87122172553), + `windows-11-arm64` image `20260706.102.1`, native arm64, runner `2.335.1` +- Remote and transport: no SSH remote; target-native artifact build and bundled-runtime execution +- Exact commands: + + ```sh + gh api 'repos/stablyai/orca/actions/runs/29343816558/jobs?per_page=100' \ + --jq '.jobs[] | [.id,.name,.status,.conclusion,.head_sha,.html_url] | @tsv' + gh api repos/stablyai/orca/actions/jobs/87122172498/logs | \ + rg -n -C 10 'resourceSettlement|Bundled runtime smoke command failed|timeoutMs=|contentId|durationMs|rssBytes' + gh api repos/stablyai/orca/actions/jobs/87122172553/logs | \ + rg -n -C 10 'resourceSettlement|Bundled runtime smoke command failed|timeoutMs=|contentId|durationMs|rssBytes' + ``` + +- Result: expected discriminating FAIL on both Windows jobs after 27/27 contract tests, exact signed + inputs, offline native compilation, archive/tree inspection, and valid functional smoke output. + Immediately after smoke each child reported three `PipeWrap` resources plus one `MessagePort`; + after the two-second observation window one `PipeWrap` settled but the `MessagePort` remained. + Each child then remained alive until the unchanged 45-second parent timeout sent `SIGTERM`. +- X64 metrics: functional smoke completed in 5,368.1555 ms with 60,022,784-byte RSS. The ZIP was + 37,212,066 bytes with content ID + `sha256:4ee246445ad65eec49b70fceab92fbd954de2f7fd8608a0646cc9a79ce53e69a`, archive SHA-256 + `sha256:20c98e2db2aaa9245fb2c632d37c4963e70f65a0e5257aff4e196dc64fb984dd`, and build + duration 143,545.9941 ms. +- Arm64 metrics: functional smoke completed in 5,873.1038 ms with 54,059,008-byte RSS. The ZIP was + 33,261,530 bytes with content ID + `sha256:9418590ffa09d21145dd1ff4d8492d1ff8b0328df496e300edec1969365df3a3`, archive SHA-256 + `sha256:52477f9714e7ac38b9f40ce5642b7f1c9e838f98af48387c7a88ef53b1dcf385`, and build + duration 169,800.5025 ms. +- POSIX control: Linux x64/arm64 and macOS x64/arm64 jobs at the same exact head all completed + successfully; the Windows-only observation did not add a POSIX delay or resource probe. +- Oracle proved: the retained native resource class is one persistent `MessagePort` on both Windows + architectures after successful PTY/ConPTY/watcher behavior and public PTY cleanup. The remaining + `PipeWrap` resources are the smoke child's parent stdio and are not sufficient to explain the + Windows-only non-exit because the same child-process boundary exits on POSIX. +- Does not prove: which dependency object owns the `MessagePort`, a safe correction, normal Windows + child exit, uploaded artifact retention, clean-rebuild identity, native trust, SSH, or any tuple. + Both Windows executable cells remain unchecked. +- Checklist items satisfied: native resource classification only. +- Follow-up: inspect the copied node-pty ConPTY worker lifecycle, add an exact-source fail-closed + transform contract scoped to Windows artifact staging, then require both native jobs to exit and + upload unpublished evidence before advancing. + ### E-M3-RUNTIME-LOCAL-001 — First target-native Linux arm64 runtime artifact - Date: 2026-07-14 @@ -3320,7 +3383,7 @@ owner and promotion condition. | ------------------------------------------ | ----------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------ | | Bundled runtime only partially implemented | Four unpublished POSIX native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | | No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | -| Windows runtime smoke incomplete | Native smokes work; public PTY cleanup still hangs; diagnostic is local | No prompt settlement or retained artifact proof | Codex implementation owner | Classify resource, then clean exit/upload, reproducibility/trust/live gates | Open | +| Windows runtime smoke incomplete | Native smokes work; one ConPTY-worker-class `MessagePort` remains | No prompt settlement or retained artifact proof | Codex implementation owner | Artifact-only settlement, then clean exit/upload, reproducibility/trust gates | Open | | Native clean-rebuild identity unproved | One successful artifact per corrected-head hosted runner | Toolchain drift may change native content IDs | Codex implementation owner | Same-head, same-runner clean builds match or a reviewed reproducibility policy | Open | | Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | | Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | From d9a556b9c8b3d3a639d4fc93a8ba25926089d20f Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 11:23:18 -0400 Subject: [PATCH 019/267] docs(ssh): record ConPTY worker settlement gate Co-authored-by: Orca --- ...relay-node-pty-windows-settlement.test.mjs | 4 +- ...github-release-implementation-checklist.md | 140 ++++++++++++++---- 2 files changed, 116 insertions(+), 28 deletions(-) diff --git a/config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs b/config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs index 5dfb808668f..b3b4710927f 100644 --- a/config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs +++ b/config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs @@ -42,7 +42,9 @@ describe('SSH relay copied node-pty Windows settlement', () => { ` this._ptyNative.kill(this._pty, this._useConptyDll); // Why: quiet ConPTY output cannot retrigger disposal, so start its bounded drain now. this._conoutSocketWorker.dispose(); - this._outSocket.on('data', function () {` + this._outSocket.on('data', function () { + _this._conoutSocketWorker.dispose(); + });` ) }) 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 340d44093b0..8995fa8c123 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — add a fail-closed, runtime-artifact-only correction for the ConPTY worker `MessagePort` retained after functionally successful native Windows x64 and arm64 smoke; exact-head green CI, reproducibility, oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — require exact-head native Windows x64 and arm64 proof for the locally green, fail-closed runtime-artifact-only ConPTY worker settlement correction; reproducibility, oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -73,8 +73,9 @@ same change as the work it records. (E-M3-WINDOWS-RESOURCE-DIAGNOSTIC-CI-RED-001). The active slice is an exact-source, fail-closed correction applied only to the copied node-pty Windows JavaScript inside runtime artifact staging; the repository-wide node-pty patch and legacy/default desktop path must remain unchanged. - This package may produce test artifacts but must not publish, resolve, transfer, install, launch, - or enable them. + That local contract is green at `c04b4f630` under E-M3-WINDOWS-CONPTY-WORKER-LOCAL-001; exact-head + native Windows proof remains mandatory. 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, 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 @@ -119,14 +120,12 @@ same change as the work it records. - Rollout control: existing per-SSH-target configuration; legacy is the default and the bundled runtime is an explicit per-target Beta opt-in under E-M1-ROLLOUT-DECISION-001. - Legacy fallback removal: not authorized. -- Next required action: prove the copied node-pty Windows runtime closure owns the persistent - `MessagePort`, add a discriminating source-drift-rejecting test, and apply the narrow correction - only after node-pty is copied into exclusive artifact staging. Rerun both exact-head native - Windows jobs and require normal child exit, no persistent `MessagePort` after observation, and - unpublished artifact upload before claiming either executable cell. A same-head/same-runner - clean-rebuild identity oracle remains a separate next gate. Keep cross-family remote - infrastructure, signing/trust, and measured legacy baseline gates open; do not introduce - publication, resolver, transfer, rollout, tuple enablement, or default behavior. +- Next required action: push the locally green copied-node-pty settlement correction and rerun both + exact-head native Windows jobs. Require normal child exit, no persistent `MessagePort` after + observation, and unpublished artifact upload before claiming either executable cell. A + same-head/same-runner clean-rebuild identity oracle remains a separate next gate. Keep + cross-family remote infrastructure, signing/trust, and measured legacy baseline gates open; do + not introduce publication, resolver, transfer, rollout, tuple enablement, or default behavior. ## Non-Negotiable Invariants @@ -592,12 +591,12 @@ archive inspection, bundled-Node/native-module/PTY/watcher smoke, SBOM, and prov behind purpose-named scripts and CI artifacts. No release publication, desktop resolver, SSH transfer, install, fallback, rollout setting, or tuple enablement belongs in this package. -**Active sub-gate — 2026-07-14, Codex implementation owner:** exact-head run 29343816558 identifies -one persistent `MessagePort` on native Windows x64 and arm64 after functional PTY/watcher smoke, -public PTY cleanup, and a two-second observation window. Prove and correct the copied ConPTY worker -lifecycle inside artifact staging only, rejecting missing, duplicated, or drifted source; preserve -the repository-wide node-pty patch, legacy/default desktop path, 45-second parent bound, and the -prohibition on `process.exit()`. +**Active sub-gate — 2026-07-14, Codex implementation owner:** implementation commit `c04b4f630` +starts the ConPTY worker's bounded drain only in the copied Windows artifact source and rejects +missing, duplicated, or drifted source. Local contract and repository gates are green; require +normal child exit and unpublished artifact upload on exact-head native Windows x64 and arm64 while +preserving the repository-wide node-pty patch, legacy/default desktop path, 45-second parent bound, +and prohibition on `process.exit()`. Each runtime must contain only the executable closure required by the relay. @@ -1195,16 +1194,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; ConPTY settlement active | `2aaea7046`; E-M3-NODE-RED-001, E-M3-NODE-PROVENANCE-001, E-M3-RUNTIME-LOCAL-001, E-M3-CI-001, E-M3-WINDOWS-RESOURCE-DIAGNOSTIC-CI-RED-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 settlement CI pending | `c04b4f630`; E-M3-NODE-RED-001, E-M3-NODE-PROVENANCE-001, E-M3-RUNTIME-LOCAL-001, E-M3-CI-001, E-M3-WINDOWS-CONPTY-WORKER-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 | — | Every PR must document: @@ -1254,6 +1253,7 @@ focused commands as their scripts/tests are introduced. - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-windows-tree.test.mjs` (E-M3-WINDOWS-LOCAL-002; structural Windows ConPTY closure only, no execution) - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-pty-smoke.test.mjs` (E-M3-WINDOWS-SMOKE-SETTLEMENT-LOCAL-RED-001, E-M3-WINDOWS-SMOKE-SETTLEMENT-LOCAL-001; native evidence remains pending) - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs` (E-M3-WINDOWS-RESOURCE-DIAGNOSTIC-LOCAL-001; native classification pending) +- [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs` (E-M3-WINDOWS-CONPTY-WORKER-LOCAL-RED-001, E-M3-WINDOWS-CONPTY-WORKER-LOCAL-001; native settlement pending) - [x] `node config/scripts/verify-ssh-relay-node-release-inputs.mjs --inputs-directory --archive win32-x64` (E-M3-WINDOWS-INPUT-001; signed real Node ZIP, headers, and import library; no Windows execution) - [x] `node config/scripts/build-ssh-relay-runtime.mjs --tuple linux-arm64-glibc --inputs-directory --output-directory --source-date-epoch --git-commit ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) - [x] `node config/scripts/verify-ssh-relay-runtime.mjs --runtime-directory --identity --archive ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) @@ -3144,6 +3144,92 @@ or unexpected token`; the first logs did not identify a source location. transform contract scoped to Windows artifact staging, then require both native jobs to exit and upload unpublished evidence before advancing. +### E-M3-WINDOWS-CONPTY-WORKER-LOCAL-RED-001 — Copied-source settlement contract red gate + +- Date: 2026-07-14 +- Commit SHA / PR: uncommitted red-test working tree based on + `2aaea70465dc80ced2ba4662b5bc1475a9c2f8c5`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: macOS 26.2 arm64; Node v26.0.0 and pnpm 10.24.0. Node 24 and native Windows remain the + authoritative artifact environments. +- Remote and transport: none; synthetic copied-source fixtures only +- Exact command: + `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs` +- Result: expected discriminating FAIL, 4/4 tests failed in 136 ms. The copied Windows source was + unchanged, POSIX returned no explicit no-op result, and missing/duplicate source patterns did not + fail closed. +- Oracle proved: the new contract failed for each missing behavior before the correction existed. +- Does not prove: the green transform, production wiring, native Windows execution, prompt worker + settlement, or any tuple. +- Checklist items satisfied: red half of the copied ConPTY worker settlement correction only. +- Follow-up: implement the exact one-match transform against exclusive artifact staging and rerun + focused, artifact, and repository gates. + +### E-M3-WINDOWS-CONPTY-WORKER-LOCAL-001 — Fail-closed artifact-only worker settlement + +- Date: 2026-07-14 +- Commit SHA / PR: implementation commit `c04b4f630`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: macOS 26.2 arm64; Node v26.0.0 and pnpm 10.24.0. Node 24 and native Windows remain the + authoritative artifact environments. +- Remote and transport: none; pinned installed node-pty source, synthetic source-drift fixtures, + workflow contract, and repository static gates +- Exact commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.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 \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm run typecheck + pnpm exec oxlint config/scripts/ssh-relay-node-pty-build.mjs \ + config/scripts/ssh-relay-node-pty-windows-settlement.mjs \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ + .github/workflows/ssh-relay-runtime-artifacts.yml + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check .github/workflows/ssh-relay-runtime-artifacts.yml \ + config/scripts/ssh-relay-node-pty-build.mjs \ + config/scripts/ssh-relay-node-pty-windows-settlement.mjs \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md + git diff --check + ``` + +- Result: PASS. The purpose-named suite passed 5/5 tests in 124 ms; the settlement/PTY/resource/tree/ + workflow slice passed 12/12 tests across five suites in 201 ms; all ten artifact suites passed + 32/32 tests in 378 ms. Typecheck, focused oxlint, max-lines, full lint/reliability/localization, + formatting, and whitespace gates passed with only existing out-of-package lint warnings. +- Duration and resource metrics: the combined repository static command completed in approximately + 12.4 seconds. Peak memory and open-file counts were not instrumented by these local static tests. +- Oracle proved: the transform accepts the exact pinned installed node-pty source, starts the + ConPTY `Worker` drain even when no later output arrives, preserves later-data timer resets, modifies + only the copied Windows artifact source after exclusive staging, and does not inspect a POSIX copy. + Missing, drifted, duplicate, or already-transformed source rejects before a write. Both native + workflow families run the purpose-named contract. The repository-wide + `config/patches/node-pty@1.1.0.patch` and legacy/default desktop node-pty behavior are unchanged. +- Does not prove: native Windows worker termination, normal smoke child exit, absence of a persistent + `MessagePort`, artifact upload, reproducibility, native trust, oldest baseline, SSH, or any tuple. +- Checklist items satisfied: purpose-named local copied-source settlement contract and verification + command inventory only; Windows executable rows remain unchecked. +- Follow-up: push the exact head and require both target-native Windows jobs to exit normally, show + no persistent `MessagePort` after the observation window, and upload unpublished evidence. + ### E-M3-RUNTIME-LOCAL-001 — First target-native Linux arm64 runtime artifact - Date: 2026-07-14 From d35ab55b9f977bdbbb28b460b3a9535bb24c610a Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 11:35:19 -0400 Subject: [PATCH 020/267] docs(ssh): record native Windows runtime proof Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 176 +++++++++++++----- 1 file changed, 129 insertions(+), 47 deletions(-) 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 8995fa8c123..776ae307de6 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — require exact-head native Windows x64 and arm64 proof for the locally green, fail-closed runtime-artifact-only ConPTY worker settlement correction; reproducibility, oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — add and prove a same-head, same-runner two-clean-build identity oracle now that all six glibc/macOS/Windows target-native artifact and executable cells are green; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -73,9 +73,12 @@ same change as the work it records. (E-M3-WINDOWS-RESOURCE-DIAGNOSTIC-CI-RED-001). The active slice is an exact-source, fail-closed correction applied only to the copied node-pty Windows JavaScript inside runtime artifact staging; the repository-wide node-pty patch and legacy/default desktop path must remain unchanged. - That local contract is green at `c04b4f630` under E-M3-WINDOWS-CONPTY-WORKER-LOCAL-001; exact-head - native Windows proof remains mandatory. This package may produce test artifacts but must not - publish, resolve, transfer, install, launch, or enable them. + That local contract is green at `c04b4f630` under E-M3-WINDOWS-CONPTY-WORKER-LOCAL-001. Exact-head + run [29345126283](https://github.com/stablyai/orca/actions/runs/29345126283) then passed all six + native jobs; both Windows architectures shed the `MessagePort` within the two-second observation + 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, 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 @@ -94,8 +97,8 @@ same change as the work it records. - Windows PTY-closure correction: E-M3-WINDOWS-CONPTY-GAP-001 proved the candidate omitted the `conpty.dll` and `OpenConsole.exe` files required by Orca's production `useConptyDll: true` path and therefore exercised a weaker system-ConPTY smoke. E-M3-WINDOWS-LOCAL-002 proves the corrected - local tree-closure and smoke configuration contracts; native execution remains open and no - Windows executable cell is complete. + local tree-closure and smoke configuration contracts. E-M3-WINDOWS-CI-001 proves the corrected + native x64/arm64 closure executes and settles; oldest-baseline and native-trust cells remain open. - Windows native progression: E-M3-WINDOWS-CI-RED-002 captured and corrected NTFS fixture modes, Windows shebang parsing, authenticated-key checkout bytes, and POSIX-only mode assertions. At the corrected exact head, E-M3-WINDOWS-CI-RED-003 proves 20 passing and one intentionally skipped test @@ -107,7 +110,9 @@ same change as the work it records. and E-M3-WINDOWS-SMOKE-SETTLEMENT-CI-RED-002 then proved functional PTY/watcher smoke still did not settle after public listener and terminal cleanup. E-M3-WINDOWS-RESOURCE-DIAGNOSTIC-CI-RED-001 identifies the persistent native resource as one `MessagePort` on both x64 and arm64 after a - two-second drain window. No Windows executable cell is claimed. + two-second drain window. E-M3-WINDOWS-CI-001 proves the artifact-only correction removes that + resource before normal exit on both architectures and retains the uploaded bytes. No oldest- + baseline, native-trust, SSH, reproducibility, or enabled-tuple claim is made. - Production behavior: unchanged; Orca embeds relay JavaScript and installs `node-pty` plus `@parcel/watcher` with remote npm. - New runtime assets published: none. @@ -120,12 +125,11 @@ same change as the work it records. - Rollout control: existing per-SSH-target configuration; legacy is the default and the bundled runtime is an explicit per-target Beta opt-in under E-M1-ROLLOUT-DECISION-001. - Legacy fallback removal: not authorized. -- Next required action: push the locally green copied-node-pty settlement correction and rerun both - exact-head native Windows jobs. Require normal child exit, no persistent `MessagePort` after - observation, and unpublished artifact upload before claiming either executable cell. A - same-head/same-runner clean-rebuild identity oracle remains a separate next gate. Keep - cross-family remote infrastructure, signing/trust, and measured legacy baseline gates open; do - not introduce publication, resolver, transfer, rollout, tuple enablement, or default behavior. +- Next required action: add a same-head/same-runner two-clean-build identity comparison that fails + closed on any tree, archive, SBOM, provenance, or identity drift, then run it for every target- + native candidate. Keep oldest-baseline, cross-family remote infrastructure, signing/trust, and + measured legacy baseline gates open; do not introduce publication, resolver, transfer, rollout, + tuple enablement, or default behavior. ## Non-Negotiable Invariants @@ -591,12 +595,10 @@ archive inspection, bundled-Node/native-module/PTY/watcher smoke, SBOM, and prov behind purpose-named scripts and CI artifacts. No release publication, desktop resolver, SSH transfer, install, fallback, rollout setting, or tuple enablement belongs in this package. -**Active sub-gate — 2026-07-14, Codex implementation owner:** implementation commit `c04b4f630` -starts the ConPTY worker's bounded drain only in the copied Windows artifact source and rejects -missing, duplicated, or drifted source. Local contract and repository gates are green; require -normal child exit and unpublished artifact upload on exact-head native Windows x64 and arm64 while -preserving the repository-wide node-pty patch, legacy/default desktop path, 45-second parent bound, -and prohibition on `process.exit()`. +**Active sub-gate — 2026-07-14, Codex implementation owner:** exact-head run 29345126283 proves all +six target-native artifact/executable jobs, including normal Windows x64/arm64 settlement and +unpublished upload. Add a bounded same-head/same-runner two-clean-build comparison without changing +artifact consumers, publication, the repository-wide node-pty patch, or the legacy/default path. Each runtime must contain only the executable closure required by the relay. @@ -614,10 +616,11 @@ Each runtime must contain only the executable closure required by the relay. - [ ] Build Orca’s patched `node-pty@1.1.0` against the exact bundled Node runtime. - [ ] Assert Orca-required patched exports/diagnostics exist; do not silently use an upstream prebuild that omits the patch. -- [ ] For Windows, copy the tuple-architecture `conpty.dll` and `OpenConsole.exe` from the pinned +- [x] For Windows, copy the tuple-architecture `conpty.dll` and `OpenConsole.exe` from the pinned node-pty source into `build/Release/conpty`, hash them into the runtime identity as native runtime files, and prove PTY spawn/resize/exit with `useConptyDll: true`. Do not substitute a - system-ConPTY smoke for the production path. (E-M3-WINDOWS-CONPTY-GAP-001) + system-ConPTY smoke for the production path. (E-M3-WINDOWS-CONPTY-GAP-001, + E-M3-WINDOWS-CI-001) - [ ] Include exactly one compatible `@parcel/watcher@2.5.6` native optional package. - [ ] Include relay JavaScript, watcher child, required runtime JavaScript closure, licenses, SBOM, provenance, and runtime metadata. @@ -631,8 +634,9 @@ Each runtime must contain only the executable closure required by the relay. - [ ] Sign native code according to the platform decisions in Milestone 1. - [ ] Verify platform-native signatures/policy on target-native runners after signing and before aggregation; attest the exact verified bytes for the signed manifest. -- [x] Run local POSIX archive inspection and target-runtime smoke before any upload; Windows remains - open until its separate builder/verifier exists. (E-M3-RUNTIME-LOCAL-001) +- [x] Run target-native archive inspection and target-runtime smoke before upload for every current + glibc/macOS/Windows candidate. (E-M3-RUNTIME-LOCAL-001, E-M3-CI-001, + E-M3-WINDOWS-CI-001) ### Per-tuple build and executable proof @@ -644,8 +648,8 @@ Each runtime must contain only the executable closure required by the relay. | linux-arm64-musl | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | | darwin-x64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-CI-001 | | darwin-arm64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-CI-001 | -| win32-x64 | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | -| win32-arm64 | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | +| win32-x64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-WINDOWS-CI-001 | +| win32-arm64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-WINDOWS-CI-001 | Rules: @@ -1194,16 +1198,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 settlement CI pending | `c04b4f630`; E-M3-NODE-RED-001, E-M3-NODE-PROVENANCE-001, E-M3-RUNTIME-LOCAL-001, E-M3-CI-001, E-M3-WINDOWS-CONPTY-WORKER-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; reproducibility active | `d9a556b9c`; E-M3-NODE-PROVENANCE-001, E-M3-RUNTIME-LOCAL-001, E-M3-CI-001, E-M3-WINDOWS-CI-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 | — | Every PR must document: @@ -3230,6 +3234,84 @@ or unexpected token`; the first logs did not identify a source location. - Follow-up: push the exact head and require both target-native Windows jobs to exit normally, show no persistent `MessagePort` after the observation window, and upload unpublished evidence. +### E-M3-WINDOWS-CI-001 — Native Windows artifacts execute, settle, and upload + +- Date: 2026-07-14 +- Commit SHA / PR: exact workflow head `d9a556b9c8b3d3a639d4fc93a8ba25926089d20f`, containing + implementation commit `c04b4f630`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Run: GitHub Actions + [29345126283](https://github.com/stablyai/orca/actions/runs/29345126283), completed success with all + six target-native jobs green +- Windows runners and jobs: x64 job + [87126693074](https://github.com/stablyai/orca/actions/runs/29345126283/job/87126693074), + `windows-2022` image `20260706.237.1`, Windows Server 2022 build 20348, native X64, Node v24.18.0; + arm64 job + [87126693105](https://github.com/stablyai/orca/actions/runs/29345126283/job/87126693105), + `windows-11-arm64` image `20260706.102.1`, Windows build 26200, native ARM64, Node v24.18.0 +- Remote and transport: no SSH remote; target-native artifact build, bundled-runtime execution, and + GitHub Actions unpublished artifact upload +- Exact commands: + + ```sh + gh run watch 29345126283 --repo stablyai/orca --interval 10 --exit-status + gh api 'repos/stablyai/orca/actions/runs/29345126283/jobs?per_page=100' \ + --jq '.jobs[] | [.id,.name,.status,.conclusion,.started_at,.completed_at,.head_sha,.html_url] | @tsv' + gh api repos/stablyai/orca/actions/jobs/87126693074/logs | \ + rg -n -C 12 'resourceSettlement|contentId|archive|durationMs|rssBytes|Artifact .* has been successfully uploaded' + gh api repos/stablyai/orca/actions/jobs/87126693105/logs | \ + rg -n -C 12 'resourceSettlement|contentId|archive|durationMs|rssBytes|Artifact .* has been successfully uploaded' + gh api 'repos/stablyai/orca/actions/runs/29345126283/artifacts?per_page=100' \ + --jq '.artifacts[] | [.id,.name,.size_in_bytes,.expired,.created_at,.archive_download_url] | @tsv' + gh api repos/stablyai/orca/actions/artifacts/8315822805/zip > + gh api repos/stablyai/orca/actions/artifacts/8315904862/zip > + shasum -a 256 + unzip -l + unzip -p '*identity.json' | jq + unzip -l + unzip -p '*identity.json' | jq + ``` + +- Result: PASS. Both Windows jobs passed 32/32 artifact contract tests, exact signed Node input + verification, offline target-native compilation, exact archive/tree inspection, bundled Node + v24.18.0, production-DLL ConPTY input/101×37 resize/exit 23, watcher lifecycle, normal child exit, + and unpublished upload. Immediately after smoke, each child reported three `PipeWrap` resources, + one `MessagePort`, and the diagnostic's own `Timeout`; after the two-second observation only the + two parent stdio `PipeWrap` resources remained. No `MessagePort` or timer survived, and neither + 45-second parent timeout fired. +- X64 metrics and bytes: build 135,657.3557 ms; full verification 6,495.1387 ms; smoke 5,353.5073 ms + with 54,075,392-byte RSS. The 42-file ZIP was 37,212,121 bytes, expanded to 97,248,569 bytes, had + content ID `sha256:32efa708b1fbefb05a11b0de6703551e5680d64c8015bb956095db3b50292c92`, and archive + SHA-256 `sha256:2ec9db55b5c417ea7ed9ab6cf5a960be4b9c305e68c68f8123a11dab0e3119a4`. + Uploaded artifact + [8315822805](https://github.com/stablyai/orca/actions/runs/29345126283/artifacts/8315822805) was + 37,075,355 bytes and downloaded with SHA-256 + `d98d1c935514667ea5c48a160ef54b9674a93f995ca24df2c19759735c62eefb`; total job duration was + 4m06s. +- Arm64 metrics and bytes: build 190,135.9561 ms; full verification 7,908.8115 ms; smoke 5,874.805 ms + with 51,843,072-byte RSS. The 42-file ZIP was 33,261,582 bytes, expanded to 86,189,895 bytes, had + content ID `sha256:e1d5eab84a7b1eb38d55acb9f87d852569995e22c47c02d202f29eb293467944`, and archive + SHA-256 `sha256:577285df23333a3de89489dec52027afd76aeb9ef23f74ffb6d5706e06a75ae8`. + Uploaded artifact + [8315904862](https://github.com/stablyai/orca/actions/runs/29345126283/artifacts/8315904862) was + 33,137,908 bytes and downloaded with SHA-256 + `77b78cb33117c8c8b4f5d639dada063651f7d303a815a097bfa36d9e651a2419`; total job duration was + 7m09s. +- POSIX control: Linux x64/arm64 and macOS x64/arm64 passed the same exact-head contract, build, + verification, smoke, and unpublished upload steps. All six run artifacts exist with seven-day + retention; no release asset was published. +- Oracle proved: the copied-source correction terminates the node-pty ConPTY worker on both native + Windows architectures without masking the process with `process.exit()`, while preserving full + PTY/watcher behavior and the four POSIX controls. The exact uploaded evidence contains the runtime + ZIP, identity, SPDX SBOM, and provenance files. +- Does not prove: same-runner clean-rebuild identity, oldest baselines, Authenticode/native trust, + SSH/SFTP/system-SSH transfer, relay RPC behavior, cancellation, publication, or any enabled tuple. +- Checklist items satisfied: Milestone 3 Windows `conpty.dll`/`OpenConsole.exe` production-path item; + Windows x64 and arm64 build/provenance, bundled Node, real PTY, and watcher cells; target-native + pre-upload inspection/smoke command. +- Follow-up: add and execute the separately gated same-head/same-runner two-clean-build identity + oracle before oldest-baseline or trust work. + ### E-M3-RUNTIME-LOCAL-001 — First target-native Linux arm64 runtime artifact - Date: 2026-07-14 @@ -3465,18 +3547,18 @@ or unexpected token`; the first logs did not identify a source location. No product gap is accepted merely because it appears in this list. Each entry requires explicit owner and promotion condition. -| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | -| ------------------------------------------ | ----------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------ | -| Bundled runtime only partially implemented | Four unpublished POSIX native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | -| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | -| Windows runtime smoke incomplete | Native smokes work; one ConPTY-worker-class `MessagePort` remains | No prompt settlement or retained artifact proof | Codex implementation owner | Artifact-only settlement, then clean exit/upload, reproducibility/trust gates | Open | -| Native clean-rebuild identity unproved | One successful artifact per corrected-head hosted runner | Toolchain drift may change native content IDs | Codex implementation owner | Same-head, same-runner clean builds match or a reviewed reproducibility policy | Open | -| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | -| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | -| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | -| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | -| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | -| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | +| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | +| ------------------------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------ | +| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | +| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | +| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | +| Native clean-rebuild identity unproved | One successful artifact per corrected-head hosted runner | Toolchain drift may change native content IDs | Codex implementation owner | Same-head, same-runner clean builds match or a reviewed reproducibility policy | Open | +| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | +| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | +| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | +| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | +| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | +| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | ## Final Definition of Done From c70e96374cafb681c52b3d60e42322ba5a76791c Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 11:49:48 -0400 Subject: [PATCH 021/267] test(ssh): gate runtime clean-build identity Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 119 ++++---- .../ssh-relay-runtime-reproducibility.mjs | 268 ++++++++++++++++++ ...ssh-relay-runtime-reproducibility.test.mjs | 145 ++++++++++ .../ssh-relay-runtime-workflow.test.mjs | 26 +- ...github-release-implementation-checklist.md | 43 +-- 5 files changed, 531 insertions(+), 70 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-reproducibility.mjs create mode 100644 config/scripts/ssh-relay-runtime-reproducibility.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index f0d4dc9aee2..21e36216a16 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -110,6 +110,7 @@ jobs: config/scripts/ssh-relay-node-zip-inspection.test.mjs \ config/scripts/ssh-relay-runtime-artifact.test.mjs \ config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ config/scripts/ssh-relay-runtime-zip.test.mjs \ @@ -129,34 +130,44 @@ jobs: done printf 'NODE_INPUTS=%s\n' "$inputs" >> "$GITHUB_ENV" - - name: Build, inspect, and smoke exact runtime + - name: Build twice, inspect, smoke, and compare exact runtime shell: bash run: | set -euo pipefail - output="$RUNNER_TEMP/ssh-relay-runtime/${{ matrix.tuple }}" - mkdir -p "$(dirname "$output")" + output_root="$RUNNER_TEMP/ssh-relay-runtime/${{ matrix.tuple }}" + mkdir -p "$output_root" + first_output="$output_root/first" + second_output="$output_root/second" source_commit=$(git rev-parse HEAD) source_date_epoch=$(git show -s --format=%ct "$source_commit") - /usr/bin/time -p node config/scripts/build-ssh-relay-runtime.mjs \ + for output in "$first_output" "$second_output"; do + printf 'clean_build_output=%s\n' "$output" + /usr/bin/time -p node config/scripts/build-ssh-relay-runtime.mjs \ + --tuple '${{ matrix.tuple }}' \ + --inputs-directory "$NODE_INPUTS" \ + --output-directory "$output" \ + --source-date-epoch "$source_date_epoch" \ + --git-commit "$source_commit" + identity="$output/orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json" + archive=$(find "$output" -maxdepth 1 -name '*.tar.xz' -print -quit) + test -n "$archive" + /usr/bin/time -p node config/scripts/verify-ssh-relay-runtime.mjs \ + --runtime-directory "$output/runtime" \ + --identity "$identity" \ + --archive "$archive" + if find "$output/runtime" -type f \( -name npm -o -name npx -o -name corepack -o -name '*.map' \) -print -quit | grep -q .; then + echo 'Runtime contains a prohibited package manager or source map.' >&2 + exit 1 + fi + parcel_packages=$(find "$output/runtime/node_modules/@parcel" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ') + test "$parcel_packages" = 2 + done + node config/scripts/ssh-relay-runtime-reproducibility.mjs \ --tuple '${{ matrix.tuple }}' \ - --inputs-directory "$NODE_INPUTS" \ - --output-directory "$output" \ - --source-date-epoch "$source_date_epoch" \ - --git-commit "$source_commit" - identity="$output/orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json" - archive=$(find "$output" -maxdepth 1 -name '*.tar.xz' -print -quit) - /usr/bin/time -p node config/scripts/verify-ssh-relay-runtime.mjs \ - --runtime-directory "$output/runtime" \ - --identity "$identity" \ - --archive "$archive" - if find "$output/runtime" -type f \( -name npm -o -name npx -o -name corepack -o -name '*.map' \) -print -quit | grep -q .; then - echo 'Runtime contains a prohibited package manager or source map.' >&2 - exit 1 - fi - parcel_packages=$(find "$output/runtime/node_modules/@parcel" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ') - test "$parcel_packages" = 2 + --first-output-directory "$first_output" \ + --second-output-directory "$second_output" mkdir -p "$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}" - cp "$output"/*.tar.xz "$output"/*.spdx.json "$output"/*.provenance.json "$output"/*.identity.json \ + cp "$first_output"/*.tar.xz "$first_output"/*.spdx.json "$first_output"/*.provenance.json "$first_output"/*.identity.json \ "$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}/" - name: Upload unpublished artifact evidence @@ -254,6 +265,7 @@ jobs: config/scripts/ssh-relay-node-zip-inspection.test.mjs ` config/scripts/ssh-relay-runtime-artifact.test.mjs ` config/scripts/ssh-relay-runtime-pty-smoke.test.mjs ` + config/scripts/ssh-relay-runtime-reproducibility.test.mjs ` config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs ` config/scripts/ssh-relay-runtime-windows-tree.test.mjs ` config/scripts/ssh-relay-runtime-zip.test.mjs ` @@ -284,41 +296,52 @@ jobs: } "NODE_INPUTS=$inputs" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - name: Build, inspect, and smoke exact runtime + - name: Build twice, inspect, smoke, and compare exact runtime shell: pwsh run: | $ErrorActionPreference = 'Stop' - $output = Join-Path $env:RUNNER_TEMP 'ssh-relay-runtime/${{ matrix.tuple }}' - New-Item -ItemType Directory -Path (Split-Path -Parent $output) -Force | Out-Null + $outputRoot = Join-Path $env:RUNNER_TEMP 'ssh-relay-runtime/${{ matrix.tuple }}' + New-Item -ItemType Directory -Path $outputRoot -Force | Out-Null + $firstOutput = Join-Path $outputRoot 'first' + $secondOutput = Join-Path $outputRoot 'second' $sourceCommit = git rev-parse HEAD $sourceDateEpoch = git show -s --format=%ct $sourceCommit - node config/scripts/build-ssh-relay-runtime.mjs ` - --tuple '${{ matrix.tuple }}' ` - --inputs-directory $env:NODE_INPUTS ` - --output-directory $output ` - --source-date-epoch $sourceDateEpoch ` - --git-commit $sourceCommit - if ($LASTEXITCODE -ne 0) { throw 'runtime build failed' } - $identity = Join-Path $output 'orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json' - $archive = Get-ChildItem -LiteralPath $output -Filter '*.zip' -File | Select-Object -First 1 - if (-not $archive) { throw 'runtime ZIP is missing' } - node config/scripts/verify-ssh-relay-runtime.mjs ` - --runtime-directory (Join-Path $output 'runtime') ` - --identity $identity ` - --archive $archive.FullName - if ($LASTEXITCODE -ne 0) { throw 'runtime verification failed' } - $prohibited = Get-ChildItem -LiteralPath (Join-Path $output 'runtime') -Recurse -File | - Where-Object { $_.Name -in @('npm', 'npm.cmd', 'npx', 'npx.cmd', 'corepack', 'corepack.cmd') -or $_.Name.EndsWith('.map') } | - Select-Object -First 1 - if ($prohibited) { throw "Runtime contains a prohibited file: $($prohibited.FullName)" } - $parcelRoot = Join-Path $output 'runtime/node_modules/@parcel' - if (@(Get-ChildItem -LiteralPath $parcelRoot -Directory).Count -ne 2) { - throw 'Runtime must contain exactly the watcher wrapper and one native package' + foreach ($output in @($firstOutput, $secondOutput)) { + "clean_build_output=$output" + node config/scripts/build-ssh-relay-runtime.mjs ` + --tuple '${{ matrix.tuple }}' ` + --inputs-directory $env:NODE_INPUTS ` + --output-directory $output ` + --source-date-epoch $sourceDateEpoch ` + --git-commit $sourceCommit + if ($LASTEXITCODE -ne 0) { throw 'runtime build failed' } + $identity = Join-Path $output 'orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json' + $archive = Get-ChildItem -LiteralPath $output -Filter '*.zip' -File | Select-Object -First 1 + if (-not $archive) { throw 'runtime ZIP is missing' } + node config/scripts/verify-ssh-relay-runtime.mjs ` + --runtime-directory (Join-Path $output 'runtime') ` + --identity $identity ` + --archive $archive.FullName + if ($LASTEXITCODE -ne 0) { throw 'runtime verification failed' } + $prohibited = Get-ChildItem -LiteralPath (Join-Path $output 'runtime') -Recurse -File | + Where-Object { $_.Name -in @('npm', 'npm.cmd', 'npx', 'npx.cmd', 'corepack', 'corepack.cmd') -or $_.Name.EndsWith('.map') } | + Select-Object -First 1 + if ($prohibited) { throw "Runtime contains a prohibited file: $($prohibited.FullName)" } + $parcelRoot = Join-Path $output 'runtime/node_modules/@parcel' + if (@(Get-ChildItem -LiteralPath $parcelRoot -Directory).Count -ne 2) { + throw 'Runtime must contain exactly the watcher wrapper and one native package' + } } + node config/scripts/ssh-relay-runtime-reproducibility.mjs ` + --tuple '${{ matrix.tuple }}' ` + --first-output-directory $firstOutput ` + --second-output-directory $secondOutput + if ($LASTEXITCODE -ne 0) { throw 'runtime reproducibility verification failed' } $evidence = Join-Path $env:GITHUB_WORKSPACE 'runtime-evidence/${{ matrix.tuple }}' New-Item -ItemType Directory -Path $evidence -Force | Out-Null - Copy-Item -LiteralPath $archive.FullName -Destination $evidence - Get-ChildItem -LiteralPath $output -File | + $firstArchive = Get-ChildItem -LiteralPath $firstOutput -Filter '*.zip' -File | Select-Object -First 1 + Copy-Item -LiteralPath $firstArchive.FullName -Destination $evidence + Get-ChildItem -LiteralPath $firstOutput -File | Where-Object { $_.Name -match '\.(?:spdx|provenance|identity)\.json$' } | Copy-Item -Destination $evidence diff --git a/config/scripts/ssh-relay-runtime-reproducibility.mjs b/config/scripts/ssh-relay-runtime-reproducibility.mjs new file mode 100644 index 00000000000..febadab9db0 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-reproducibility.mjs @@ -0,0 +1,268 @@ +#!/usr/bin/env node + +import { createHash } from 'node:crypto' +import { createReadStream } from 'node:fs' +import { lstat, opendir, readFile, realpath } from 'node:fs/promises' +import { join, resolve } from 'node:path' + +// The bounded output adds four release assets around a runtime tree capped at 5,000 entries. +const MAX_ENTRIES = 6_000 +const MAX_FILE_BYTES = 250 * 1024 * 1024 +const MAX_TOTAL_BYTES = 500 * 1024 * 1024 +const MAX_PATH_BYTES = 512 +const MAX_PATH_DEPTH = 40 +const MAX_IDENTITY_BYTES = 1024 * 1024 +const COMPARISON_TIMEOUT_MS = 5 * 60 * 1000 +const SUPPORTED_TUPLES = new Set([ + 'linux-x64-glibc', + 'linux-arm64-glibc', + 'darwin-x64', + 'darwin-arm64', + 'win32-x64', + 'win32-arm64' +]) + +async function sha256File(path, signal) { + const digest = createHash('sha256') + for await (const chunk of createReadStream(path, { signal })) { + signal.throwIfAborted() + digest.update(chunk) + } + return `sha256:${digest.digest('hex')}` +} + +function validateRelativePath(path) { + if ( + Buffer.byteLength(path) > MAX_PATH_BYTES || + path.split('/').length > MAX_PATH_DEPTH || + path.includes('\\') + ) { + throw new Error(`reproducibility output path exceeds limits: ${path}`) + } +} + +async function snapshotOutput(directory, label, signal) { + const entries = new Map() + let files = 0 + let bytes = 0 + + async function visit(absoluteDirectory, relativeDirectory = '') { + const children = [] + const handle = await opendir(absoluteDirectory) + for await (const child of handle) { + children.push(child.name) + } + children.sort() + for (const name of children) { + signal.throwIfAborted() + const path = relativeDirectory ? `${relativeDirectory}/${name}` : name + validateRelativePath(path) + const absolutePath = join(absoluteDirectory, name) + const metadata = await lstat(absolutePath) + if (metadata.isSymbolicLink() || (!metadata.isDirectory() && !metadata.isFile())) { + throw new Error(`${label} contains a prohibited special entry: ${path}`) + } + if (entries.size >= MAX_ENTRIES) { + throw new Error(`${label} exceeds the reproducibility entry-count limit`) + } + if (metadata.isDirectory()) { + entries.set(path, { path, type: 'directory', mode: metadata.mode & 0o777 }) + await visit(absolutePath, path) + continue + } + if (metadata.size > MAX_FILE_BYTES) { + throw new Error(`${label} file exceeds the reproducibility size limit: ${path}`) + } + files += 1 + bytes += metadata.size + if (bytes > MAX_TOTAL_BYTES) { + throw new Error(`${label} exceeds the reproducibility total-size limit`) + } + entries.set(path, { + path, + type: 'file', + mode: metadata.mode & 0o777, + size: metadata.size, + sha256: await sha256File(absolutePath, signal) + }) + } + } + + const root = await lstat(directory) + if (!root.isDirectory() || root.isSymbolicLink()) { + throw new Error(`${label} must be a real directory`) + } + await visit(directory) + return { bytes, entries, files } +} + +async function validateCompleteOutput(directory, label, tuple, snapshot) { + const identityName = `orca-ssh-relay-runtime-${tuple}.identity.json` + const required = [ + 'runtime', + 'runtime/relay.js', + 'runtime/relay-watcher.js', + 'runtime/runtime-metadata.json', + identityName, + `orca-ssh-relay-runtime-${tuple}.spdx.json`, + `orca-ssh-relay-runtime-${tuple}.provenance.json` + ] + for (const path of required) { + if (!snapshot.entries.has(path)) { + throw new Error(`${label} is missing required asset: ${path}`) + } + } + const identityEntry = snapshot.entries.get(identityName) + if (identityEntry.type !== 'file' || identityEntry.size > MAX_IDENTITY_BYTES) { + throw new Error(`${label} identity exceeds its size or type contract`) + } + let identity + try { + identity = JSON.parse(await readFile(join(directory, identityName), 'utf8')) + } catch { + throw new Error(`${label} identity is not valid JSON`) + } + if ( + identity.tupleId !== tuple || + !/^sha256:[0-9a-f]{64}$/.test(identity.contentId) || + typeof identity.archive?.name !== 'string' || + !/^sha256:[0-9a-f]{64}$/.test(identity.archive.sha256) + ) { + throw new Error(`${label} identity does not match the requested tuple or digest contract`) + } + if (!snapshot.entries.has(identity.archive.name)) { + throw new Error(`${label} is missing required asset: ${identity.archive.name}`) + } + const archiveEntry = snapshot.entries.get(identity.archive.name) + if (archiveEntry.type !== 'file' || archiveEntry.sha256 !== identity.archive.sha256) { + throw new Error(`${label} archive digest does not match its identity`) + } + const expectedTopLevel = new Set([...required.slice(4), identity.archive.name, 'runtime']) + const unexpected = [...snapshot.entries.keys()].find( + (path) => !path.includes('/') && !expectedTopLevel.has(path) + ) + if (unexpected) { + throw new Error(`${label} contains an unexpected top-level asset: ${unexpected}`) + } + return identity +} + +function compareSnapshots(first, second) { + const paths = [...new Set([...first.entries.keys(), ...second.entries.keys()])].sort() + for (const path of paths) { + const left = first.entries.get(path) + const right = second.entries.get(path) + if (!left) { + throw new Error(`first output is missing reproducibility entry: ${path}`) + } + if (!right) { + throw new Error(`second output is missing reproducibility entry: ${path}`) + } + for (const field of ['type', 'mode', 'size', 'sha256']) { + if (left[field] !== right[field]) { + throw new Error(`reproducibility mismatch for ${path}: ${field}`) + } + } + } +} + +export async function verifySshRelayRuntimeReproducibility({ + firstOutputDirectory, + secondOutputDirectory, + tuple, + signal = AbortSignal.timeout(COMPARISON_TIMEOUT_MS) +}) { + const started = process.hrtime.bigint() + if (!SUPPORTED_TUPLES.has(tuple)) { + throw new Error(`unsupported runtime reproducibility tuple: ${tuple}`) + } + const firstDirectory = resolve(firstOutputDirectory) + const secondDirectory = resolve(secondOutputDirectory) + if (firstDirectory === secondDirectory) { + throw new Error('clean-build outputs must be distinct directories') + } + signal.throwIfAborted() + const [firstRealPath, secondRealPath] = await Promise.all([ + realpath(firstDirectory), + realpath(secondDirectory) + ]) + if (firstRealPath === secondRealPath) { + throw new Error('clean-build outputs must resolve to distinct directories') + } + + // Walk sequentially so the clean-build gate cannot double file and read pressure on CI runners. + const first = await snapshotOutput(firstDirectory, 'first output', signal) + const second = await snapshotOutput(secondDirectory, 'second output', signal) + const firstIdentity = await validateCompleteOutput(firstDirectory, 'first output', tuple, first) + const secondIdentity = await validateCompleteOutput( + secondDirectory, + 'second output', + tuple, + second + ) + compareSnapshots(first, second) + if ( + firstIdentity.contentId !== secondIdentity.contentId || + firstIdentity.archive.sha256 !== secondIdentity.archive.sha256 + ) { + throw new Error('reproducibility identity or archive digest mismatch') + } + + return { + tuple, + contentId: firstIdentity.contentId, + archiveSha256: firstIdentity.archive.sha256, + entries: first.entries.size, + files: first.files, + bytes: first.bytes, + durationMs: Number(process.hrtime.bigint() - started) / 1e6 + } +} + +function valueAfter(argv, index, flag) { + const value = argv[index + 1] + if (!value || value.startsWith('--')) { + throw new Error(`${flag} requires a value`) + } + return value +} + +export function parseReproducibilityArguments(argv) { + const result = {} + for (let index = 0; index < argv.length; index += 1) { + const flag = argv[index] + const value = valueAfter(argv, index, flag) + if (flag === '--tuple') { + result.tuple = value + } else if (flag === '--first-output-directory') { + result.firstOutputDirectory = resolve(value) + } else if (flag === '--second-output-directory') { + result.secondOutputDirectory = resolve(value) + } else { + throw new Error(`Unknown argument: ${flag}`) + } + index += 1 + } + for (const field of ['tuple', 'firstOutputDirectory', 'secondOutputDirectory']) { + if (!result[field]) { + throw new Error(`Missing required reproducibility argument: ${field}`) + } + } + return result +} + +async function main() { + const result = await verifySshRelayRuntimeReproducibility( + parseReproducibilityArguments(process.argv.slice(2)) + ) + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`) +} + +if (process.argv[1] && resolve(process.argv[1]) === import.meta.filename) { + main().catch((error) => { + process.stderr.write( + `SSH relay runtime reproducibility verification failed: ${error.message}\n` + ) + process.exitCode = 1 + }) +} diff --git a/config/scripts/ssh-relay-runtime-reproducibility.test.mjs b/config/scripts/ssh-relay-runtime-reproducibility.test.mjs new file mode 100644 index 00000000000..0a346a6f6c9 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-reproducibility.test.mjs @@ -0,0 +1,145 @@ +import { createHash } from 'node:crypto' +import { chmod, cp, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { verifySshRelayRuntimeReproducibility } from './ssh-relay-runtime-reproducibility.mjs' + +const temporaryDirectories = [] +const tuple = 'linux-x64-glibc' +const contentId = `sha256:${'a'.repeat(64)}` +const archiveBytes = 'archive bytes' +const archiveSha256 = `sha256:${createHash('sha256').update(archiveBytes).digest('hex')}` +const archiveName = `orca-ssh-relay-runtime-v1-${tuple}-${contentId.slice('sha256:'.length)}.tar.xz` + +afterEach(async () => { + await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true }))) +}) + +async function writeOutput(directory) { + await mkdir(join(directory, 'runtime'), { recursive: true }) + await Promise.all([ + writeFile(join(directory, 'runtime', 'relay.js'), 'relay bytes'), + writeFile(join(directory, 'runtime', 'relay-watcher.js'), 'watcher bytes'), + writeFile(join(directory, 'runtime', 'runtime-metadata.json'), '{"schemaVersion":1}\n'), + writeFile(join(directory, archiveName), archiveBytes), + writeFile( + join(directory, `orca-ssh-relay-runtime-${tuple}.identity.json`), + `${JSON.stringify({ tupleId: tuple, contentId, archive: { name: archiveName, sha256: archiveSha256 } })}\n` + ), + writeFile( + join(directory, `orca-ssh-relay-runtime-${tuple}.spdx.json`), + '{"spdxVersion":"SPDX-2.3"}\n' + ), + writeFile( + join(directory, `orca-ssh-relay-runtime-${tuple}.provenance.json`), + '{"predicateType":"https://slsa.dev/provenance/v1"}\n' + ) + ]) +} + +async function createOutputs() { + const directory = await mkdtemp(join(tmpdir(), 'orca-runtime-reproducibility-')) + temporaryDirectories.push(directory) + const firstOutputDirectory = join(directory, 'first') + const secondOutputDirectory = join(directory, 'second') + await writeOutput(firstOutputDirectory) + await cp(firstOutputDirectory, secondOutputDirectory, { recursive: true }) + return { directory, firstOutputDirectory, secondOutputDirectory } +} + +describe('SSH relay runtime clean-build reproducibility', () => { + it('accepts byte-and-mode-identical complete outputs', async () => { + const fixture = await createOutputs() + + await expect( + verifySshRelayRuntimeReproducibility({ + ...fixture, + tuple + }) + ).resolves.toEqual(expect.objectContaining({ tuple, contentId, archiveSha256, files: 7 })) + }) + + it('rejects runtime-tree byte drift', async () => { + const fixture = await createOutputs() + await writeFile(join(fixture.secondOutputDirectory, 'runtime', 'relay.js'), 'other bytes') + + await expect(verifySshRelayRuntimeReproducibility({ ...fixture, tuple })).rejects.toThrow( + 'reproducibility mismatch for runtime/relay.js: sha256' + ) + }) + + it('rejects SBOM byte drift', async () => { + const fixture = await createOutputs() + await writeFile( + join(fixture.secondOutputDirectory, `orca-ssh-relay-runtime-${tuple}.spdx.json`), + '{"spdxVersion":"SPDX-2.2"}\n' + ) + + await expect(verifySshRelayRuntimeReproducibility({ ...fixture, tuple })).rejects.toThrow( + `reproducibility mismatch for orca-ssh-relay-runtime-${tuple}.spdx.json: sha256` + ) + }) + + it('rejects an archive that disagrees with its identity', async () => { + const fixture = await createOutputs() + await writeFile(join(fixture.secondOutputDirectory, archiveName), 'other archive bytes') + + await expect(verifySshRelayRuntimeReproducibility({ ...fixture, tuple })).rejects.toThrow( + 'second output archive digest does not match its identity' + ) + }) + + it('rejects a missing published metadata asset', async () => { + const fixture = await createOutputs() + await rm(join(fixture.secondOutputDirectory, `orca-ssh-relay-runtime-${tuple}.spdx.json`)) + + await expect(verifySshRelayRuntimeReproducibility({ ...fixture, tuple })).rejects.toThrow( + `second output is missing required asset: orca-ssh-relay-runtime-${tuple}.spdx.json` + ) + }) + + it.skipIf(process.platform === 'win32')('rejects mode drift', async () => { + const fixture = await createOutputs() + await chmod(join(fixture.secondOutputDirectory, 'runtime', 'relay.js'), 0o755) + + await expect(verifySshRelayRuntimeReproducibility({ ...fixture, tuple })).rejects.toThrow( + 'reproducibility mismatch for runtime/relay.js: mode' + ) + }) + + it.skipIf(process.platform === 'win32')( + 'rejects links before reading their targets', + async () => { + const fixture = await createOutputs() + await symlink( + join(fixture.directory, 'outside'), + join(fixture.firstOutputDirectory, 'runtime', 'escape') + ) + + await expect(verifySshRelayRuntimeReproducibility({ ...fixture, tuple })).rejects.toThrow( + 'first output contains a prohibited special entry: runtime/escape' + ) + } + ) + + it('rejects the same directory and a pre-aborted comparison', async () => { + const fixture = await createOutputs() + await expect( + verifySshRelayRuntimeReproducibility({ + firstOutputDirectory: fixture.firstOutputDirectory, + secondOutputDirectory: fixture.firstOutputDirectory, + tuple + }) + ).rejects.toThrow('clean-build outputs must be distinct directories') + await expect( + verifySshRelayRuntimeReproducibility({ + ...fixture, + tuple, + signal: AbortSignal.abort() + }) + ).rejects.toThrow('aborted') + }) +}) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 3fc07e0ff8d..e505f7887f1 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -35,12 +35,13 @@ describe('SSH relay runtime artifact workflow', () => { } }) - it('uploads only archive and metadata evidence after executable verification', async () => { + it('uploads only the first output after two clean builds verify and compare', async () => { const source = await readFile(workflowUrl, 'utf8') const workflow = parse(source) const steps = workflow.jobs['build-posix-runtime'].steps + const windowsSteps = workflow.jobs['build-windows-runtime'].steps const buildIndex = steps.findIndex( - (step) => step.name === 'Build, inspect, and smoke exact runtime' + (step) => step.name === 'Build twice, inspect, smoke, and compare exact runtime' ) const uploadIndex = steps.findIndex( (step) => step.name === 'Upload unpublished artifact evidence' @@ -52,12 +53,16 @@ describe('SSH relay runtime artifact workflow', () => { expect(source).toContain('ssh-relay-runtime-workflow.test.mjs') expect(source).toContain('pnpm install --frozen-lockfile --ignore-scripts') expect(source).toContain('--connect-timeout 20 --max-time 300 --retry 2') - expect(source).toContain('mkdir -p "$(dirname "$output")"') + expect(source).toContain('mkdir -p "$output_root"') expect(source).toContain('source_commit=$(git rev-parse HEAD)') expect(source).toContain('--git-commit "$source_commit"') expect(source).not.toContain('--git-commit "$GITHUB_SHA"') - expect(source).toContain('cp "$output"/*.tar.xz') - expect(source).toContain("Get-ChildItem -LiteralPath $output -Filter '*.zip'") + expect(source).toContain('for output in "$first_output" "$second_output"') + expect(source).toContain('foreach ($output in @($firstOutput, $secondOutput))') + expect(source).toContain('ssh-relay-runtime-reproducibility.mjs') + expect(source).toContain('ssh-relay-runtime-reproducibility.test.mjs') + expect(source).toContain('cp "$first_output"/*.tar.xz') + expect(source).toContain("Get-ChildItem -LiteralPath $firstOutput -Filter '*.zip'") expect(source).toContain('ssh-relay-node-zip-inspection.test.mjs') expect(source).toContain('ssh-relay-runtime-pty-smoke.test.mjs') expect(source).toContain('ssh-relay-runtime-resource-diagnostics.test.mjs') @@ -65,6 +70,17 @@ describe('SSH relay runtime artifact workflow', () => { expect(source).toContain('node-v24.18.0-headers.tar.gz') expect(source).toContain('node_library: win-x64/node.lib') expect(source).toContain("@('gpg.exe', 'gpgv.exe')") + for (const jobSteps of [steps, windowsSteps]) { + const run = jobSteps.find( + (step) => step.name === 'Build twice, inspect, smoke, and compare exact runtime' + ).run + expect(run.indexOf('ssh-relay-runtime-reproducibility.mjs')).toBeGreaterThan( + run.indexOf('verify-ssh-relay-runtime.mjs') + ) + expect(run.indexOf('runtime-evidence/${{ matrix.tuple }}')).toBeGreaterThan( + run.indexOf('ssh-relay-runtime-reproducibility.mjs') + ) + } expect(steps[uploadIndex].with.path).toBe('runtime-evidence/${{ matrix.tuple }}/') expect(source).not.toMatch(/releases\/|gh release|contents:\s*write/i) }) 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 776ae307de6..72dd4cd7ae6 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — add and prove a same-head, same-runner two-clean-build identity oracle now that all six glibc/macOS/Windows target-native artifact and executable cells are green; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — the bounded same-head, same-runner two-clean-build identity oracle is locally green and awaits exact-head execution on all six glibc/macOS/Windows native runners; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -87,8 +87,10 @@ same change as the work it records. diff gates. The first exact-head native run exposed a macOS `/var` versus `/private/var` watcher oracle mismatch under E-M3-CI-RED-001. The corrected exact-head run passed target-native build, archive/tree verification, bundled Node, patched PTY, and watcher smoke for Linux x64/arm64 and - macOS x64/arm64 under E-M3-CI-001. Same-runner clean-rebuild identity, oldest-baseline, - native-trust, SSH, Windows, and musl cells remain open. + macOS x64/arm64 under E-M3-CI-001. A bounded comparator now rejects incomplete outputs and any + runtime-tree, archive, identity, SPDX, provenance, type, mode, size, or digest drift after two + independently verified clean builds; its focused contract is locally green, but exact-head native + execution remains pending. Oldest-baseline, native-trust, SSH, and musl cells remain open. - Windows input correction: E-M3-WINDOWS-INPUT-GAP-001 proved the official Windows ZIP lacks headers and `node.lib`. Both artifacts now require the exact signed headers archive and tuple import library as explicit inputs. The schema, signed-checksum verifier, bounded ZIP/header extraction, @@ -125,11 +127,12 @@ same change as the work it records. - Rollout control: existing per-SSH-target configuration; legacy is the default and the bundled runtime is an explicit per-target Beta opt-in under E-M1-ROLLOUT-DECISION-001. - Legacy fallback removal: not authorized. -- Next required action: add a same-head/same-runner two-clean-build identity comparison that fails - closed on any tree, archive, SBOM, provenance, or identity drift, then run it for every target- - native candidate. Keep oldest-baseline, cross-family remote infrastructure, signing/trust, and - measured legacy baseline gates open; do not introduce publication, resolver, transfer, rollout, - tuple enablement, or default behavior. +- Next required action: finish the exact implementation-commit local gates, push the bounded + two-clean-build comparator/workflow package to draft PR #8741, and require every target-native job + to compare complete runtime-tree, archive, identity, SPDX, and provenance outputs before upload. + Record every native drift instead of weakening the oracle. Keep oldest-baseline, cross-family + remote infrastructure, signing/trust, and measured legacy baseline gates open; do not introduce + publication, resolver, transfer, rollout, tuple enablement, or default behavior. ## Non-Negotiable Invariants @@ -597,8 +600,11 @@ transfer, install, fallback, rollout setting, or tuple enablement belongs in thi **Active sub-gate — 2026-07-14, Codex implementation owner:** exact-head run 29345126283 proves all six target-native artifact/executable jobs, including normal Windows x64/arm64 settlement and -unpublished upload. Add a bounded same-head/same-runner two-clean-build comparison without changing -artifact consumers, publication, the repository-wide node-pty patch, or the legacy/default path. +unpublished upload. The locally green bounded comparator now requires two independently verified +clean outputs and exact type/mode/size/SHA-256 equality for the runtime tree, archive, identity, +SPDX, and provenance before uploading only the first output. Exact-head native execution remains +open; artifact consumers, publication, the repository-wide node-pty patch, and the legacy/default +path remain unchanged. Each runtime must contain only the executable closure required by the relay. @@ -1258,6 +1264,7 @@ focused commands as their scripts/tests are introduced. - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-pty-smoke.test.mjs` (E-M3-WINDOWS-SMOKE-SETTLEMENT-LOCAL-RED-001, E-M3-WINDOWS-SMOKE-SETTLEMENT-LOCAL-001; native evidence remains pending) - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs` (E-M3-WINDOWS-RESOURCE-DIAGNOSTIC-LOCAL-001; native classification pending) - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs` (E-M3-WINDOWS-CONPTY-WORKER-LOCAL-RED-001, E-M3-WINDOWS-CONPTY-WORKER-LOCAL-001; native settlement pending) +- [ ] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-reproducibility.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` (locally green on the uncommitted implementation tree; rerun on the exact implementation commit before checking) - [x] `node config/scripts/verify-ssh-relay-node-release-inputs.mjs --inputs-directory --archive win32-x64` (E-M3-WINDOWS-INPUT-001; signed real Node ZIP, headers, and import library; no Windows execution) - [x] `node config/scripts/build-ssh-relay-runtime.mjs --tuple linux-arm64-glibc --inputs-directory --output-directory --source-date-epoch --git-commit ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) - [x] `node config/scripts/verify-ssh-relay-runtime.mjs --runtime-directory --identity --archive ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) @@ -3599,10 +3606,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the bounded post-smoke resource diagnostic, then capture both native Windows -resource snapshots through the existing parent timeout without making another cleanup change. Use -that evidence to isolate the owning subsystem before requiring exact-head normal exit and artifact -upload. The separate clean-build identity gate, cross-family Layer B target pool, protected -manifest-signing environment, and paired legacy performance baseline remain release/default-path -blockers; no publication, desktop resolver, SSH transfer/install, per-target Beta, fallback, tuple -enablement, or default behavior may be connected by this package. +Complete the exact implementation-commit local gates, push the bounded two-clean-build +reproducibility package to draft PR #8741, and run all six target-native jobs. Each job must build, +verify, and smoke two exclusive outputs, compare every runtime-tree/archive/identity/SPDX/provenance +entry, and upload only after equality. Record exact differing paths and fields for every native red +result and correct the producer without weakening the oracle. 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 publication, desktop resolver, SSH +transfer/install, per-target Beta, fallback, tuple enablement, or default behavior may be connected +by this package. From 09b047f4eee224b45ca337b46b8402016290a1a9 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 11:52:07 -0400 Subject: [PATCH 022/267] docs(ssh): record clean-build oracle gate Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 107 ++++++++++++++---- 1 file changed, 88 insertions(+), 19 deletions(-) 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 72dd4cd7ae6..09fd779be96 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 @@ -89,8 +89,9 @@ same change as the work it records. archive/tree verification, bundled Node, patched PTY, and watcher smoke for Linux x64/arm64 and macOS x64/arm64 under E-M3-CI-001. A bounded comparator now rejects incomplete outputs and any runtime-tree, archive, identity, SPDX, provenance, type, mode, size, or digest drift after two - independently verified clean builds; its focused contract is locally green, but exact-head native - execution remains pending. Oldest-baseline, native-trust, SSH, and musl cells remain open. + independently verified clean builds; its exact implementation-commit contract and static gates + are locally green under E-M3-REPRODUCIBILITY-LOCAL-001, but exact-head native execution remains + pending. Oldest-baseline, native-trust, SSH, and musl cells remain open. - Windows input correction: E-M3-WINDOWS-INPUT-GAP-001 proved the official Windows ZIP lacks headers and `node.lib`. Both artifacts now require the exact signed headers archive and tuple import library as explicit inputs. The schema, signed-checksum verifier, bounded ZIP/header extraction, @@ -600,11 +601,11 @@ transfer, install, fallback, rollout setting, or tuple enablement belongs in thi **Active sub-gate — 2026-07-14, Codex implementation owner:** exact-head run 29345126283 proves all six target-native artifact/executable jobs, including normal Windows x64/arm64 settlement and -unpublished upload. The locally green bounded comparator now requires two independently verified -clean outputs and exact type/mode/size/SHA-256 equality for the runtime tree, archive, identity, -SPDX, and provenance before uploading only the first output. Exact-head native execution remains -open; artifact consumers, publication, the repository-wide node-pty patch, and the legacy/default -path remain unchanged. +unpublished upload. E-M3-REPRODUCIBILITY-LOCAL-001 proves the bounded comparator requires two +independently verified clean outputs and exact type/mode/size/SHA-256 equality for the runtime tree, +archive, identity, SPDX, and provenance before uploading only the first output. Exact-head native +execution remains open; artifact consumers, publication, the repository-wide node-pty patch, and +the legacy/default path remain unchanged. Each runtime must contain only the executable closure required by the relay. @@ -1204,16 +1205,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; reproducibility active | `d9a556b9c`; E-M3-NODE-PROVENANCE-001, E-M3-RUNTIME-LOCAL-001, E-M3-CI-001, E-M3-WINDOWS-CI-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 reproducibility pending | `c70e96374`; E-M3-REPRODUCIBILITY-LOCAL-001, E-M3-CI-001, E-M3-WINDOWS-CI-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 | — | Every PR must document: @@ -1264,7 +1265,7 @@ focused commands as their scripts/tests are introduced. - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-pty-smoke.test.mjs` (E-M3-WINDOWS-SMOKE-SETTLEMENT-LOCAL-RED-001, E-M3-WINDOWS-SMOKE-SETTLEMENT-LOCAL-001; native evidence remains pending) - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs` (E-M3-WINDOWS-RESOURCE-DIAGNOSTIC-LOCAL-001; native classification pending) - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs` (E-M3-WINDOWS-CONPTY-WORKER-LOCAL-RED-001, E-M3-WINDOWS-CONPTY-WORKER-LOCAL-001; native settlement pending) -- [ ] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-reproducibility.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` (locally green on the uncommitted implementation tree; rerun on the exact implementation commit before checking) +- [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-reproducibility.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` (E-M3-REPRODUCIBILITY-LOCAL-001; synthetic comparator/workflow contract only, native execution pending) - [x] `node config/scripts/verify-ssh-relay-node-release-inputs.mjs --inputs-directory --archive win32-x64` (E-M3-WINDOWS-INPUT-001; signed real Node ZIP, headers, and import library; no Windows execution) - [x] `node config/scripts/build-ssh-relay-runtime.mjs --tuple linux-arm64-glibc --inputs-directory --output-directory --source-date-epoch --git-commit ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) - [x] `node config/scripts/verify-ssh-relay-runtime.mjs --runtime-directory --identity --archive ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) @@ -3549,6 +3550,74 @@ or unexpected token`; the first logs did not identify a source location. deterministic Windows ZIP assembly and native Windows jobs, and leave all tuples disabled until the remaining trust, baseline, and two-layer live SSH gates pass. +### E-M3-REPRODUCIBILITY-LOCAL-001 — Bounded two-clean-build identity oracle + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `c70e96374cafb681c52b3d60e42322ba5a76791c` in stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741); native PR CI pending +- Runner: macOS 26.2 build 25C56 arm64, native; Node v26.0.0 and pnpm 10.24.0. Node v24.18.0 on + each target-native GitHub runner remains the authoritative artifact environment. +- Remote and transport: none; synthetic output trees and parsed GitHub Actions workflow only +- Exact commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.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 \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm run typecheck + pnpm exec oxlint config/scripts/ssh-relay-runtime-reproducibility.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check .github/workflows/ssh-relay-runtime-artifacts.yml \ + config/scripts/ssh-relay-runtime-reproducibility.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md + git diff HEAD^ --check + ``` + +- Result: PASS. The purpose-named comparator/workflow command passed 10/10 tests in 200 ms; the + complete artifact set passed 40/40 tests across 11 suites in 1.09 seconds. Typecheck, focused + oxlint, the 355-entry max-lines ratchet, full repository lint/reliability/localization, formatting, + and exact-commit diff checks all exited zero. Full lint emitted only pre-existing warnings outside + this package. +- Duration and resource metrics: purpose suite 0.87 seconds wall; full artifact suite 3.02 seconds; + typecheck 8.47 seconds; focused oxlint 1.94 seconds; max-lines 4.95 seconds; full lint 26.34 seconds; + formatting 5.56 seconds; diff check 90 ms. Comparator file reads are incremental, bounded to 250 + MiB per file and 500 MiB per output, with 6,000 entries, 512-byte paths, depth 40, a five-minute + AbortSignal, and sequential tree walks. Peak RSS/open files were not instrumented for synthetic + fixtures; native CI remains responsible for real artifact duration and size evidence. +- Artifact/log/trace link: exact source and test files in commit `c70e96374`; durable target-native + run and job links pending +- Oracle proved: distinct real output roots are required; symlinks and special files reject before + hashing; complete runtime/archive/identity/SPDX/provenance output is mandatory; archive bytes must + match their identity; any path type, mode, size, or SHA-256 drift fails closed. Both POSIX and + Windows jobs build and independently verify/smoke two exclusive outputs, compare only afterward, + and copy/upload only the first verified equal output. The workflow retains read-only permissions, + exact-head checkout, SHA-pinned actions, explicit runner labels, and no release publication path. +- Does not prove: any real native build is reproducible, Node v24 execution, compiler/linker + determinism, Linux/macOS/Windows output equality, oldest baselines, native trust/signing, SSH, + publication, transfer, cache, fallback, UI, or an enabled tuple. +- Checklist items satisfied: purpose-named local reproducibility/workflow command and exact-commit + static handoff only; no per-tuple or native-trust checkbox changes. +- Follow-up: push the exact head, run all six native jobs, and record the exact differing path/field + for every red result before correcting producer nondeterminism without weakening this oracle. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -3559,7 +3628,7 @@ owner and promotion condition. | Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | | No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | | Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | -| Native clean-rebuild identity unproved | One successful artifact per corrected-head hosted runner | Toolchain drift may change native content IDs | Codex implementation owner | Same-head, same-runner clean builds match or a reviewed reproducibility policy | Open | +| Native clean-rebuild identity unproved | Bounded local oracle is green; six target-native comparisons pending | Toolchain drift may change native content IDs | Codex implementation owner | Same-head, same-runner clean builds match or a reviewed reproducibility policy | Open | | Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | | Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | | Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | From e58913d1a571f4c578a7fe1dc51ef5c93e9b3136 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 12:05:53 -0400 Subject: [PATCH 023/267] test(ssh): improve native reproducibility diagnostics Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 6 ++++++ .../ssh-relay-runtime-reproducibility.mjs | 11 ++++++++--- .../ssh-relay-runtime-reproducibility.test.mjs | 17 +++++++++++++++++ .../scripts/ssh-relay-runtime-workflow.test.mjs | 3 +++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 21e36216a16..48a82e1c950 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -103,6 +103,8 @@ jobs: shell: bash run: | set -euo pipefail + node --check config/scripts/ssh-relay-runtime-reproducibility.mjs + node --check config/scripts/ssh-relay-runtime-reproducibility.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 \ @@ -258,6 +260,10 @@ jobs: shell: pwsh run: | $ErrorActionPreference = 'Stop' + node --check config/scripts/ssh-relay-runtime-reproducibility.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime reproducibility script syntax check failed' } + node --check config/scripts/ssh-relay-runtime-reproducibility.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime reproducibility 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 ` diff --git a/config/scripts/ssh-relay-runtime-reproducibility.mjs b/config/scripts/ssh-relay-runtime-reproducibility.mjs index febadab9db0..f3217a7650d 100644 --- a/config/scripts/ssh-relay-runtime-reproducibility.mjs +++ b/config/scripts/ssh-relay-runtime-reproducibility.mjs @@ -1,5 +1,3 @@ -#!/usr/bin/env node - import { createHash } from 'node:crypto' import { createReadStream } from 'node:fs' import { lstat, opendir, readFile, realpath } from 'node:fs/promises' @@ -148,7 +146,14 @@ async function validateCompleteOutput(directory, label, tuple, snapshot) { } function compareSnapshots(first, second) { - const paths = [...new Set([...first.entries.keys(), ...second.entries.keys()])].sort() + const paths = [...new Set([...first.entries.keys(), ...second.entries.keys()])].sort( + (left, right) => { + // Runtime bytes are the producer root cause; metadata and archive names often drift after them. + const leftPriority = left === 'runtime' || left.startsWith('runtime/') ? 0 : 1 + const rightPriority = right === 'runtime' || right.startsWith('runtime/') ? 0 : 1 + return leftPriority - rightPriority || left.localeCompare(right) + } + ) for (const path of paths) { const left = first.entries.get(path) const right = second.entries.get(path) diff --git a/config/scripts/ssh-relay-runtime-reproducibility.test.mjs b/config/scripts/ssh-relay-runtime-reproducibility.test.mjs index 0a346a6f6c9..d14e9a0b752 100644 --- a/config/scripts/ssh-relay-runtime-reproducibility.test.mjs +++ b/config/scripts/ssh-relay-runtime-reproducibility.test.mjs @@ -71,6 +71,23 @@ describe('SSH relay runtime clean-build reproducibility', () => { ) }) + it('reports runtime drift before derived identity drift', async () => { + const fixture = await createOutputs() + await writeFile(join(fixture.secondOutputDirectory, 'runtime', 'relay.js'), 'other bytes') + const identityPath = join( + fixture.secondOutputDirectory, + `orca-ssh-relay-runtime-${tuple}.identity.json` + ) + await writeFile( + identityPath, + `${JSON.stringify({ tupleId: tuple, contentId: `sha256:${'c'.repeat(64)}`, archive: { name: archiveName, sha256: archiveSha256 } })}\n` + ) + + await expect(verifySshRelayRuntimeReproducibility({ ...fixture, tuple })).rejects.toThrow( + 'reproducibility mismatch for runtime/relay.js: sha256' + ) + }) + it('rejects SBOM byte drift', async () => { const fixture = await createOutputs() await writeFile( diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index e505f7887f1..0e370504669 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -61,6 +61,9 @@ describe('SSH relay runtime artifact workflow', () => { expect(source).toContain('foreach ($output in @($firstOutput, $secondOutput))') expect(source).toContain('ssh-relay-runtime-reproducibility.mjs') expect(source).toContain('ssh-relay-runtime-reproducibility.test.mjs') + expect(source).toContain( + 'node --check config/scripts/ssh-relay-runtime-reproducibility.test.mjs' + ) expect(source).toContain('cp "$first_output"/*.tar.xz') expect(source).toContain("Get-ChildItem -LiteralPath $firstOutput -Filter '*.zip'") expect(source).toContain('ssh-relay-node-zip-inspection.test.mjs') From c62b0d0a9580b132fec5f11c3234a983503a011c Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 12:08:20 -0400 Subject: [PATCH 024/267] docs(ssh): record native reproducibility red Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 161 ++++++++++++++++-- 1 file changed, 148 insertions(+), 13 deletions(-) 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 09fd779be96..63f487e6b6f 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — the bounded same-head, same-runner two-clean-build identity oracle is locally green and awaits exact-head execution on all six glibc/macOS/Windows native runners; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — completed exact-head two-clean-build run 29347236627 is an evidence-producing red: both Linux cells passed, both macOS cells exposed reproducibility drift, and both Windows cells exposed a comparator test-collection syntax failure; bounded diagnostic corrections are local only and await a new exact-head run; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -90,8 +90,14 @@ same change as the work it records. macOS x64/arm64 under E-M3-CI-001. A bounded comparator now rejects incomplete outputs and any runtime-tree, archive, identity, SPDX, provenance, type, mode, size, or digest drift after two independently verified clean builds; its exact implementation-commit contract and static gates - are locally green under E-M3-REPRODUCIBILITY-LOCAL-001, but exact-head native execution remains - pending. Oldest-baseline, native-trust, SSH, and musl cells remain open. + are locally green under E-M3-REPRODUCIBILITY-LOCAL-001. Exact-head run + [29347236627](https://github.com/stablyai/orca/actions/runs/29347236627) passed both Linux cells, + exposed macOS x64/arm64 runtime identity drift, and failed Windows x64/arm64 while collecting the + new comparator suite under E-M3-REPRODUCIBILITY-CI-RED-001. + E-M3-REPRODUCIBILITY-DIAGNOSTIC-LOCAL-001 removes the comparator shebang, checks both new files + with `node --check` on POSIX and Windows, and reports runtime-tree drift before derived metadata + drift; native proof of those corrections remains open. Oldest-baseline, native-trust, SSH, and + musl cells remain open. - Windows input correction: E-M3-WINDOWS-INPUT-GAP-001 proved the official Windows ZIP lacks headers and `node.lib`. Both artifacts now require the exact signed headers archive and tuple import library as explicit inputs. The schema, signed-checksum verifier, bounded ZIP/header extraction, @@ -125,13 +131,16 @@ same change as the work it records. - Validation orchestration: GitHub Actions is the primary runner and evidence surface under E-M1-RUNNER-DECISION-001; exact native labels are recorded, while representative cross-family remote targets remain open. -- Rollout control: existing per-SSH-target configuration; legacy is the default and the bundled - runtime is an explicit per-target Beta opt-in under E-M1-ROLLOUT-DECISION-001. +- Rollout control: the Beta-tagged option lives on SSH target add/edit surfaces and is persisted per + target through existing target configuration. It is off by default for existing, new, imported, + missing, unknown, and malformed configurations, all of which resolve to legacy. Only an explicit + 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: finish the exact implementation-commit local gates, push the bounded - two-clean-build comparator/workflow package to draft PR #8741, and require every target-native job - to compare complete runtime-tree, archive, identity, SPDX, and provenance outputs before upload. - Record every native drift instead of weakening the oracle. Keep oldest-baseline, cross-family +- Next required action: finish the exact implementation-commit local gates for the bounded Windows + parser and diagnostic-order correction, then push and rerun all six target-native jobs. Use the + runtime-first comparator output to isolate + macOS producer nondeterminism without weakening the oracle. Keep oldest-baseline, cross-family remote infrastructure, signing/trust, and measured legacy baseline gates open; do not introduce publication, resolver, transfer, rollout, tuple enablement, or default behavior. @@ -603,9 +612,12 @@ transfer, install, fallback, rollout setting, or tuple enablement belongs in thi six target-native artifact/executable jobs, including normal Windows x64/arm64 settlement and unpublished upload. E-M3-REPRODUCIBILITY-LOCAL-001 proves the bounded comparator requires two independently verified clean outputs and exact type/mode/size/SHA-256 equality for the runtime tree, -archive, identity, SPDX, and provenance before uploading only the first output. Exact-head native -execution remains open; artifact consumers, publication, the repository-wide node-pty patch, and -the legacy/default path remain unchanged. +archive, identity, SPDX, and provenance before uploading only the first output. Exact-head run +29347236627 is intentionally red under E-M3-REPRODUCIBILITY-CI-RED-001: Linux x64/arm64 passed, +macOS x64/arm64 produced unequal clean outputs, and Windows x64/arm64 failed comparator test +collection. E-M3-REPRODUCIBILITY-DIAGNOSTIC-LOCAL-001 proves the bounded parser/diagnostic +correction locally; native execution is still open. Artifact consumers, publication, the +repository-wide node-pty patch, and the legacy/default path remain unchanged. Each runtime must contain only the executable closure required by the relay. @@ -3618,6 +3630,129 @@ or unexpected token`; the first logs did not identify a source location. - Follow-up: push the exact head, run all six native jobs, and record the exact differing path/field for every red result before correcting producer nondeterminism without weakening this oracle. +### E-M3-REPRODUCIBILITY-CI-RED-001 — First native clean-build comparison + +- Date: 2026-07-14 +- Commit SHA / PR: exact head `09b047f4eee224b45ca337b46b8402016290a1a9` in stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Run and jobs: [run 29347236627](https://github.com/stablyai/orca/actions/runs/29347236627); + Linux x64 `87134028593`, Linux arm64 `87134028669`, macOS arm64 `87134028547`, macOS x64 + `87134028651`, Windows x64 `87134028652`, Windows arm64 `87134028632` +- Runners: `ubuntu-24.04` x64, `ubuntu-24.04-arm` arm64, `macos-15-intel` x64, + `macos-15` arm64, `windows-2022` x64, and `windows-11-arm` arm64; Node v24.18.0. The macOS + arm64 failure ran on macOS 15.7.7 image `20260706.0213.1`; the x64 failure ran on macOS 15.7.7 + image `20260629.0276.1`. Both used Xcode 16.4 build 16F6. +- Remote and transport: none; GitHub-hosted target-native artifact jobs only +- Exact evidence commands: + + ```sh + gh run view 29347236627 --repo stablyai/orca \ + --json databaseId,headSha,status,conclusion,url,jobs + gh api -H 'Accept: application/vnd.github+json' \ + repos/stablyai/orca/actions/jobs//logs + ``` + +- Result: Linux x64 and arm64 independently built, verified, smoked, compared equal, and uploaded + unpublished evidence. macOS arm64 independently verified and smoked both outputs but failed the + equality gate: content IDs were + `sha256:f1bbc1de26d6e9aae3899754c4ac4aea694ad9f5197fcf1c773e29fc50401129` and + `sha256:c26eba0226e73542a0cd7aec0ca5fa7e3809051453e40095e47fe9d154ba944f`; archive + digests/sizes were + `sha256:9cd56317ff03eb4e89a811e46736dc7558e447b9f0cd2d7e20fdeb5e5c40114a` / + 24,714,656 bytes and + `sha256:cde8fa92470fdf73ec02e8e6ebbf23417e43d98b465fa9c6bdea4373be8c3e84` / + 24,714,700 bytes. macOS x64 likewise failed with content IDs + `sha256:b71d7d50bba78b6cf9e6dc24b006d48c709a784c7eb77f500e86ab5fe692690b` and + `sha256:ceae81924c97c917855af864239b87a7d2eecc28de7d00c21c32c47c5385354f`; + archive digests/sizes were + `sha256:01f7d4c6d54241e211c5063b89e92a3511433b18a50cb643bbfbe0384546a50e` / + 26,384,540 bytes and + `sha256:f98dfdead87f4884382fedc3e3bff0197a3b2aa6981c38107e198cb38e8bdf67` / + 26,384,576 bytes. The committed comparator first reported identity SHA-256 drift on both macOS + architectures. Windows x64 and arm64 stopped before downloading Node inputs or building artifacts + because Vitest collected zero comparator tests with `SyntaxError: Invalid or unexpected token`; + the other 10 suites passed with 31 tests passed and one skipped. +- Duration and resource metrics: Linux build/compare steps completed in 125 seconds (x64) and 131 + seconds (arm64); macOS arm64 failed after 123 seconds in the build/compare step. Windows contract + suites completed in 1.54 seconds on arm64 and before artifact assembly on both architectures. + Peak RSS, file/channel counts, and cancellation settlement were not instrumented by this gate. +- Artifact/log/trace link: run and job links above. The two Linux evidence artifacts are unpublished; + failed cells uploaded no runtime artifact. +- Oracle proved: the equality gate accepts both Linux glibc tuples and detects real macOS + clean-build drift; Windows contract collection fails before unverified artifact work can start. + No asset was published and no production or default path changed. +- Does not prove: Windows parser correction, macOS producer determinism, oldest baselines, native + trust/signing, SSH, SFTP/system-SSH, release publication, resolver/cache, + transfer/install, fallback, UI, or any enabled tuple. +- Checklist items satisfied: none newly complete; native reproducibility remains open. +- Follow-up: remove the comparator shebang, add explicit native `node --check` gates, report + runtime-tree drift before derived metadata, rerun all six jobs, and correct the macOS producer only + after the exact differing runtime file is identified. Never ignore or normalize unequal bytes in + the comparator. + +### E-M3-REPRODUCIBILITY-DIAGNOSTIC-LOCAL-001 — Windows parser and runtime-first drift diagnostics + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `e58913d1a571f4c578a7fe1dc51ef5c93e9b3136` in stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: macOS 26.2 build 25C56 arm64, native; Node v26.0.0 and pnpm 10.24.0. Node v24.18.0 + target-native GitHub execution remains pending. +- Remote and transport: none; local synthetic comparator fixtures and workflow source contract only +- Exact commands: + + ```sh + node --check config/scripts/ssh-relay-runtime-reproducibility.mjs + node --check config/scripts/ssh-relay-runtime-reproducibility.test.mjs + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.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 \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm run typecheck + pnpm exec oxlint config/scripts/ssh-relay-runtime-reproducibility.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check .github/workflows/ssh-relay-runtime-artifacts.yml \ + config/scripts/ssh-relay-runtime-reproducibility.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md + git diff --check + ``` + +- Result: PASS. Both new files passed `node --check`; the purpose suite passed 11/11 tests; the + complete artifact set passed 41/41 tests across 11 suites. Typecheck, focused oxlint, the + 355-entry max-lines ratchet, full repository lint/reliability/localization, formatting, and diff + checks exited zero. Full lint emitted only pre-existing warnings outside this package. +- Duration and resource metrics: syntax checks 60 ms and 50 ms; purpose suite 850 ms wall; complete + artifact suite 1.27 seconds wall; typecheck 2.00 seconds; focused oxlint 480 ms; max-lines 780 ms; + full lint 11.96 seconds. Synthetic fixture peak RSS/open files were not instrumented. +- Artifact/log/trace link: exact sources in commit `e58913d1a`; target-native rerun pending +- Oracle proved: the comparator and its test import parse locally without the shebang; both POSIX + and Windows workflow contract stages run explicit syntax checks before Vitest or artifact inputs; + a simultaneous runtime/identity drift reports the runtime-tree path first. The comparator still + compares every byte and does not normalize or ignore drift. +- Does not prove: Windows x64/arm64 parsing, any native build, the differing macOS runtime path, + macOS determinism, oldest baselines, native trust, SSH, publication, transfer, fallback, UI, or an + enabled tuple. +- Checklist items satisfied: no native or tuple box; this closes only the local diagnostic + correction required after E-M3-REPRODUCIBILITY-CI-RED-001. +- Follow-up: push the exact commit, rerun all six native jobs, require both Windows syntax checks to + pass, and use the macOS runtime-first failure path to correct producer nondeterminism. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -3628,7 +3763,7 @@ owner and promotion condition. | Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | | No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | | Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | -| Native clean-rebuild identity unproved | Bounded local oracle is green; six target-native comparisons pending | Toolchain drift may change native content IDs | Codex implementation owner | Same-head, same-runner clean builds match or a reviewed reproducibility policy | Open | +| Native clean-rebuild identity unproved | Linux x64/arm64 pass; macOS x64/arm64 and the Windows parser are red | Toolchain drift may change native content IDs | Codex implementation owner | All six same-head, same-runner clean builds match without weakening the oracle | Open | | Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | | Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | | Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | From a09b02ec4e623cc8cac8ece089e4ff734014dd5b Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 12:32:30 -0400 Subject: [PATCH 025/267] build(ssh): stabilize native runtime rebuilds Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 10 ++++ config/scripts/build-ssh-relay-runtime.mjs | 39 +++++++++++--- config/scripts/ssh-relay-node-pty-build.mjs | 54 +++++++++++++------ .../scripts/ssh-relay-node-pty-build.test.mjs | 35 ++++++++++++ ...lay-node-pty-windows-build-determinism.mjs | 31 +++++++++++ ...ode-pty-windows-build-determinism.test.mjs | 44 +++++++++++++++ .../scripts/ssh-relay-runtime-build.test.mjs | 51 ++++++++++++++++++ .../ssh-relay-runtime-workflow.test.mjs | 4 ++ 8 files changed, 244 insertions(+), 24 deletions(-) create mode 100644 config/scripts/ssh-relay-node-pty-build.test.mjs create mode 100644 config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs create mode 100644 config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs create mode 100644 config/scripts/ssh-relay-runtime-build.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 48a82e1c950..f233ef39ad9 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -108,9 +108,12 @@ jobs: 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 \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ config/scripts/ssh-relay-node-zip-inspection.test.mjs \ config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-build.test.mjs \ config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ @@ -140,6 +143,7 @@ jobs: mkdir -p "$output_root" first_output="$output_root/first" second_output="$output_root/second" + work_directory="$output_root/build-work" source_commit=$(git rev-parse HEAD) source_date_epoch=$(git show -s --format=%ct "$source_commit") for output in "$first_output" "$second_output"; do @@ -148,6 +152,7 @@ jobs: --tuple '${{ matrix.tuple }}' \ --inputs-directory "$NODE_INPUTS" \ --output-directory "$output" \ + --work-directory "$work_directory" \ --source-date-epoch "$source_date_epoch" \ --git-commit "$source_commit" identity="$output/orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json" @@ -267,9 +272,12 @@ jobs: 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 ` + config/scripts/ssh-relay-node-pty-build.test.mjs ` + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs ` config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs ` config/scripts/ssh-relay-node-zip-inspection.test.mjs ` config/scripts/ssh-relay-runtime-artifact.test.mjs ` + config/scripts/ssh-relay-runtime-build.test.mjs ` config/scripts/ssh-relay-runtime-pty-smoke.test.mjs ` config/scripts/ssh-relay-runtime-reproducibility.test.mjs ` config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs ` @@ -310,6 +318,7 @@ jobs: New-Item -ItemType Directory -Path $outputRoot -Force | Out-Null $firstOutput = Join-Path $outputRoot 'first' $secondOutput = Join-Path $outputRoot 'second' + $workDirectory = Join-Path $outputRoot 'build-work' $sourceCommit = git rev-parse HEAD $sourceDateEpoch = git show -s --format=%ct $sourceCommit foreach ($output in @($firstOutput, $secondOutput)) { @@ -318,6 +327,7 @@ jobs: --tuple '${{ matrix.tuple }}' ` --inputs-directory $env:NODE_INPUTS ` --output-directory $output ` + --work-directory $workDirectory ` --source-date-epoch $sourceDateEpoch ` --git-commit $sourceCommit if ($LASTEXITCODE -ne 0) { throw 'runtime build failed' } diff --git a/config/scripts/build-ssh-relay-runtime.mjs b/config/scripts/build-ssh-relay-runtime.mjs index 346481f5c06..8bf4725b455 100644 --- a/config/scripts/build-ssh-relay-runtime.mjs +++ b/config/scripts/build-ssh-relay-runtime.mjs @@ -1,9 +1,8 @@ #!/usr/bin/env node import { execFile } from 'node:child_process' -import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { dirname, join, resolve } from 'node:path' +import { mkdir, readFile, rm } from 'node:fs/promises' +import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path' import { promisify } from 'node:util' import { buildPatchedSshRelayNodePty } from './ssh-relay-node-pty-build.mjs' @@ -51,6 +50,8 @@ export function parseBuildArguments(argv) { result.inputsDirectory = resolve(value) } else if (flag === '--output-directory') { result.outputDirectory = resolve(value) + } else if (flag === '--work-directory') { + result.workDirectory = resolve(value) } else if (flag === '--contract') { result.contractPath = resolve(value) } else if (flag === '--source-date-epoch') { @@ -66,6 +67,7 @@ export function parseBuildArguments(argv) { 'tuple', 'inputsDirectory', 'outputDirectory', + 'workDirectory', 'sourceDateEpoch', 'gitCommit' ]) { @@ -85,6 +87,20 @@ export function parseBuildArguments(argv) { return result } +function containsPath(parent, candidate) { + const path = relative(parent, candidate) + return path === '' || (path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path)) +} + +export function assertDisjointRuntimeBuildDirectories(outputDirectory, workDirectory) { + if ( + containsPath(outputDirectory, workDirectory) || + containsPath(workDirectory, outputDirectory) + ) { + throw new Error('Runtime output and work directories must be disjoint') + } +} + function assertTargetNative(tuple) { const os = tuple.startsWith('linux-') ? 'linux' : tuple.startsWith('darwin-') ? 'darwin' : 'win32' const architecture = tuple.includes('arm64') ? 'arm64' : 'x64' @@ -153,6 +169,7 @@ function relayPlatform(tuple) { export async function buildSshRelayRuntime(options) { const started = process.hrtime.bigint() assertTargetNative(options.tuple) + assertDisjointRuntimeBuildDirectories(options.outputDirectory, options.workDirectory) const release = validateSshRelayNodeReleaseContract( JSON.parse(await readFile(options.contractPath, 'utf8')) ) @@ -161,11 +178,17 @@ export async function buildSshRelayRuntime(options) { inputsDirectory: options.inputsDirectory, archiveTuples: [options.tuple] }) - const workDirectory = await mkdtemp(join(tmpdir(), `orca-runtime-${options.tuple}-`)) - // Why: the final directory is still created exclusively, but callers need not pre-create parents. - await mkdir(dirname(options.outputDirectory), { recursive: true }) + const workDirectory = options.workDirectory + // Why: both final and clean-build paths are exclusive, but callers need not pre-create parents. + await Promise.all([ + mkdir(dirname(options.outputDirectory), { recursive: true }), + mkdir(dirname(workDirectory), { recursive: true }) + ]) await mkdir(options.outputDirectory) + let workDirectoryCreated = false try { + await mkdir(workDirectory) + workDirectoryCreated = true await run(process.execPath, [resolve(scriptDirectory, 'build-relay.mjs')], { cwd: projectRoot }) const extractedDirectory = join(workDirectory, 'node-inputs') const nodeArchivePath = join(options.inputsDirectory, release.archives[options.tuple].name) @@ -244,7 +267,9 @@ export async function buildSshRelayRuntime(options) { await rm(options.outputDirectory, { recursive: true, force: true }) throw error } finally { - await rm(workDirectory, { recursive: true, force: true }) + if (workDirectoryCreated) { + await rm(workDirectory, { recursive: true, force: true }) + } } } diff --git a/config/scripts/ssh-relay-node-pty-build.mjs b/config/scripts/ssh-relay-node-pty-build.mjs index 2d8bd2b3bfb..88024154e27 100644 --- a/config/scripts/ssh-relay-node-pty-build.mjs +++ b/config/scripts/ssh-relay-node-pty-build.mjs @@ -5,6 +5,7 @@ import { dirname, join, resolve } from 'node:path' import { promisify } from 'node:util' import { applyWindowsNodePtySettlement } from './ssh-relay-node-pty-windows-settlement.mjs' +import { applyWindowsNodePtyBuildDeterminism } from './ssh-relay-node-pty-windows-build-determinism.mjs' const require = createRequire(import.meta.url) const execFileAsync = promisify(execFile) @@ -14,6 +15,21 @@ const BUILD_TIMEOUT_MS = 10 * 60 * 1000 const COMMAND_TIMEOUT_MS = 60 * 1000 const MAX_COMMAND_OUTPUT_BYTES = 16 * 1024 * 1024 +export function nodePtyNativeBuildCommands({ nodePath, nodeRoot, tuple }) { + const nodeGypArguments = ['--release', `--nodedir=${nodeRoot}`] + if (!tuple.startsWith('darwin-')) { + return [{ command: nodePath, args: [NODE_GYP_PATH, 'rebuild', ...nodeGypArguments] }] + } + return [ + { command: nodePath, args: [NODE_GYP_PATH, 'configure', ...nodeGypArguments] }, + { + command: 'make', + // Why: ld must retain a loadable UUID while ignoring incidental inputs such as build time. + args: ['-C', 'build', 'BUILDTYPE=Release', 'LDFLAGS.target=-Wl,-reproducible'] + } + ] +} + async function runCommand(command, args, options = {}) { // Why: native build tools can be noisy or hang; both cases must settle within explicit bounds. const result = await execFileAsync(command, args, { @@ -134,6 +150,7 @@ export async function buildPatchedSshRelayNodePty({ dereference: true, filter: sourceFilter }) + await applyWindowsNodePtyBuildDeterminism({ nodePtyDirectory: buildDirectory, tuple }) await applyWindowsNodePtySettlement({ nodePtyLibraryDirectory: join(buildDirectory, 'lib'), tuple @@ -147,23 +164,26 @@ export async function buildPatchedSshRelayNodePty({ const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), BUILD_TIMEOUT_MS) try { - await runCommand(nodePath, [NODE_GYP_PATH, 'rebuild', '--release', `--nodedir=${nodeRoot}`], { - cwd: buildDirectory, - signal: controller.signal, - timeout: BUILD_TIMEOUT_MS, - windowsHide: true, - env: { - ...process.env, - PATH: `${dirname(nodePath)}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH ?? ''}`, - npm_config_arch: tuple.includes('arm64') ? 'arm64' : 'x64', - npm_config_build_from_source: 'true', - npm_config_nodedir: nodeRoot, - // Why: a successful build must prove all Node inputs were staged, never fetched implicitly. - npm_config_offline: 'true', - npm_config_disturl: 'http://127.0.0.1:9', - npm_config_target: nodeVersion - } - }) + const buildEnvironment = { + ...process.env, + PATH: `${dirname(nodePath)}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH ?? ''}`, + npm_config_arch: tuple.includes('arm64') ? 'arm64' : 'x64', + npm_config_build_from_source: 'true', + npm_config_nodedir: nodeRoot, + // Why: a successful build must prove all Node inputs were staged, never fetched implicitly. + npm_config_offline: 'true', + npm_config_disturl: 'http://127.0.0.1:9', + npm_config_target: nodeVersion + } + for (const buildCommand of nodePtyNativeBuildCommands({ nodePath, nodeRoot, tuple })) { + await runCommand(buildCommand.command, buildCommand.args, { + cwd: buildDirectory, + signal: controller.signal, + timeout: BUILD_TIMEOUT_MS, + windowsHide: true, + env: buildEnvironment + }) + } } finally { clearTimeout(timeout) } diff --git a/config/scripts/ssh-relay-node-pty-build.test.mjs b/config/scripts/ssh-relay-node-pty-build.test.mjs new file mode 100644 index 00000000000..9a8f567cf58 --- /dev/null +++ b/config/scripts/ssh-relay-node-pty-build.test.mjs @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest' + +import { nodePtyNativeBuildCommands } from './ssh-relay-node-pty-build.mjs' + +const nodePath = '/staged/node' +const nodeRoot = '/staged/node-root' + +describe('SSH relay node-pty native build commands', () => { + it('uses the ordinary node-gyp rebuild on Linux and Windows', () => { + for (const tuple of ['linux-x64-glibc', 'linux-arm64-glibc', 'win32-x64', 'win32-arm64']) { + const commands = nodePtyNativeBuildCommands({ nodePath, nodeRoot, tuple }) + expect(commands).toHaveLength(1) + expect(commands[0]).toMatchObject({ command: nodePath }) + expect(commands[0].args).toEqual( + expect.arrayContaining(['rebuild', '--release', `--nodedir=${nodeRoot}`]) + ) + expect(commands[0].args).not.toContain('LDFLAGS.target=-Wl,-reproducible') + } + }) + + it('configures macOS before reproducibly linking a loadable Mach-O UUID', () => { + for (const tuple of ['darwin-x64', 'darwin-arm64']) { + const commands = nodePtyNativeBuildCommands({ nodePath, nodeRoot, tuple }) + expect(commands).toHaveLength(2) + expect(commands[0]).toMatchObject({ command: nodePath }) + expect(commands[0].args).toEqual( + expect.arrayContaining(['configure', '--release', `--nodedir=${nodeRoot}`]) + ) + expect(commands[1]).toEqual({ + command: 'make', + args: ['-C', 'build', 'BUILDTYPE=Release', 'LDFLAGS.target=-Wl,-reproducible'] + }) + } + }) +}) diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs new file mode 100644 index 00000000000..199375674d7 --- /dev/null +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs @@ -0,0 +1,31 @@ +import { readFile, writeFile } from 'node:fs/promises' +import { join } from 'node:path' + +const LINKER_OPTIONS = ` 'VCLinkerTool': { + 'AdditionalOptions': [ + '/DYNAMICBASE', + '/guard:cf' + ] + }` + +const REPRODUCIBLE_LINKER_OPTIONS = ` 'VCLinkerTool': { + 'AdditionalOptions': [ + '/DYNAMICBASE', + '/guard:cf', + '/Brepro' + ] + }` + +export async function applyWindowsNodePtyBuildDeterminism({ nodePtyDirectory, tuple }) { + if (!tuple.startsWith('win32-')) { + return false + } + const bindingPath = join(nodePtyDirectory, 'binding.gyp') + const source = await readFile(bindingPath, 'utf8') + if (source.split(LINKER_OPTIONS).length !== 2 || source.includes("'/Brepro'")) { + throw new Error('node-pty Windows linker settings do not match the reviewed source') + } + // Why: MSVC otherwise embeds per-build timestamps/identities in the copied artifact binaries. + await writeFile(bindingPath, source.replace(LINKER_OPTIONS, REPRODUCIBLE_LINKER_OPTIONS), 'utf8') + return true +} diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs new file mode 100644 index 00000000000..24985dc889a --- /dev/null +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs @@ -0,0 +1,44 @@ +import { cp, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { applyWindowsNodePtyBuildDeterminism } from './ssh-relay-node-pty-windows-build-determinism.mjs' + +const temporaryDirectories = [] +afterEach(async () => { + await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true }))) +}) + +async function copiedSource() { + const directory = await mkdtemp(join(tmpdir(), 'orca-node-pty-windows-determinism-')) + temporaryDirectories.push(directory) + await cp(resolve('node_modules/node-pty/binding.gyp'), join(directory, 'binding.gyp')) + return directory +} + +describe('SSH relay Windows node-pty build determinism', () => { + it('adds deterministic MSVC linking only to the copied Windows source', async () => { + const directory = await copiedSource() + await expect( + applyWindowsNodePtyBuildDeterminism({ nodePtyDirectory: directory, tuple: 'win32-arm64' }) + ).resolves.toBe(true) + const source = await readFile(join(directory, 'binding.gyp'), 'utf8') + expect(source.match(/'\/Brepro'/g)).toHaveLength(1) + }) + + it('leaves POSIX source untouched and rejects an unexpected Windows source shape', async () => { + const directory = await copiedSource() + const original = await readFile(join(directory, 'binding.gyp'), 'utf8') + await expect( + applyWindowsNodePtyBuildDeterminism({ nodePtyDirectory: directory, tuple: 'darwin-arm64' }) + ).resolves.toBe(false) + expect(await readFile(join(directory, 'binding.gyp'), 'utf8')).toBe(original) + + await writeFile(join(directory, 'binding.gyp'), '{}', 'utf8') + await expect( + applyWindowsNodePtyBuildDeterminism({ nodePtyDirectory: directory, tuple: 'win32-x64' }) + ).rejects.toThrow('do not match the reviewed source') + }) +}) diff --git a/config/scripts/ssh-relay-runtime-build.test.mjs b/config/scripts/ssh-relay-runtime-build.test.mjs new file mode 100644 index 00000000000..2e9c8bf8c03 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-build.test.mjs @@ -0,0 +1,51 @@ +import { resolve } from 'node:path' + +import { describe, expect, it } from 'vitest' + +import { + assertDisjointRuntimeBuildDirectories, + parseBuildArguments +} from './build-ssh-relay-runtime.mjs' + +const commit = 'a'.repeat(40) +const requiredArguments = [ + '--tuple', + 'darwin-arm64', + '--inputs-directory', + 'inputs', + '--output-directory', + 'outputs/first', + '--work-directory', + 'outputs/build-work', + '--source-date-epoch', + '1752710400', + '--git-commit', + commit +] + +describe('SSH relay runtime build isolation', () => { + it('requires and resolves a caller-owned stable clean-build directory', () => { + expect(parseBuildArguments(requiredArguments)).toMatchObject({ + outputDirectory: resolve('outputs/first'), + workDirectory: resolve('outputs/build-work') + }) + expect(() => + parseBuildArguments(requiredArguments.filter((value, index) => ![6, 7].includes(index))) + ).toThrow('Missing required build argument: workDirectory') + }) + + it('rejects work/output overlap before either exclusive directory is created', () => { + expect(() => assertDisjointRuntimeBuildDirectories('/tmp/output', '/tmp/output')).toThrow( + 'must be disjoint' + ) + expect(() => assertDisjointRuntimeBuildDirectories('/tmp/output', '/tmp/output/work')).toThrow( + 'must be disjoint' + ) + expect(() => assertDisjointRuntimeBuildDirectories('/tmp/output/work', '/tmp/output')).toThrow( + 'must be disjoint' + ) + expect(() => + assertDisjointRuntimeBuildDirectories('/tmp/output/first', '/tmp/output/build-work') + ).not.toThrow() + }) +}) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 0e370504669..aa05141d367 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -64,6 +64,10 @@ describe('SSH relay runtime artifact workflow', () => { expect(source).toContain( 'node --check config/scripts/ssh-relay-runtime-reproducibility.test.mjs' ) + expect(source.match(/ssh-relay-node-pty-build\.test\.mjs/g)).toHaveLength(2) + expect(source.match(/ssh-relay-node-pty-windows-build-determinism\.test\.mjs/g)).toHaveLength(2) + expect(source.match(/ssh-relay-runtime-build\.test\.mjs/g)).toHaveLength(2) + expect(source.match(/--work-directory/g)).toHaveLength(2) expect(source).toContain('cp "$first_output"/*.tar.xz') expect(source).toContain("Get-ChildItem -LiteralPath $firstOutput -Filter '*.zip'") expect(source).toContain('ssh-relay-node-zip-inspection.test.mjs') From 3e433b3438e8e1aa11d2728e3f2258996b3d37f2 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 12:36:14 -0400 Subject: [PATCH 026/267] build(ssh): canonicalize native build path Co-authored-by: Orca --- .github/workflows/ssh-relay-runtime-artifacts.yml | 4 ++-- config/scripts/ssh-relay-node-pty-build.mjs | 2 +- config/scripts/ssh-relay-runtime-workflow.test.mjs | 6 ++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index f233ef39ad9..605235033af 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -143,7 +143,7 @@ jobs: mkdir -p "$output_root" first_output="$output_root/first" second_output="$output_root/second" - work_directory="$output_root/build-work" + work_directory="$RUNNER_TEMP/orca-ssh-relay-runtime-build-work" source_commit=$(git rev-parse HEAD) source_date_epoch=$(git show -s --format=%ct "$source_commit") for output in "$first_output" "$second_output"; do @@ -318,7 +318,7 @@ jobs: New-Item -ItemType Directory -Path $outputRoot -Force | Out-Null $firstOutput = Join-Path $outputRoot 'first' $secondOutput = Join-Path $outputRoot 'second' - $workDirectory = Join-Path $outputRoot 'build-work' + $workDirectory = Join-Path $env:RUNNER_TEMP 'orca-ssh-relay-runtime-build-work' $sourceCommit = git rev-parse HEAD $sourceDateEpoch = git show -s --format=%ct $sourceCommit foreach ($output in @($firstOutput, $secondOutput)) { diff --git a/config/scripts/ssh-relay-node-pty-build.mjs b/config/scripts/ssh-relay-node-pty-build.mjs index 88024154e27..af277f207a7 100644 --- a/config/scripts/ssh-relay-node-pty-build.mjs +++ b/config/scripts/ssh-relay-node-pty-build.mjs @@ -24,7 +24,7 @@ export function nodePtyNativeBuildCommands({ nodePath, nodeRoot, tuple }) { { command: nodePath, args: [NODE_GYP_PATH, 'configure', ...nodeGypArguments] }, { command: 'make', - // Why: ld must retain a loadable UUID while ignoring incidental inputs such as build time. + // Why: a canonical build path plus this flag retains a loadable, reproducible Mach-O UUID. args: ['-C', 'build', 'BUILDTYPE=Release', 'LDFLAGS.target=-Wl,-reproducible'] } ] diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index aa05141d367..44966859d41 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -68,6 +68,12 @@ describe('SSH relay runtime artifact workflow', () => { expect(source.match(/ssh-relay-node-pty-windows-build-determinism\.test\.mjs/g)).toHaveLength(2) expect(source.match(/ssh-relay-runtime-build\.test\.mjs/g)).toHaveLength(2) expect(source.match(/--work-directory/g)).toHaveLength(2) + expect(source).toContain('work_directory="$RUNNER_TEMP/orca-ssh-relay-runtime-build-work"') + expect(source).toContain( + "$workDirectory = Join-Path $env:RUNNER_TEMP 'orca-ssh-relay-runtime-build-work'" + ) + expect(source).not.toContain('work_directory="$output_root/build-work"') + expect(source).not.toContain("Join-Path $outputRoot 'build-work'") expect(source).toContain('cp "$first_output"/*.tar.xz') expect(source).toContain("Get-ChildItem -LiteralPath $firstOutput -Filter '*.zip'") expect(source).toContain('ssh-relay-node-zip-inspection.test.mjs') From 54df2cb998ae564c1a19cf32a8c93bf28770d07a Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 12:40:31 -0400 Subject: [PATCH 027/267] docs(ssh): record native linker correction Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 258 +++++++++++++++--- 1 file changed, 215 insertions(+), 43 deletions(-) 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 63f487e6b6f..7cace04205a 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — completed exact-head two-clean-build run 29347236627 is an evidence-producing red: both Linux cells passed, both macOS cells exposed reproducibility drift, and both Windows cells exposed a comparator test-collection syntax failure; bounded diagnostic corrections are local only and await a new exact-head run; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — completed exact-head diagnostic run 29348424235 keeps Linux x64/arm64 green and isolates native drift to macOS `pty.node` and Windows `conpty_console_list.node`; canonical clean-work paths plus platform-native reproducible-link corrections are locally green at `3e433b343` and await a new six-runner exact-head proof; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -96,8 +96,14 @@ same change as the work it records. new comparator suite under E-M3-REPRODUCIBILITY-CI-RED-001. E-M3-REPRODUCIBILITY-DIAGNOSTIC-LOCAL-001 removes the comparator shebang, checks both new files with `node --check` on POSIX and Windows, and reports runtime-tree drift before derived metadata - drift; native proof of those corrections remains open. Oldest-baseline, native-trust, SSH, and - musl cells remain open. + drift. E-M3-REPRODUCIBILITY-DIAGNOSTIC-CI-RED-001 proves those diagnostics on all six native + runners: Linux x64/arm64 remain equal, both macOS builds first differ at `pty.node`, and both + Windows builds first differ at `conpty_console_list.node`. The bounded correction in + `a09b02ec4` makes macOS retain a reproducible loadable UUID, applies `/Brepro` only to copied + Windows artifact source, and requires exclusive caller-owned work paths; `3e433b343` makes the CI + path canonical across runs. E-M3-REPRODUCIBILITY-LINKER-LOCAL-001 is green, but a new exact-head + six-runner proof remains mandatory. Oldest-baseline, native-trust, SSH, and musl cells remain + open. - Windows input correction: E-M3-WINDOWS-INPUT-GAP-001 proved the official Windows ZIP lacks headers and `node.lib`. Both artifacts now require the exact signed headers archive and tuple import library as explicit inputs. The schema, signed-checksum verifier, bounded ZIP/header extraction, @@ -137,10 +143,9 @@ 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: finish the exact implementation-commit local gates for the bounded Windows - parser and diagnostic-order correction, then push and rerun all six target-native jobs. Use the - runtime-first comparator output to isolate - macOS producer nondeterminism without weakening the oracle. Keep oldest-baseline, cross-family +- Next required action: push exact correction head `3e433b343` and rerun all six target-native jobs. + Require Linux to remain equal, macOS to retain loadable `LC_UUID` values while comparing equal, + and Windows `/Brepro` outputs to compare equal before upload. Keep oldest-baseline, cross-family remote infrastructure, signing/trust, and measured legacy baseline gates open; do not introduce publication, resolver, transfer, rollout, tuple enablement, or default behavior. @@ -613,12 +618,22 @@ six target-native artifact/executable jobs, including normal Windows x64/arm64 s unpublished upload. E-M3-REPRODUCIBILITY-LOCAL-001 proves the bounded comparator requires two independently verified clean outputs and exact type/mode/size/SHA-256 equality for the runtime tree, archive, identity, SPDX, and provenance before uploading only the first output. Exact-head run -29347236627 is intentionally red under E-M3-REPRODUCIBILITY-CI-RED-001: Linux x64/arm64 passed, -macOS x64/arm64 produced unequal clean outputs, and Windows x64/arm64 failed comparator test -collection. E-M3-REPRODUCIBILITY-DIAGNOSTIC-LOCAL-001 proves the bounded parser/diagnostic -correction locally; native execution is still open. Artifact consumers, publication, the +29347236627 first exposed native reproducibility and Windows parser failures. Diagnostic run +29348424235 then passed the parser/contracts on all six runners, kept both Linux tuples equal, and +isolated the first native differences to macOS `pty.node` and Windows `conpty_console_list.node` +under E-M3-REPRODUCIBILITY-DIAGNOSTIC-CI-RED-001. Artifact consumers, publication, the repository-wide node-pty patch, and the legacy/default path remain unchanged. +**Active correction — 2026-07-14, Codex implementation owner:** implementation `a09b02ec4` requires +disjoint exclusive output/work directories, reuses one caller-owned work path across clean passes, +links macOS native outputs with `-Wl,-reproducible` while retaining the required `LC_UUID`, and +adds `/Brepro` only to the copied Windows node-pty source with an exact-source fail-closed guard. +Follow-up `3e433b343` removes the run ID from the work path so separate CI runs use one canonical +runner-local location while their outputs remain run-scoped. E-M3-REPRODUCIBILITY-LINKER-LOCAL-001 +proves two clean macOS passes plus a separate repeat session are byte-identical and loadable. Native +Windows proof and the new six-runner equality gate remain open; no tuple or production consumer is +enabled. + Each runtime must contain only the executable closure required by the relay. - [ ] Replace or extend `config/scripts/build-relay.mjs` without weakening its existing relay and @@ -1217,16 +1232,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 reproducibility pending | `c70e96374`; E-M3-REPRODUCIBILITY-LOCAL-001, E-M3-CI-001, E-M3-WINDOWS-CI-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; corrected native reproducibility rerun pending | `3e433b343`; E-M3-REPRODUCIBILITY-LINKER-LOCAL-001, E-M3-CI-001, E-M3-WINDOWS-CI-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 | — | Every PR must document: @@ -1278,8 +1293,9 @@ focused commands as their scripts/tests are introduced. - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs` (E-M3-WINDOWS-RESOURCE-DIAGNOSTIC-LOCAL-001; native classification pending) - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs` (E-M3-WINDOWS-CONPTY-WORKER-LOCAL-RED-001, E-M3-WINDOWS-CONPTY-WORKER-LOCAL-001; native settlement pending) - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-reproducibility.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` (E-M3-REPRODUCIBILITY-LOCAL-001; synthetic comparator/workflow contract only, native execution pending) +- [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-node-pty-build.test.mjs config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs config/scripts/ssh-relay-runtime-build.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` (E-M3-REPRODUCIBILITY-LINKER-LOCAL-001; command/linker/work-path contracts plus local macOS native proof; Windows native equality pending) - [x] `node config/scripts/verify-ssh-relay-node-release-inputs.mjs --inputs-directory --archive win32-x64` (E-M3-WINDOWS-INPUT-001; signed real Node ZIP, headers, and import library; no Windows execution) -- [x] `node config/scripts/build-ssh-relay-runtime.mjs --tuple linux-arm64-glibc --inputs-directory --output-directory --source-date-epoch --git-commit ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) +- [x] `node config/scripts/build-ssh-relay-runtime.mjs --tuple linux-arm64-glibc --inputs-directory --output-directory --work-directory --source-date-epoch --git-commit ` (E-M3-RUNTIME-LOCAL-001, E-M3-REPRODUCIBILITY-LINKER-LOCAL-001; local native Linux arm64 history plus current stable-work contract) - [x] `node config/scripts/verify-ssh-relay-runtime.mjs --runtime-directory --identity --archive ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) ### Commands/scripts that must be added or formally identified @@ -3753,23 +3769,179 @@ or unexpected token`; the first logs did not identify a source location. - Follow-up: push the exact commit, rerun all six native jobs, require both Windows syntax checks to pass, and use the macOS runtime-first failure path to correct producer nondeterminism. +### E-M3-REPRODUCIBILITY-DIAGNOSTIC-CI-RED-001 — Native drift paths isolated on all six runners + +- Date: 2026-07-14 +- Commit SHA / PR: exact head `c62b0d0a9580b132fec5f11c3234a983503a011c`; stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Run and jobs: [run 29348424235](https://github.com/stablyai/orca/actions/runs/29348424235), + conclusion `failure`; Linux x64 `87138091964`, Linux arm64 `87138091995`, macOS arm64 + `87138091994`, macOS x64 `87138092013`, Windows x64 `87138091948`, and Windows arm64 + `87138091977` +- Runners: `ubuntu-24.04` x64 image `20260705.232.1`, `ubuntu-24.04-arm` arm64 image + `20260706.52.2`, `macos-15` arm64 image `20260706.0213.1`, `macos-15-intel` x64 image + `20260629.0276.1`, `windows-2022` x64 image `20260706.237.1`, and `windows-11-arm` arm64 + image `20260706.102.1`; all native and using Node v24.18.0 +- Remote and transport: none; target-native artifact assembly/execution and unpublished Actions + artifact upload only +- Exact evidence commands: + + ```sh + gh run view 29348424235 --repo stablyai/orca \ + --json databaseId,headSha,status,conclusion,url,createdAt,updatedAt,jobs + gh api -H 'Accept: application/vnd.github+json' \ + repos/stablyai/orca/actions/jobs//logs + ``` + +- Result: FAIL as an evidence-producing native gate. Every POSIX runner passed 41/41 contracts; + Windows passed 38 and skipped three POSIX-only tests. Linux x64 and arm64 built, verified, + smoked, compared exactly, and uploaded unpublished artifacts `8317161547` and `8317164595`. + macOS arm64 first differed at + `runtime/node_modules/node-pty/build/Release/pty.node`, with content IDs + `sha256:206de506d0499ded3c16913e28f76c528f5bead5214e6675ee4a5ee6bb0a642c` and + `sha256:68e23c3608b3d3bc95d591e6f99a5126ab6a6b0066c84c8340f4bc4195e64ac8`; + macOS x64 differed at the same path with + `sha256:eda4590b97d003f9a94fb59595a84273f3660fb920d85ff699c9d7925c52a7a2` and + `sha256:701b4310acbeb1f7340d50b554cfe65e2802c5346aca0036aae4ebb3f974becf`. + Windows x64 first differed at + `runtime/node_modules/node-pty/build/Release/conpty_console_list.node`, with content IDs + `sha256:df55ffd5a5614a61e5fd2ae8cd990f28b84b9a0d557df861989c92b9e66652eb` and + `sha256:1bbf496013af3d0172a275ba48dcb1a054aeb1b17261b606ecdd4c2dd3ce95fb`; + Windows arm64 differed at the same path with + `sha256:4d93459ffefb8f8b8250c6b0267e34f04a9ac5b5f99b60671f484fb34054519a` and + `sha256:844c6f07a92ed3602af74bba92c8c98047313b97080b668aeb0b3a51ef7d2db8`. +- Duration and resource metrics: build/compare steps were 138 seconds for Linux x64, 132 seconds + for Linux arm64, 100 seconds for macOS arm64, 301 seconds for macOS x64, 296 seconds for Windows + x64, and 285 seconds for Windows arm64. Both builds in every failing cell completed bundled + Node/native PTY/watcher smoke before comparison. Smoke RSS ranged from 40,615,936 to 53,633,024 + bytes in failing cells. Peak build RSS, open files/channels, and cancellation settlement were not + instrumented by this gate. +- Artifact/log/trace link: run/job links above; Linux x64 upload + [8317161547](https://github.com/stablyai/orca/actions/runs/29348424235/artifacts/8317161547) and + Linux arm64 upload + [8317164595](https://github.com/stablyai/orca/actions/runs/29348424235/artifacts/8317164595); + failing cells correctly uploaded nothing +- Oracle proved: the parser and runtime-first diagnostic corrections execute on all native runner + families; Linux reproducibility remains green; macOS and Windows drift is inside native node-pty + outputs rather than archive/identity derivation; rejected cells cannot upload or publish bytes. +- Does not prove: either linker correction, macOS/Windows equality, cross-run identity, oldest + baselines, native trust/signing, SSH, transfer, publication, resolver/cache, fallback, UI, or an + enabled tuple. +- Checklist items satisfied: no new completion checkbox; this closes only the diagnostic run and + identifies the bounded producer corrections required next. +- Follow-up: retain the complete-output comparator, correct native producers without post-build + normalization, and require a new same-head six-runner pass before advancing. + +### E-M3-REPRODUCIBILITY-LINKER-LOCAL-001 — Canonical work path and native linker correction + +- Date: 2026-07-14 +- Commit SHA / PR: implementation `a09b02ec4e623cc8cac8ece089e4ff734014dd5b`; canonical-path + follow-up `3e433b3438e8e1aa11d2728e3f2258996b3d37f2`; stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741), push/native CI pending +- Runner: macOS 26.2 build 25C56, native Apple M4 arm64; local test/static process Node v26.0.0 + and pnpm 10.24.0; native harness used official bundled Node v24.18.0, node-gyp 12.3.0, + Apple Clang/make, and Python 3.13.5 +- Remote and transport: none; exact Node archive downloaded over HTTPS, then local native builds; + no SSH, publication, or production consumer +- Exact commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-runtime-build.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.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 \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-build.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs + pnpm run typecheck + pnpm exec oxlint \ + config/scripts/build-ssh-relay-runtime.mjs \ + config/scripts/ssh-relay-runtime-build.test.mjs \ + config/scripts/ssh-relay-node-pty-build.mjs \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check \ + .github/workflows/ssh-relay-runtime-artifacts.yml \ + config/scripts/build-ssh-relay-runtime.mjs \ + config/scripts/ssh-relay-runtime-build.test.mjs \ + config/scripts/ssh-relay-node-pty-build.mjs \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + git diff HEAD --check + ``` + + The native oracle downloaded `node-v24.18.0-darwin-arm64.tar.xz`, twice extracted it under the + fixed exclusive `/tmp/orca-ssh-relay-runtime-build-work/node-inputs` path, called + `buildPatchedSshRelayNodePty` twice with the same clean `node-pty` path, compared both outputs, + required one `LC_UUID` from `otool -l`, and then repeated the build in a separate process session + against the prior hashes. + +- Result: PASS locally. The focused command passed 8/8 tests; all 14 artifact suites passed 47/47. + Typecheck, focused oxlint, the 355-entry max-lines ratchet, full repository + lint/reliability/localization, formatting, syntax, and diff gates exited zero. Full lint emitted + only pre-existing out-of-package warnings. Two clean native passes and the separate repeat session + produced byte-identical outputs: `pty.node` was 83,256 bytes with SHA-256 + `e73f80560b8ecc0ae19421fc82fb612e4f50e66f17dfd491cdf95599bc7b685a`, and + `spawn-helper` was 50,192 bytes with SHA-256 + `3396961b0f8f9cdb0fcd2155eb54035276ee7ee2813d84faa1dfae5ba84dc95d`. + Each retained exactly one `LC_UUID`, and every `buildPatchedSshRelayNodePty` call completed its + bundled-Node native-load oracle. A separate `-Wl,-no_uuid` diagnostic made bytes equal but caused + bundled Node/dlopen to reject `pty.node` with `missing LC_UUID load command`; that approach was + discarded and is not present in the implementation. +- Duration and resource metrics: focused tests 229 ms; exact-head full artifact suite 1.35 seconds; + parallel static gate 11.5 seconds; two-pass canonical native harness 11.126 seconds; separate + repeat native build 2.511 seconds excluding archive download/extraction. Native harness peak RSS, + file counts, and cancellation settlement were not instrumented. +- Artifact/log/trace link: exact source commits and local command output; no artifact was published +- Oracle proved: output/work overlap rejects; both paths are exclusive; a caller-provided canonical + work path is reused cleanly and safely; macOS configures then links with `-Wl,-reproducible` + without deleting `LC_UUID`; copied Windows `binding.gyp` receives exactly one `/Brepro` and source + drift fails closed; Linux and legacy/repository-wide node-pty behavior are unchanged; both native + workflow families use the same canonical runner-local path while retaining run-scoped outputs. +- Does not prove: native Windows `/Brepro` behavior, macOS x64, GitHub-hosted equality, different + runner-image/toolchain revisions, oldest baselines, native signatures/trust, SSH, transfer, + publication, resolver/cache, fallback, UI, or any enabled tuple. +- Checklist items satisfied: exact-head local linker/work-path contracts and macOS arm64 native + correction only; native reproducibility remains open until all six jobs pass together. +- Follow-up: push `3e433b343`, run all six target-native jobs, and require complete equality plus + upload in each cell without weakening the comparator. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit owner and promotion condition. -| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | -| ------------------------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------ | -| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | -| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | -| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | -| Native clean-rebuild identity unproved | Linux x64/arm64 pass; macOS x64/arm64 and the Windows parser are red | Toolchain drift may change native content IDs | Codex implementation owner | All six same-head, same-runner clean builds match without weakening the oracle | Open | -| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | -| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | -| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | -| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | -| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | -| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | +| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | +| ------------------------------------------ | --------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------ | +| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | +| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | +| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | +| Native clean-rebuild identity unproved | Linux x64/arm64 pass; macOS and Windows native drift is locally corrected but not rerun | Toolchain drift may change native content IDs | Codex implementation owner | All six same-head, same-runner clean builds match without weakening the oracle | Open | +| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | +| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | +| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | +| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | +| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | +| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | ## Final Definition of Done @@ -3810,12 +3982,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Complete the exact implementation-commit local gates, push the bounded two-clean-build -reproducibility package to draft PR #8741, and run all six target-native jobs. Each job must build, -verify, and smoke two exclusive outputs, compare every runtime-tree/archive/identity/SPDX/provenance -entry, and upload only after equality. Record exact differing paths and fields for every native red -result and correct the producer without weakening the oracle. 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 publication, desktop resolver, SSH -transfer/install, per-target Beta, fallback, tuple enablement, or default behavior may be connected -by this package. +Push exact correction head `3e433b343` to draft PR #8741 and run all six target-native jobs. Each +job must build, verify, and smoke two exclusive outputs at the canonical work path, compare every +runtime-tree/archive/identity/SPDX/provenance entry, and upload only after equality. macOS must +retain loadable `LC_UUID` values; Windows must prove `/Brepro` natively; Linux must remain green. +Record exact paths and fields for any new red without weakening the oracle. 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 publication, desktop +resolver, SSH transfer/install, per-target Beta, fallback, tuple enablement, or default behavior may +be connected by this package. From 25ca53910d9901220663f9e4a50d27623f7b5321 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 12:52:02 -0400 Subject: [PATCH 028/267] docs(ssh): record builder parser boundary Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 154 +++++++++++++----- 1 file changed, 111 insertions(+), 43 deletions(-) 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 7cace04205a..f9865edee4b 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — completed exact-head diagnostic run 29348424235 keeps Linux x64/arm64 green and isolates native drift to macOS `pty.node` and Windows `conpty_console_list.node`; canonical clean-work paths plus platform-native reproducible-link corrections are locally green at `3e433b343` and await a new six-runner exact-head proof; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29350639822 proves the canonical-work/linker correction on Linux x64/arm64 and macOS x64/arm64, while both Windows jobs stop safely before inputs/build/upload because the imported builder retains an unused Unix shebang; the bounded shebang/syntax-gate correction is locally green and awaits exact-head commit plus a new six-runner proof; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -101,9 +101,12 @@ same change as the work it records. Windows builds first differ at `conpty_console_list.node`. The bounded correction in `a09b02ec4` makes macOS retain a reproducible loadable UUID, applies `/Brepro` only to copied Windows artifact source, and requires exclusive caller-owned work paths; `3e433b343` makes the CI - path canonical across runs. E-M3-REPRODUCIBILITY-LINKER-LOCAL-001 is green, but a new exact-head - six-runner proof remains mandatory. Oldest-baseline, native-trust, SSH, and musl cells remain - open. + path canonical across runs. E-M3-REPRODUCIBILITY-LINKER-CI-RED-001 proves both Linux and macOS + architectures now build, verify, smoke, compare exactly, and upload, but both Windows jobs fail + closed in contract collection because `ssh-relay-runtime-build.test.mjs` imports a builder with an + unused Unix shebang. The active correction removes only that shebang and adds explicit POSIX and + Windows `node --check` gates before Vitest. Native Windows `/Brepro` equality remains mandatory. + Oldest-baseline, native-trust, SSH, and musl cells remain open. - Windows input correction: E-M3-WINDOWS-INPUT-GAP-001 proved the official Windows ZIP lacks headers and `node.lib`. Both artifacts now require the exact signed headers archive and tuple import library as explicit inputs. The schema, signed-checksum verifier, bounded ZIP/header extraction, @@ -143,11 +146,12 @@ 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: push exact correction head `3e433b343` and rerun all six target-native jobs. - Require Linux to remain equal, macOS to retain loadable `LC_UUID` values while comparing equal, - and Windows `/Brepro` outputs to compare equal before upload. Keep oldest-baseline, cross-family - remote infrastructure, signing/trust, and measured legacy baseline gates open; do not introduce - publication, resolver, transfer, rollout, tuple enablement, or default behavior. +- Next required action: finish exact local gates for the bounded builder-shebang/syntax correction, + commit it separately from this evidence record, push both commits, and rerun all six target-native + jobs. Require Linux and macOS to remain equal and Windows `/Brepro` outputs to build, smoke, + compare equal, and upload. Keep oldest-baseline, cross-family remote infrastructure, + signing/trust, and measured legacy baseline gates open; do not introduce publication, resolver, + transfer, rollout, tuple enablement, or default behavior. ## Non-Negotiable Invariants @@ -630,9 +634,12 @@ links macOS native outputs with `-Wl,-reproducible` while retaining the required adds `/Brepro` only to the copied Windows node-pty source with an exact-source fail-closed guard. Follow-up `3e433b343` removes the run ID from the work path so separate CI runs use one canonical runner-local location while their outputs remain run-scoped. E-M3-REPRODUCIBILITY-LINKER-LOCAL-001 -proves two clean macOS passes plus a separate repeat session are byte-identical and loadable. Native -Windows proof and the new six-runner equality gate remain open; no tuple or production consumer is -enabled. +proves two clean macOS passes plus a separate repeat session are byte-identical and loadable. +E-M3-REPRODUCIBILITY-LINKER-CI-RED-001 proves Linux and macOS x64/arm64 equality on exact head +`54df2cb99`; Windows stopped before artifact inputs because Vitest imported the builder's unused Unix +shebang. The active bounded correction removes only that shebang and syntax-checks the builder and +its test on POSIX and Windows before contract collection. Native Windows proof and the six-runner +equality gate remain open; no tuple or production consumer is enabled. Each runtime must contain only the executable closure required by the relay. @@ -1232,16 +1239,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; corrected native reproducibility rerun pending | `3e433b343`; E-M3-REPRODUCIBILITY-LINKER-LOCAL-001, E-M3-CI-001, E-M3-WINDOWS-CI-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; Windows parser correction/rerun pending | `54df2cb99`; E-M3-REPRODUCIBILITY-LINKER-CI-RED-001, E-M3-CI-001, E-M3-WINDOWS-CI-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 | — | Every PR must document: @@ -3925,23 +3932,83 @@ or unexpected token`; the first logs did not identify a source location. - Follow-up: push `3e433b343`, run all six target-native jobs, and require complete equality plus upload in each cell without weakening the comparator. +### E-M3-REPRODUCIBILITY-LINKER-CI-RED-001 — POSIX equality and Windows builder parser boundary + +- Date: 2026-07-14 +- Commit SHA / PR: exact head `54df2cb998ae564c1a19cf32a8c93bf28770d07a`; stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Run and jobs: [run 29350639822](https://github.com/stablyai/orca/actions/runs/29350639822), + conclusion `failure`; Linux x64 `87145674443`, Linux arm64 `87145674440`, macOS arm64 + `87145674422`, macOS x64 `87145674490`, Windows x64 `87145674413`, and Windows arm64 + `87145674455` +- Runners: `ubuntu-24.04` x64 image `20260705.232.1`, `ubuntu-24.04-arm` arm64 image + `20260706.52.2`, `macos-15` arm64 image `20260706.0213.1`, `macos-15-intel` x64 image + `20260629.0276.1`, `windows-2022` x64 image `20260706.237.1`, and `windows-11-arm` arm64 + image `20260706.102.1`; all native, runner `2.335.1`, provisioner `20260624.560`, and Node + v24.18.0. Windows x64 was Server 2022 build 20348; arm64 was Windows Enterprise build 26200. +- Remote and transport: none; target-native artifact assembly/execution and unpublished Actions + artifact upload only +- Exact evidence commands: + + ```sh + gh run view 29350639822 --repo stablyai/orca \ + --json databaseId,headSha,headBranch,status,conclusion,url,createdAt,updatedAt,jobs + gh api repos/stablyai/orca/actions/jobs//logs + gh api 'repos/stablyai/orca/actions/runs/29350639822/artifacts?per_page=100' + ``` + +- Result: FAIL as an evidence-producing six-runner gate. Linux x64/arm64 and macOS x64/arm64 each + passed 47/47 contracts, authenticated exact Node inputs, performed two exclusive native builds, + fully inspected and smoked both outputs, compared the runtime tree/archive/identity/SPDX/ + provenance byte-for-byte, and uploaded only the first equal output. Windows x64/arm64 each passed + 42 tests and skipped three POSIX-only tests across 13 suites, then failed while collecting + `ssh-relay-runtime-build.test.mjs` with `SyntaxError: Invalid or unexpected token`: that test + imports `build-ssh-relay-runtime.mjs`, whose first line was an unused Unix shebang. Input download, + native build, verification, comparison, and upload were all skipped on both Windows jobs. +- Equal-output and upload details: + + | Tuple | Content ID / runtime archive SHA-256 | Archive / expanded / files | Build 1 / build 2 / max smoke RSS | Actions artifact | + | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------- | + | `linux-x64-glibc` | `960546cd96c67fcf9bb0a61e96ecdbecbffd9104d3a495578f8bb19dd810649a` / `1881a082b5c8ab1caf28801e42a8f62601b0275096ec67468498d1a370c4af3b` | 29,266,692 / 124,846,502 / 34 | 67,504 / 65,510 ms / 56,438,784 bytes | [8318063540](https://github.com/stablyai/orca/actions/runs/29350639822/artifacts/8318063540), 29,282,697 bytes | + | `linux-arm64-glibc` | `aa3aa8ae8b42334ba7b0dbe5c43fd1184e36b3f4f4a9bec0e990e9b78f090756` / `909a8f69acf9edb294e5d74b4323adc399821e049f0cba933206621f75c3effe` | 28,197,220 / 122,865,172 / 34 | 67,457 / 65,054 ms / 52,596,736 bytes | [8318072302](https://github.com/stablyai/orca/actions/runs/29350639822/artifacts/8318072302), 28,212,074 bytes | + | `darwin-arm64` | `40ff5d2036784b794e7b09f78596409f63f3145280c530bece5280d40897f6cb` / `132b0b6b3ecd4386ecf72119a950334f54ec34fe46e51c4f871d8c7878247775` | 24,736,104 / 122,027,869 / 35 | 55,320 / 52,391 ms / 51,658,752 bytes | [8318052438](https://github.com/stablyai/orca/actions/runs/29350639822/artifacts/8318052438), 24,749,047 bytes | + | `darwin-x64` | `585ea6034cdd07487d8667059f975a877c795a45dc0d6eeee1617f2e3749faa2` / `b47ed8b464989b1638a2024ac5980a45a494ff33f9a3dbe648c8f7974efb1e33` | 26,373,464 / 124,316,655 / 35 | 88,815 / 78,961 ms / 40,665,088 bytes | [8318098409](https://github.com/stablyai/orca/actions/runs/29350639822/artifacts/8318098409), 26,388,039 bytes | + +- Duration and resource metrics: total jobs were 3m12s Linux x64, 3m28s Linux arm64, 2m50s macOS + arm64, 4m28s macOS x64, 1m19s Windows x64, and 3m42s Windows arm64. Windows contract collection + took 1.97s and 2.76s. POSIX comparison scanned 54/55 entries and 146.8–154.1 MB in 285–895 ms. + Build peak RSS, open files/channels, and cancellation settlement were not instrumented. +- Artifact/log/trace link: run/job links and four unpublished seven-day artifacts above; Windows + correctly produced no artifact +- Oracle proved: the canonical clean-work directory plus `-Wl,-reproducible` makes both native + macOS architectures byte-identical while retaining successful bundled-Node/native PTY/watcher + execution; both Linux controls remain equal; failed contract collection prevents Windows inputs, + execution, and upload. The parser boundary is the imported builder shebang, not `/Brepro` output. +- Does not prove: Windows builder parsing after correction, native `/Brepro` equality, a six-job + pass, cross-run identity, oldest baselines, native trust/signing, SSH, transfer, publication, + resolver/cache, fallback, UI, or an enabled tuple. +- Checklist items satisfied: native macOS linker reproducibility evidence only; native + reproducibility remains open until all six same-head jobs pass together. +- Follow-up: remove only the unused builder shebang, syntax-check the builder and its test on POSIX + and Windows before Vitest, rerun local gates, then require a new six-runner exact-head pass. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit owner and promotion condition. -| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | -| ------------------------------------------ | --------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------ | -| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | -| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | -| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | -| Native clean-rebuild identity unproved | Linux x64/arm64 pass; macOS and Windows native drift is locally corrected but not rerun | Toolchain drift may change native content IDs | Codex implementation owner | All six same-head, same-runner clean builds match without weakening the oracle | Open | -| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | -| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | -| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | -| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | -| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | -| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | +| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | +| ------------------------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------ | +| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | +| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | +| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | +| Native clean-rebuild identity unproved | Linux/macOS x64/arm64 pass; Windows parser correction awaits rerun | Toolchain drift may change native content IDs | Codex implementation owner | All six same-head, same-runner clean builds match without weakening the oracle | Open | +| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | +| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | +| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | +| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | +| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | +| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | ## Final Definition of Done @@ -3982,12 +4049,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push exact correction head `3e433b343` to draft PR #8741 and run all six target-native jobs. Each -job must build, verify, and smoke two exclusive outputs at the canonical work path, compare every -runtime-tree/archive/identity/SPDX/provenance entry, and upload only after equality. macOS must -retain loadable `LC_UUID` values; Windows must prove `/Brepro` natively; Linux must remain green. -Record exact paths and fields for any new red without weakening the oracle. 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 publication, desktop -resolver, SSH transfer/install, per-target Beta, fallback, tuple enablement, or default behavior may -be connected by this package. +Run the complete local artifact/static gates for the bounded builder-shebang/syntax correction, +commit it separately from E-M3-REPRODUCIBILITY-LINKER-CI-RED-001, push both commits to draft PR +#8741, and run all six target-native jobs. Each job must build, verify, and smoke two exclusive +outputs at the canonical work path, compare every runtime-tree/archive/identity/SPDX/provenance +entry, and upload only after equality. macOS must retain loadable `LC_UUID` values; Windows must +prove `/Brepro` natively; Linux must remain green. Record exact paths and fields for any new red +without weakening the oracle. 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 publication, desktop resolver, SSH transfer/install, per-target +Beta, fallback, tuple enablement, or default behavior may be connected by this package. From f864d3fa6df7a938e509c7622604e2fd2bd85493 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 12:52:15 -0400 Subject: [PATCH 029/267] build(ssh): syntax-check runtime builder Co-authored-by: Orca --- .github/workflows/ssh-relay-runtime-artifacts.yml | 6 ++++++ config/scripts/build-ssh-relay-runtime.mjs | 2 -- config/scripts/ssh-relay-runtime-workflow.test.mjs | 8 +++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 605235033af..bd2e5713c30 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -105,6 +105,8 @@ jobs: set -euo pipefail node --check config/scripts/ssh-relay-runtime-reproducibility.mjs node --check config/scripts/ssh-relay-runtime-reproducibility.test.mjs + node --check config/scripts/build-ssh-relay-runtime.mjs + node --check config/scripts/ssh-relay-runtime-build.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 \ @@ -269,6 +271,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime reproducibility script syntax check failed' } node --check config/scripts/ssh-relay-runtime-reproducibility.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime reproducibility test syntax check failed' } + node --check config/scripts/build-ssh-relay-runtime.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime build script syntax check failed' } + node --check config/scripts/ssh-relay-runtime-build.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime build 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 ` diff --git a/config/scripts/build-ssh-relay-runtime.mjs b/config/scripts/build-ssh-relay-runtime.mjs index 8bf4725b455..2ae076aafc0 100644 --- a/config/scripts/build-ssh-relay-runtime.mjs +++ b/config/scripts/build-ssh-relay-runtime.mjs @@ -1,5 +1,3 @@ -#!/usr/bin/env node - import { execFile } from 'node:child_process' import { mkdir, readFile, rm } from 'node:fs/promises' import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path' diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 44966859d41..9d701eaa429 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -64,9 +64,15 @@ describe('SSH relay runtime artifact workflow', () => { expect(source).toContain( 'node --check config/scripts/ssh-relay-runtime-reproducibility.test.mjs' ) + expect( + source.match(/node --check config\/scripts\/build-ssh-relay-runtime\.mjs/g) + ).toHaveLength(2) + expect( + source.match(/node --check config\/scripts\/ssh-relay-runtime-build\.test\.mjs/g) + ).toHaveLength(2) expect(source.match(/ssh-relay-node-pty-build\.test\.mjs/g)).toHaveLength(2) expect(source.match(/ssh-relay-node-pty-windows-build-determinism\.test\.mjs/g)).toHaveLength(2) - expect(source.match(/ssh-relay-runtime-build\.test\.mjs/g)).toHaveLength(2) + expect(source.match(/ssh-relay-runtime-build\.test\.mjs/g)).toHaveLength(4) expect(source.match(/--work-directory/g)).toHaveLength(2) expect(source).toContain('work_directory="$RUNNER_TEMP/orca-ssh-relay-runtime-build-work"') expect(source).toContain( From de541efd11989d2e6dce0402307912afffae3510 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 12:54:04 -0400 Subject: [PATCH 030/267] docs(ssh): record builder parser correction Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 126 +++++++++++++----- 1 file changed, 96 insertions(+), 30 deletions(-) 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 f9865edee4b..04574335845 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29350639822 proves the canonical-work/linker correction on Linux x64/arm64 and macOS x64/arm64, while both Windows jobs stop safely before inputs/build/upload because the imported builder retains an unused Unix shebang; the bounded shebang/syntax-gate correction is locally green and awaits exact-head commit plus a new six-runner proof; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29350639822 proves the canonical-work/linker correction on Linux x64/arm64 and macOS x64/arm64, while both Windows jobs stop safely before inputs/build/upload because the imported builder retains an unused Unix shebang; the bounded shebang/syntax-gate correction is exact-head locally green at `f864d3fa6` and awaits a new six-runner proof; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -106,7 +106,8 @@ same change as the work it records. closed in contract collection because `ssh-relay-runtime-build.test.mjs` imports a builder with an unused Unix shebang. The active correction removes only that shebang and adds explicit POSIX and Windows `node --check` gates before Vitest. Native Windows `/Brepro` equality remains mandatory. - Oldest-baseline, native-trust, SSH, and musl cells remain open. + E-M3-REPRODUCIBILITY-BUILDER-PARSER-LOCAL-001 proves the bounded correction at exact commit + `f864d3fa6`. Oldest-baseline, native-trust, SSH, and musl cells remain open. - Windows input correction: E-M3-WINDOWS-INPUT-GAP-001 proved the official Windows ZIP lacks headers and `node.lib`. Both artifacts now require the exact signed headers archive and tuple import library as explicit inputs. The schema, signed-checksum verifier, bounded ZIP/header extraction, @@ -146,12 +147,12 @@ 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: finish exact local gates for the bounded builder-shebang/syntax correction, - commit it separately from this evidence record, push both commits, and rerun all six target-native - jobs. Require Linux and macOS to remain equal and Windows `/Brepro` outputs to build, smoke, - compare equal, and upload. Keep oldest-baseline, cross-family remote infrastructure, - signing/trust, and measured legacy baseline gates open; do not introduce publication, resolver, - transfer, rollout, tuple enablement, or default behavior. +- Next required action: commit the exact local evidence record for `f864d3fa6`, push the bounded + correction and evidence commits, and rerun all six target-native jobs. Require Linux and macOS to + remain equal and Windows `/Brepro` outputs to build, smoke, compare equal, and upload. Keep + oldest-baseline, cross-family remote infrastructure, signing/trust, and measured legacy baseline + gates open; do not introduce publication, resolver, transfer, rollout, tuple enablement, or + default behavior. ## Non-Negotiable Invariants @@ -638,8 +639,9 @@ proves two clean macOS passes plus a separate repeat session are byte-identical E-M3-REPRODUCIBILITY-LINKER-CI-RED-001 proves Linux and macOS x64/arm64 equality on exact head `54df2cb99`; Windows stopped before artifact inputs because Vitest imported the builder's unused Unix shebang. The active bounded correction removes only that shebang and syntax-checks the builder and -its test on POSIX and Windows before contract collection. Native Windows proof and the six-runner -equality gate remain open; no tuple or production consumer is enabled. +its test on POSIX and Windows before contract collection. Its exact implementation commit +`f864d3fa6` is locally green under E-M3-REPRODUCIBILITY-BUILDER-PARSER-LOCAL-001. Native Windows +proof and the six-runner equality gate remain open; no tuple or production consumer is enabled. Each runtime must contain only the executable closure required by the relay. @@ -1239,16 +1241,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; Windows parser correction/rerun pending | `54df2cb99`; E-M3-REPRODUCIBILITY-LINKER-CI-RED-001, E-M3-CI-001, E-M3-WINDOWS-CI-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; six-runner parser correction rerun pending | `f864d3fa6`; E-M3-REPRODUCIBILITY-BUILDER-PARSER-LOCAL-001, E-M3-CI-001, E-M3-WINDOWS-CI-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 | — | Every PR must document: @@ -1301,6 +1303,7 @@ focused commands as their scripts/tests are introduced. - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs` (E-M3-WINDOWS-CONPTY-WORKER-LOCAL-RED-001, E-M3-WINDOWS-CONPTY-WORKER-LOCAL-001; native settlement pending) - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-reproducibility.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` (E-M3-REPRODUCIBILITY-LOCAL-001; synthetic comparator/workflow contract only, native execution pending) - [x] `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-node-pty-build.test.mjs config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs config/scripts/ssh-relay-runtime-build.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` (E-M3-REPRODUCIBILITY-LINKER-LOCAL-001; command/linker/work-path contracts plus local macOS native proof; Windows native equality pending) +- [x] `node --check config/scripts/build-ssh-relay-runtime.mjs && node --check config/scripts/ssh-relay-runtime-build.test.mjs` (E-M3-REPRODUCIBILITY-BUILDER-PARSER-LOCAL-001; exact local parser proof; native Windows remains pending) - [x] `node config/scripts/verify-ssh-relay-node-release-inputs.mjs --inputs-directory --archive win32-x64` (E-M3-WINDOWS-INPUT-001; signed real Node ZIP, headers, and import library; no Windows execution) - [x] `node config/scripts/build-ssh-relay-runtime.mjs --tuple linux-arm64-glibc --inputs-directory --output-directory --work-directory --source-date-epoch --git-commit ` (E-M3-RUNTIME-LOCAL-001, E-M3-REPRODUCIBILITY-LINKER-LOCAL-001; local native Linux arm64 history plus current stable-work contract) - [x] `node config/scripts/verify-ssh-relay-runtime.mjs --runtime-directory --identity --archive ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) @@ -3992,6 +3995,69 @@ or unexpected token`; the first logs did not identify a source location. - Follow-up: remove only the unused builder shebang, syntax-check the builder and its test on POSIX and Windows before Vitest, rerun local gates, then require a new six-runner exact-head pass. +### E-M3-REPRODUCIBILITY-BUILDER-PARSER-LOCAL-001 — Imported builder parser correction + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `f864d3fa6df7a938e509c7622604e2fd2bd85493`; stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741), push/native CI pending +- Runner: macOS 26.2 build 25C56, native Apple M4 arm64; Node v26.0.0 and pnpm 10.24.0. The + repository requires Node 24, so the new target-native run remains authoritative for Windows. +- Remote and transport: none; local syntax, contract, and repository static gates only +- Exact commands: + + ```sh + node --check config/scripts/build-ssh-relay-runtime.mjs + node --check config/scripts/ssh-relay-runtime-build.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 \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-build.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs + pnpm run typecheck + pnpm exec oxlint config/scripts/build-ssh-relay-runtime.mjs \ + config/scripts/ssh-relay-runtime-build.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + .github/workflows/ssh-relay-runtime-artifacts.yml + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check .github/workflows/ssh-relay-runtime-artifacts.yml \ + config/scripts/build-ssh-relay-runtime.mjs \ + config/scripts/ssh-relay-runtime-build.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + git diff HEAD --check + ``` + +- Result: PASS at the exact implementation commit. Both syntax checks exited zero; all 14 artifact + suites passed 47/47 tests. Typecheck, focused oxlint, the 355-entry max-lines ratchet, full + repository lint/reliability/localization, formatting, and exact-head diff checks exited zero. + Full lint emitted only pre-existing warnings outside this package. +- Duration and resource metrics: full artifact command 3.08s wall; typecheck 3.30s; focused oxlint + 0.49s; max-lines 0.76s; full lint 12.79s. Synthetic test peak RSS, open files/channels, and + cancellation settlement were not instrumented. +- Artifact/log/trace link: exact commit and local command output; no runtime artifact was produced +- Oracle proved: the builder and importing test parse after removing only the unused shebang; every + real builder invocation remains explicitly through `node`; both workflow families syntax-check + the builder and test before Vitest or Node input download; workflow source contracts require both + POSIX and Windows checks without changing permissions, artifact scope, or production behavior. +- Does not prove: native Windows parsing, Node 24 behavior, `/Brepro` output equality, any native + build, oldest baselines, native trust, SSH, transfer, publication, resolver/cache, fallback, UI, + or an enabled tuple. +- Checklist items satisfied: exact local builder parser/syntax-gate correction only; no native or + tuple checkbox. +- Follow-up: push the exact commit and require all six target-native jobs to pass together before + closing native reproducibility. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -4049,13 +4115,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Run the complete local artifact/static gates for the bounded builder-shebang/syntax correction, -commit it separately from E-M3-REPRODUCIBILITY-LINKER-CI-RED-001, push both commits to draft PR -#8741, and run all six target-native jobs. Each job must build, verify, and smoke two exclusive -outputs at the canonical work path, compare every runtime-tree/archive/identity/SPDX/provenance -entry, and upload only after equality. macOS must retain loadable `LC_UUID` values; Windows must -prove `/Brepro` natively; Linux must remain green. Record exact paths and fields for any new red -without weakening the oracle. 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 publication, desktop resolver, SSH transfer/install, per-target -Beta, fallback, tuple enablement, or default behavior may be connected by this package. +Commit E-M3-REPRODUCIBILITY-BUILDER-PARSER-LOCAL-001, push it with exact implementation commit +`f864d3fa6` to draft PR #8741, and run all six target-native jobs. Each job must build, verify, and +smoke two exclusive outputs at the canonical work path, compare every runtime-tree/archive/ +identity/SPDX/provenance entry, and upload only after equality. macOS must retain loadable `LC_UUID` +values; Windows must prove `/Brepro` natively; Linux must remain green. Record exact paths and +fields for any new red without weakening the oracle. 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 publication, desktop resolver, SSH transfer/ +install, per-target Beta, fallback, tuple enablement, or default behavior may be connected by this +package. From a7151f9750fd9bfcdcff8c01c1fd6caff2e6116a Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 13:08:01 -0400 Subject: [PATCH 031/267] docs(ssh): record arm64 reproducibility boundary Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 102 ++++++++++++++---- 1 file changed, 80 insertions(+), 22 deletions(-) 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 04574335845..53de05f1acd 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29350639822 proves the canonical-work/linker correction on Linux x64/arm64 and macOS x64/arm64, while both Windows jobs stop safely before inputs/build/upload because the imported builder retains an unused Unix shebang; the bounded shebang/syntax-gate correction is exact-head locally green at `f864d3fa6` and awaits a new six-runner proof; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29351557922 proves clean-build equality on Linux x64/arm64, macOS x64/arm64, and Windows x64, but fails closed because Windows arm64 first differs at `conpty_console_list.node`; the active slice is bounded diagnosis/correction of that copied artifact build only; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -104,10 +104,12 @@ same change as the work it records. path canonical across runs. E-M3-REPRODUCIBILITY-LINKER-CI-RED-001 proves both Linux and macOS architectures now build, verify, smoke, compare exactly, and upload, but both Windows jobs fail closed in contract collection because `ssh-relay-runtime-build.test.mjs` imports a builder with an - unused Unix shebang. The active correction removes only that shebang and adds explicit POSIX and - Windows `node --check` gates before Vitest. Native Windows `/Brepro` equality remains mandatory. - E-M3-REPRODUCIBILITY-BUILDER-PARSER-LOCAL-001 proves the bounded correction at exact commit - `f864d3fa6`. Oldest-baseline, native-trust, SSH, and musl cells remain open. + unused Unix shebang. E-M3-REPRODUCIBILITY-BUILDER-PARSER-LOCAL-001 proves the bounded correction at + exact commit `f864d3fa6`. Exact-head run 29351557922 then parses and builds on all six native + runners: Linux x64/arm64, macOS x64/arm64, and Windows x64 compare exactly and upload, while + Windows arm64 fully builds, verifies, and smokes both outputs but fails closed before upload when + `conpty_console_list.node` differs (E-M3-REPRODUCIBILITY-WINDOWS-ARM64-CI-RED-001). Native Windows + arm64 equality remains mandatory; oldest-baseline, native-trust, SSH, and musl cells remain open. - Windows input correction: E-M3-WINDOWS-INPUT-GAP-001 proved the official Windows ZIP lacks headers and `node.lib`. Both artifacts now require the exact signed headers archive and tuple import library as explicit inputs. The schema, signed-checksum verifier, bounded ZIP/header extraction, @@ -1241,16 +1243,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; six-runner parser correction rerun pending | `f864d3fa6`; E-M3-REPRODUCIBILITY-BUILDER-PARSER-LOCAL-001, E-M3-CI-001, E-M3-WINDOWS-CI-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; Windows arm64 equality diagnosis active | `de541efd1`; E-M3-REPRODUCIBILITY-WINDOWS-ARM64-CI-RED-001, E-M3-CI-001, E-M3-WINDOWS-CI-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 | — | Every PR must document: @@ -4058,6 +4060,63 @@ or unexpected token`; the first logs did not identify a source location. - Follow-up: push the exact commit and require all six target-native jobs to pass together before closing native reproducibility. +### E-M3-REPRODUCIBILITY-WINDOWS-ARM64-CI-RED-001 — Native arm64 residual binary drift + +- Date: 2026-07-14 +- Commit SHA / PR: exact head `de541efd11989d2e6dce0402307912afffae3510`; stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Run and jobs: [run 29351557922](https://github.com/stablyai/orca/actions/runs/29351557922), + conclusion `failure`; Linux arm64 `87148758201`, Linux x64 `87148758213`, Windows x64 + `87148758259`, macOS arm64 `87148758282`, macOS x64 `87148758287`, and failing Windows arm64 + `87148758290` +- Runners: `ubuntu-24.04-arm` image `20260706.52.2`, `ubuntu-24.04` image `20260705.232.1`, + `windows-2022` image `20260706.237.1`, `macos-15-arm64` image `20260706.0213.1`, `macos-15` + image `20260629.0276.1`, and `windows-11-arm64` image `20260706.102.1`; all native, runner + `2.335.1`, provisioner `20260624.560`, and Node v24.18.0 +- Remote and transport: none; target-native artifact assembly/execution and unpublished Actions + artifact upload only +- Exact evidence commands: + + ```sh + gh run view 29351557922 --repo stablyai/orca \ + --json databaseId,headSha,headBranch,status,conclusion,url,createdAt,updatedAt,jobs + gh api repos/stablyai/orca/actions/jobs//logs + gh api 'repos/stablyai/orca/actions/runs/29351557922/artifacts?per_page=100' + ``` + +- Result: FAIL as a six-runner reproducibility gate. All six jobs passed the builder/test syntax + checks and artifact contracts, authenticated the exact Node inputs, and completed two native + builds. Linux x64/arm64, macOS x64/arm64, and Windows x64 inspected and smoked both outputs, + compared every runtime/archive/identity/SPDX/provenance entry exactly, and uploaded only after + equality. Windows arm64 also inspected and smoked both complete 60-entry, 42-file, 86,189,895-byte + outputs, including PTY input/resize/exit, watcher lifecycle, and resource settlement, but the + comparator first found SHA-256 drift at + `runtime/node_modules/node-pty/build/Release/conpty_console_list.node`; no arm64 artifact uploaded. +- Equal-output artifacts: Linux x64 `8318426445` (29,283,624 bytes), Linux arm64 `8318449064` + (28,212,246 bytes), macOS arm64 `8318457230` (24,710,997 bytes), Windows x64 `8318478579` + (37,075,312 bytes), and macOS x64 `8318524353` (26,365,330 bytes), all unpublished seven-day + Actions artifacts. Their runtime content IDs are respectively `960546cd96c6`, `aa3aa8ae8b42`, + `40ff5d203678`, `2a6bfa06b445`, and `585ea6034cdd` (prefixes shown; full values remain in logs). +- Windows arm64 drift details: build one content ID `58afdfbe6b3e48ee1a46bcb09f20a9c7d4d6fe25bdf346e82038fa0a2211ea86`, + archive 33,261,552 bytes with SHA-256 `0277fa35bb1ec644a509e3068bf120294c99484ac3638fdd352ae386f83bdd2f`; + build two content ID `6e64071c12f58b0f73a34339c6f1f1dac725d852224537c32d2c87f8e587bdb9`, + archive 33,261,545 bytes with SHA-256 `234aa56026009c91abdfa37abae484723a39f027f8afa00760afc784a3ecfd05`. +- Duration and resource metrics: jobs were 3m43s Linux arm64, 2m57s Linux x64, 4m54s Windows x64, + 4m7s macOS arm64, 6m34s macOS x64, and 10m37s Windows arm64. Windows arm64 native builds took + 152,711 ms and 132,480 ms; smoke took 5,845 ms and 5,408 ms at 53,018,624 and 51,732,480 bytes + RSS. Build peak RSS, open files/channels, and cancellation settlement were not instrumented. +- Oracle proved: the parser correction works natively on both Windows architectures; `/Brepro` + yields complete equality on Windows x64; all four POSIX controls remain equal; a native arm64 + residual is isolated to the copied node-pty `conpty_console_list.node`; mismatch prevents upload. +- Does not prove: the differing byte region or PE field, a Windows arm64 correction, all-six + equality, cross-run identity, oldest baselines, native trust/signing, SSH, transfer, publication, + resolver/cache, fallback, UI, or an enabled tuple. +- Checklist items satisfied: native parser boundary and five same-head reproducibility cells only; + native reproducibility remains open. +- Follow-up: add a bounded PE-diff diagnostic for the two Windows arm64 binaries, record byte ranges + and headers, then correct only the copied artifact build if evidence identifies a safe source. + Do not weaken comparison, upload failed outputs, or change repository-wide/legacy node-pty. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -4068,7 +4127,7 @@ owner and promotion condition. | Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | | No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | | Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | -| Native clean-rebuild identity unproved | Linux/macOS x64/arm64 pass; Windows parser correction awaits rerun | Toolchain drift may change native content IDs | Codex implementation owner | All six same-head, same-runner clean builds match without weakening the oracle | Open | +| Native clean-rebuild identity unproved | Five native cells pass; Windows arm64 `conpty_console_list.node` drifts | Toolchain drift may change native content IDs | Codex implementation owner | All six same-head, same-runner clean builds match without weakening the oracle | Open | | Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | | Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | | Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | @@ -4115,12 +4174,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit E-M3-REPRODUCIBILITY-BUILDER-PARSER-LOCAL-001, push it with exact implementation commit -`f864d3fa6` to draft PR #8741, and run all six target-native jobs. Each job must build, verify, and -smoke two exclusive outputs at the canonical work path, compare every runtime-tree/archive/ -identity/SPDX/provenance entry, and upload only after equality. macOS must retain loadable `LC_UUID` -values; Windows must prove `/Brepro` natively; Linux must remain green. Record exact paths and -fields for any new red without weakening the oracle. Cross-family Layer B targets, the protected +Add a bounded Windows PE diagnostic that compares the two native arm64 +`conpty_console_list.node` outputs before cleanup, reports hashes, differing ranges, and relevant +headers without uploading failed bytes, and proves x64 stays unchanged. Use that evidence to make +the narrowest copied-artifact build correction, then rerun local gates and all six target-native +jobs without weakening the comparator. 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 publication, desktop resolver, SSH transfer/ install, per-target Beta, fallback, tuple enablement, or default behavior may be connected by this From 39ee3451b8cc38b5311a0cb1085ad48a1f302185 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 13:19:46 -0400 Subject: [PATCH 032/267] build(ssh): diagnose Windows PE reproducibility drift Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 20 +- ...sh-relay-runtime-windows-pe-diagnostic.mjs | 444 ++++++++++++++++++ ...lay-runtime-windows-pe-diagnostic.test.mjs | 142 ++++++ .../ssh-relay-runtime-workflow.test.mjs | 14 + ...github-release-implementation-checklist.md | 101 +++- 5 files changed, 698 insertions(+), 23 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-windows-pe-diagnostic.mjs create mode 100644 config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index bd2e5713c30..394651d7686 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -271,6 +271,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime reproducibility script syntax check failed' } node --check config/scripts/ssh-relay-runtime-reproducibility.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime reproducibility test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-windows-pe-diagnostic.mjs + if ($LASTEXITCODE -ne 0) { throw 'Windows PE diagnostic script syntax check failed' } + node --check config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'Windows PE diagnostic test syntax check failed' } node --check config/scripts/build-ssh-relay-runtime.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime build script syntax check failed' } node --check config/scripts/ssh-relay-runtime-build.test.mjs @@ -287,6 +291,7 @@ jobs: config/scripts/ssh-relay-runtime-pty-smoke.test.mjs ` config/scripts/ssh-relay-runtime-reproducibility.test.mjs ` config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs ` + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs ` config/scripts/ssh-relay-runtime-windows-tree.test.mjs ` config/scripts/ssh-relay-runtime-zip.test.mjs ` config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -358,7 +363,20 @@ jobs: --tuple '${{ matrix.tuple }}' ` --first-output-directory $firstOutput ` --second-output-directory $secondOutput - if ($LASTEXITCODE -ne 0) { throw 'runtime reproducibility verification failed' } + if ($LASTEXITCODE -ne 0) { + $firstPe = Join-Path $firstOutput 'runtime/node_modules/node-pty/build/Release/conpty_console_list.node' + $secondPe = Join-Path $secondOutput 'runtime/node_modules/node-pty/build/Release/conpty_console_list.node' + if ((Test-Path -LiteralPath $firstPe -PathType Leaf) -and (Test-Path -LiteralPath $secondPe -PathType Leaf)) { + # Why: rejected PE bytes remain local, but bounded headers/ranges identify the producer drift. + node config/scripts/ssh-relay-runtime-windows-pe-diagnostic.mjs ` + --first-pe $firstPe ` + --second-pe $secondPe + if ($LASTEXITCODE -ne 0) { Write-Output 'windows_pe_diagnostic=failed' } + } else { + Write-Output 'windows_pe_diagnostic=skipped_missing_input' + } + throw 'runtime reproducibility verification failed' + } $evidence = Join-Path $env:GITHUB_WORKSPACE 'runtime-evidence/${{ matrix.tuple }}' New-Item -ItemType Directory -Path $evidence -Force | Out-Null $firstArchive = Get-ChildItem -LiteralPath $firstOutput -Filter '*.zip' -File | Select-Object -First 1 diff --git a/config/scripts/ssh-relay-runtime-windows-pe-diagnostic.mjs b/config/scripts/ssh-relay-runtime-windows-pe-diagnostic.mjs new file mode 100644 index 00000000000..795ae1dceaf --- /dev/null +++ b/config/scripts/ssh-relay-runtime-windows-pe-diagnostic.mjs @@ -0,0 +1,444 @@ +import { createHash } from 'node:crypto' +import { lstat, open } from 'node:fs/promises' +import { resolve } from 'node:path' + +const MAX_PE_BYTES = 64 * 1024 * 1024 +const MAX_HEADER_BYTES = 1024 * 1024 +const MAX_SECTIONS = 96 +const MAX_DEBUG_ENTRIES = 32 +const MAX_DIFF_RANGES = 128 +const MAX_HEADER_DIFFERENCES = 128 +const READ_CHUNK_BYTES = 64 * 1024 +const DIAGNOSTIC_TIMEOUT_MS = 60_000 + +const DATA_DIRECTORY_NAMES = [ + 'export', + 'import', + 'resource', + 'exception', + 'security', + 'baseRelocation', + 'debug', + 'architecture', + 'globalPointer', + 'tls', + 'loadConfig', + 'boundImport', + 'iat', + 'delayImport', + 'clr', + 'reserved' +] + +function requireBytes(buffer, offset, length, label) { + if (!Number.isSafeInteger(offset) || offset < 0 || offset + length > buffer.length) { + throw new Error(`PE ${label} exceeds the bounded header`) + } +} + +function hex(value, width = 0) { + return `0x${value.toString(16).padStart(width, '0')}` +} + +function readSectionName(buffer, offset) { + const bytes = buffer.subarray(offset, offset + 8) + const end = bytes.indexOf(0) + const name = bytes.subarray(0, end === -1 ? bytes.length : end).toString('ascii') + return /^[ -~]*$/.test(name) ? name : bytes.toString('hex') +} + +function parsePeHeader(buffer) { + requireBytes(buffer, 0, 0x40, 'DOS header') + if (buffer.readUInt16LE(0) !== 0x5a4d) { + throw new Error('PE diagnostic input is missing the MZ signature') + } + const peOffset = buffer.readUInt32LE(0x3c) + requireBytes(buffer, peOffset, 24, 'COFF header') + if (buffer.readUInt32LE(peOffset) !== 0x00004550) { + throw new Error('PE diagnostic input is missing the PE signature') + } + + const coffOffset = peOffset + 4 + const numberOfSections = buffer.readUInt16LE(coffOffset + 2) + const optionalHeaderBytes = buffer.readUInt16LE(coffOffset + 16) + if (numberOfSections > MAX_SECTIONS) { + throw new Error('PE section count exceeds the diagnostic limit') + } + const optionalOffset = coffOffset + 20 + requireBytes(buffer, optionalOffset, optionalHeaderBytes, 'optional header') + const magic = buffer.readUInt16LE(optionalOffset) + if (magic !== 0x10b && magic !== 0x20b) { + throw new Error(`unsupported PE optional-header magic: ${hex(magic, 4)}`) + } + const pe32Plus = magic === 0x20b + const directoryCountOffset = optionalOffset + (pe32Plus ? 108 : 92) + const directoryOffset = optionalOffset + (pe32Plus ? 112 : 96) + requireBytes(buffer, directoryCountOffset, 4, 'data-directory count') + const declaredDirectoryCount = buffer.readUInt32LE(directoryCountOffset) + const availableDirectoryCount = Math.floor( + (optionalOffset + optionalHeaderBytes - directoryOffset) / 8 + ) + const directoryCount = Math.min(declaredDirectoryCount, availableDirectoryCount, 16) + const dataDirectories = {} + for (let index = 0; index < directoryCount; index += 1) { + const offset = directoryOffset + index * 8 + dataDirectories[DATA_DIRECTORY_NAMES[index]] = { + virtualAddress: hex(buffer.readUInt32LE(offset), 8), + size: buffer.readUInt32LE(offset + 4) + } + } + + const sectionTableOffset = optionalOffset + optionalHeaderBytes + requireBytes(buffer, sectionTableOffset, numberOfSections * 40, 'section table') + const sections = [] + for (let index = 0; index < numberOfSections; index += 1) { + const offset = sectionTableOffset + index * 40 + sections.push({ + name: readSectionName(buffer, offset), + virtualSize: buffer.readUInt32LE(offset + 8), + virtualAddress: buffer.readUInt32LE(offset + 12), + rawSize: buffer.readUInt32LE(offset + 16), + rawPointer: buffer.readUInt32LE(offset + 20), + characteristics: hex(buffer.readUInt32LE(offset + 36), 8), + headerOffset: offset + }) + } + + const imageBase = pe32Plus + ? hex(buffer.readBigUInt64LE(optionalOffset + 24), 16) + : hex(buffer.readUInt32LE(optionalOffset + 28), 8) + const header = { + dos: { peOffset }, + coff: { + machine: hex(buffer.readUInt16LE(coffOffset), 4), + numberOfSections, + timeDateStamp: hex(buffer.readUInt32LE(coffOffset + 4), 8), + pointerToSymbolTable: buffer.readUInt32LE(coffOffset + 8), + numberOfSymbols: buffer.readUInt32LE(coffOffset + 12), + optionalHeaderBytes, + characteristics: hex(buffer.readUInt16LE(coffOffset + 18), 4) + }, + optional: { + magic: hex(magic, 4), + linkerVersion: `${buffer[optionalOffset + 2]}.${buffer[optionalOffset + 3]}`, + sizeOfCode: buffer.readUInt32LE(optionalOffset + 4), + sizeOfInitializedData: buffer.readUInt32LE(optionalOffset + 8), + sizeOfUninitializedData: buffer.readUInt32LE(optionalOffset + 12), + addressOfEntryPoint: hex(buffer.readUInt32LE(optionalOffset + 16), 8), + imageBase, + sectionAlignment: buffer.readUInt32LE(optionalOffset + 32), + fileAlignment: buffer.readUInt32LE(optionalOffset + 36), + sizeOfImage: buffer.readUInt32LE(optionalOffset + 56), + sizeOfHeaders: buffer.readUInt32LE(optionalOffset + 60), + checksum: hex(buffer.readUInt32LE(optionalOffset + 64), 8), + subsystem: hex(buffer.readUInt16LE(optionalOffset + 68), 4), + dllCharacteristics: hex(buffer.readUInt16LE(optionalOffset + 70), 4), + declaredDirectoryCount, + dataDirectories + }, + sections: sections.map(({ headerOffset: _headerOffset, ...section }) => section), + debugDirectory: [] + } + const regions = [ + { start: coffOffset, end: coffOffset + 20, label: 'COFF header' }, + { start: optionalOffset, end: optionalOffset + optionalHeaderBytes, label: 'optional header' }, + ...sections.map((section) => ({ + start: section.headerOffset, + end: section.headerOffset + 40, + label: `section header ${section.name}` + })) + ] + return { header, regions, sections } +} + +function rvaToFileOffset(rva, header, sections) { + if (rva < header.optional.sizeOfHeaders) { + return rva + } + const section = sections.find( + (candidate) => + rva >= candidate.virtualAddress && + rva < candidate.virtualAddress + Math.max(candidate.virtualSize, candidate.rawSize) + ) + if (!section) { + return undefined + } + const delta = rva - section.virtualAddress + return delta < section.rawSize ? section.rawPointer + delta : undefined +} + +async function readAt(handle, offset, length, fileSize, signal) { + signal.throwIfAborted() + if (offset < 0 || length < 0 || offset + length > fileSize) { + throw new Error('PE diagnostic read exceeds the file boundary') + } + const buffer = Buffer.alloc(length) + let read = 0 + while (read < length) { + signal.throwIfAborted() + const result = await handle.read(buffer, read, length - read, offset + read) + if (result.bytesRead === 0) { + throw new Error('PE diagnostic encountered an early EOF') + } + read += result.bytesRead + } + return buffer +} + +async function addDebugDirectory(handle, parsed, fileSize, signal) { + const debug = parsed.header.optional.dataDirectories.debug + if (!debug || debug.size === 0) { + return + } + const debugRva = Number.parseInt(debug.virtualAddress.slice(2), 16) + const debugOffset = rvaToFileOffset(debugRva, parsed.header, parsed.sections) + if (debugOffset === undefined || debug.size % 28 !== 0) { + throw new Error('PE debug directory does not map to complete entries') + } + const entryCount = debug.size / 28 + if (entryCount > MAX_DEBUG_ENTRIES) { + throw new Error('PE debug directory exceeds the diagnostic entry limit') + } + const bytes = await readAt(handle, debugOffset, debug.size, fileSize, signal) + parsed.regions.push({ + start: debugOffset, + end: debugOffset + debug.size, + label: 'debug directory' + }) + for (let index = 0; index < entryCount; index += 1) { + const offset = index * 28 + const entry = { + characteristics: hex(bytes.readUInt32LE(offset), 8), + timeDateStamp: hex(bytes.readUInt32LE(offset + 4), 8), + version: `${bytes.readUInt16LE(offset + 8)}.${bytes.readUInt16LE(offset + 10)}`, + type: bytes.readUInt32LE(offset + 12), + sizeOfData: bytes.readUInt32LE(offset + 16), + addressOfRawData: hex(bytes.readUInt32LE(offset + 20), 8), + pointerToRawData: bytes.readUInt32LE(offset + 24) + } + if (entry.type === 2 && entry.sizeOfData > 0 && entry.sizeOfData <= 4096) { + const codeView = await readAt( + handle, + entry.pointerToRawData, + entry.sizeOfData, + fileSize, + signal + ) + const signature = codeView.subarray(0, Math.min(4, codeView.length)).toString('ascii') + const pathStart = signature === 'RSDS' && codeView.length >= 24 ? 24 : 4 + const pathBytes = codeView.subarray(pathStart) + entry.codeView = { + signature, + identifier: signature === 'RSDS' ? codeView.subarray(4, 20).toString('hex') : undefined, + age: signature === 'RSDS' ? codeView.readUInt32LE(20) : undefined, + pathBytes: pathBytes.length, + pathSha256: `sha256:${createHash('sha256').update(pathBytes).digest('hex')}` + } + parsed.regions.push({ + start: entry.pointerToRawData, + end: entry.pointerToRawData + entry.sizeOfData, + label: `CodeView data ${index}` + }) + } + parsed.header.debugDirectory.push(entry) + } +} + +async function inspectPe(path, label, signal) { + const metadata = await lstat(path) + if (!metadata.isFile() || metadata.isSymbolicLink()) { + throw new Error(`${label} PE input must be a regular file`) + } + if (metadata.size > MAX_PE_BYTES) { + throw new Error(`${label} PE input exceeds the diagnostic size limit`) + } + const handle = await open(path, 'r') + try { + const prefix = await readAt( + handle, + 0, + Math.min(metadata.size, MAX_HEADER_BYTES), + metadata.size, + signal + ) + const parsed = parsePeHeader(prefix) + await addDebugDirectory(handle, parsed, metadata.size, signal) + return { handle, metadata, parsed } + } catch (error) { + await handle.close() + throw error + } +} + +function finishRange(state, firstRegions, secondRegions) { + if (state.start === undefined) { + return + } + state.rangeCount += 1 + if (state.ranges.length < MAX_DIFF_RANGES) { + const regionAt = (regions) => + regions.find((region) => state.start >= region.start && state.start < region.end)?.label ?? + 'unmapped file data' + state.ranges.push({ + start: state.start, + endExclusive: state.end, + startHex: hex(state.start), + endExclusiveHex: hex(state.end), + length: state.end - state.start, + firstRegion: regionAt(firstRegions), + secondRegion: regionAt(secondRegions) + }) + } + state.start = undefined +} + +async function compareBytes(first, second, signal) { + const firstHash = createHash('sha256') + const secondHash = createHash('sha256') + const state = { differingBytes: 0, end: 0, rangeCount: 0, ranges: [], start: undefined } + const length = Math.max(first.metadata.size, second.metadata.size) + for (let offset = 0; offset < length; offset += READ_CHUNK_BYTES) { + signal.throwIfAborted() + const firstLength = Math.min(READ_CHUNK_BYTES, Math.max(0, first.metadata.size - offset)) + const secondLength = Math.min(READ_CHUNK_BYTES, Math.max(0, second.metadata.size - offset)) + const [firstBytes, secondBytes] = await Promise.all([ + readAt(first.handle, offset, firstLength, first.metadata.size, signal), + readAt(second.handle, offset, secondLength, second.metadata.size, signal) + ]) + firstHash.update(firstBytes) + secondHash.update(secondBytes) + const chunkLength = Math.max(firstLength, secondLength) + for (let index = 0; index < chunkLength; index += 1) { + const different = + index >= firstLength || index >= secondLength || firstBytes[index] !== secondBytes[index] + if (different) { + state.differingBytes += 1 + state.start ??= offset + index + state.end = offset + index + 1 + } else { + finishRange(state, first.parsed.regions, second.parsed.regions) + } + } + } + finishRange(state, first.parsed.regions, second.parsed.regions) + return { + firstSha256: `sha256:${firstHash.digest('hex')}`, + secondSha256: `sha256:${secondHash.digest('hex')}`, + differingBytes: state.differingBytes, + rangeCount: state.rangeCount, + ranges: state.ranges, + rangesTruncated: state.rangeCount > state.ranges.length + } +} + +function collectHeaderDifferences(first, second, path, state) { + if (state.count >= MAX_HEADER_DIFFERENCES) { + state.truncated = true + return + } + if (Object.is(first, second)) { + return + } + if ( + first === null || + second === null || + typeof first !== 'object' || + typeof second !== 'object' + ) { + state.differences.push({ field: path, first: first ?? null, second: second ?? null }) + state.count += 1 + return + } + const keys = [...new Set([...Object.keys(first), ...Object.keys(second)])].sort() + for (const key of keys) { + collectHeaderDifferences(first[key], second[key], path ? `${path}.${key}` : key, state) + } +} + +export async function diagnoseWindowsPeMismatch({ + firstPePath, + secondPePath, + signal = AbortSignal.timeout(DIAGNOSTIC_TIMEOUT_MS) +}) { + const firstPath = resolve(firstPePath) + const secondPath = resolve(secondPePath) + if (firstPath === secondPath) { + throw new Error('PE diagnostic inputs must be distinct files') + } + signal.throwIfAborted() + const first = await inspectPe(firstPath, 'first', signal) + let second + try { + second = await inspectPe(secondPath, 'second', signal) + const comparison = await compareBytes(first, second, signal) + const headerState = { count: 0, differences: [], truncated: false } + collectHeaderDifferences(first.parsed.header, second.parsed.header, '', headerState) + return { + schemaVersion: 1, + first: { + bytes: first.metadata.size, + sha256: comparison.firstSha256, + pe: first.parsed.header + }, + second: { + bytes: second.metadata.size, + sha256: comparison.secondSha256, + pe: second.parsed.header + }, + difference: { + differingBytes: comparison.differingBytes, + rangeCount: comparison.rangeCount, + ranges: comparison.ranges, + rangesTruncated: comparison.rangesTruncated, + headerDifferences: headerState.differences, + headerDifferencesTruncated: headerState.truncated + } + } + } finally { + await Promise.all([first.handle.close(), second?.handle.close()]) + } +} + +function valueAfter(argv, index, flag) { + const value = argv[index + 1] + if (!value || value.startsWith('--')) { + throw new Error(`${flag} requires a value`) + } + return value +} + +export function parseWindowsPeDiagnosticArguments(argv) { + const result = {} + for (let index = 0; index < argv.length; index += 1) { + const flag = argv[index] + const value = valueAfter(argv, index, flag) + if (flag === '--first-pe') { + result.firstPePath = value + } else if (flag === '--second-pe') { + result.secondPePath = value + } else { + throw new Error(`Unknown argument: ${flag}`) + } + index += 1 + } + for (const field of ['firstPePath', 'secondPePath']) { + if (!result[field]) { + throw new Error(`Missing required PE diagnostic argument: ${field}`) + } + } + return result +} + +async function main() { + const result = await diagnoseWindowsPeMismatch( + parseWindowsPeDiagnosticArguments(process.argv.slice(2)) + ) + process.stdout.write(`windows_pe_mismatch=${JSON.stringify(result)}\n`) +} + +if (process.argv[1] && resolve(process.argv[1]) === import.meta.filename) { + main().catch((error) => { + process.stderr.write(`Windows PE mismatch diagnostic failed: ${error.message}\n`) + process.exitCode = 1 + }) +} diff --git a/config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs b/config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs new file mode 100644 index 00000000000..c11ac8d0b46 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs @@ -0,0 +1,142 @@ +import { mkdtemp, open, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { + diagnoseWindowsPeMismatch, + parseWindowsPeDiagnosticArguments +} from './ssh-relay-runtime-windows-pe-diagnostic.mjs' + +const temporaryDirectories = [] + +afterEach(async () => { + await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true }))) +}) + +function peFixture({ identifierByte = 0x11, timestamp = 0x12345678 } = {}) { + const bytes = Buffer.alloc(0x400) + bytes.writeUInt16LE(0x5a4d, 0) + bytes.writeUInt32LE(0x80, 0x3c) + bytes.writeUInt32LE(0x00004550, 0x80) + const coff = 0x84 + bytes.writeUInt16LE(0xaa64, coff) + bytes.writeUInt16LE(1, coff + 2) + bytes.writeUInt32LE(timestamp, coff + 4) + bytes.writeUInt16LE(0xf0, coff + 16) + bytes.writeUInt16LE(0x2022, coff + 18) + const optional = coff + 20 + bytes.writeUInt16LE(0x20b, optional) + bytes[optional + 2] = 14 + bytes.writeUInt32LE(0x100, optional + 4) + bytes.writeUInt32LE(0x100, optional + 8) + bytes.writeUInt32LE(0x2000, optional + 16) + bytes.writeBigUInt64LE(0x180000000n, optional + 24) + bytes.writeUInt32LE(0x1000, optional + 32) + bytes.writeUInt32LE(0x200, optional + 36) + bytes.writeUInt32LE(0x3000, optional + 56) + bytes.writeUInt32LE(0x200, optional + 60) + bytes.writeUInt16LE(3, optional + 68) + bytes.writeUInt16LE(0x160, optional + 70) + bytes.writeUInt32LE(16, optional + 108) + const debugDataDirectory = optional + 112 + 6 * 8 + bytes.writeUInt32LE(0x2000, debugDataDirectory) + bytes.writeUInt32LE(28, debugDataDirectory + 4) + const section = optional + 0xf0 + bytes.write('.rdata', section, 'ascii') + bytes.writeUInt32LE(0x200, section + 8) + bytes.writeUInt32LE(0x2000, section + 12) + bytes.writeUInt32LE(0x200, section + 16) + bytes.writeUInt32LE(0x200, section + 20) + bytes.writeUInt32LE(0x40000040, section + 36) + const debug = 0x200 + bytes.writeUInt32LE(timestamp, debug + 4) + bytes.writeUInt32LE(2, debug + 12) + bytes.writeUInt32LE(32, debug + 16) + bytes.writeUInt32LE(0x2020, debug + 20) + bytes.writeUInt32LE(0x220, debug + 24) + bytes.write('RSDS', 0x220, 'ascii') + bytes.fill(identifierByte, 0x224, 0x234) + bytes.writeUInt32LE(1, 0x234) + bytes.write('test.pdb\0', 0x238, 'ascii') + return bytes +} + +async function fixturePair(firstBytes, secondBytes) { + const directory = await mkdtemp(join(tmpdir(), 'orca-windows-pe-diagnostic-')) + temporaryDirectories.push(directory) + const firstPePath = join(directory, 'first.node') + const secondPePath = join(directory, 'second.node') + await Promise.all([writeFile(firstPePath, firstBytes), writeFile(secondPePath, secondBytes)]) + return { firstPePath, secondPePath } +} + +describe('SSH relay Windows PE mismatch diagnostic', () => { + it('reports bounded byte ranges and relevant header differences without file paths', async () => { + const fixture = await fixturePair( + peFixture(), + peFixture({ identifierByte: 0x22, timestamp: 0x87654321 }) + ) + + const result = await diagnoseWindowsPeMismatch(fixture) + + expect(result).toEqual( + expect.objectContaining({ + schemaVersion: 1, + first: expect.objectContaining({ bytes: 0x400, sha256: expect.stringMatching(/^sha256:/) }), + second: expect.objectContaining({ + bytes: 0x400, + sha256: expect.stringMatching(/^sha256:/) + }), + difference: expect.objectContaining({ + differingBytes: 24, + rangeCount: 3, + rangesTruncated: false + }) + }) + ) + expect(result.first.pe.coff.machine).toBe('0xaa64') + expect(result.first.pe.sections[0].name).toBe('.rdata') + expect(result.difference.ranges.map((range) => range.firstRegion)).toEqual([ + 'COFF header', + 'debug directory', + 'CodeView data 0' + ]) + expect(result.difference.headerDifferences.map((entry) => entry.field)).toEqual([ + 'coff.timeDateStamp', + 'debugDirectory.0.codeView.identifier', + 'debugDirectory.0.timeDateStamp' + ]) + expect(JSON.stringify(result)).not.toContain(fixture.firstPePath) + }) + + it('rejects oversized, invalid, same-file, and aborted diagnostics', async () => { + const fixture = await fixturePair(peFixture(), peFixture()) + await expect( + diagnoseWindowsPeMismatch({ + firstPePath: fixture.firstPePath, + secondPePath: fixture.firstPePath + }) + ).rejects.toThrow('must be distinct') + await writeFile(fixture.secondPePath, 'not a PE') + await expect(diagnoseWindowsPeMismatch(fixture)).rejects.toThrow('bounded header') + + const handle = await open(fixture.secondPePath, 'w') + await handle.truncate(64 * 1024 * 1024 + 1) + await handle.close() + await expect(diagnoseWindowsPeMismatch(fixture)).rejects.toThrow('size limit') + await expect( + diagnoseWindowsPeMismatch({ ...fixture, signal: AbortSignal.abort() }) + ).rejects.toThrow('aborted') + }) + + it('parses only the two explicit CLI inputs', () => { + expect( + parseWindowsPeDiagnosticArguments(['--first-pe', 'first.node', '--second-pe', 'second.node']) + ).toEqual({ firstPePath: 'first.node', secondPePath: 'second.node' }) + expect(() => parseWindowsPeDiagnosticArguments(['--other', 'value'])).toThrow( + 'Unknown argument' + ) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 9d701eaa429..cb6407a90ca 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -61,6 +61,8 @@ describe('SSH relay runtime artifact workflow', () => { expect(source).toContain('foreach ($output in @($firstOutput, $secondOutput))') expect(source).toContain('ssh-relay-runtime-reproducibility.mjs') expect(source).toContain('ssh-relay-runtime-reproducibility.test.mjs') + expect(source).toContain('ssh-relay-runtime-windows-pe-diagnostic.mjs') + expect(source).toContain('ssh-relay-runtime-windows-pe-diagnostic.test.mjs') expect(source).toContain( 'node --check config/scripts/ssh-relay-runtime-reproducibility.test.mjs' ) @@ -100,6 +102,18 @@ describe('SSH relay runtime artifact workflow', () => { run.indexOf('ssh-relay-runtime-reproducibility.mjs') ) } + const windowsRun = windowsSteps.find( + (step) => step.name === 'Build twice, inspect, smoke, and compare exact runtime' + ).run + expect(windowsRun.indexOf('ssh-relay-runtime-windows-pe-diagnostic.mjs')).toBeGreaterThan( + windowsRun.indexOf('ssh-relay-runtime-reproducibility.mjs') + ) + expect( + windowsRun.indexOf("throw 'runtime reproducibility verification failed'") + ).toBeGreaterThan(windowsRun.indexOf('ssh-relay-runtime-windows-pe-diagnostic.mjs')) + expect(windowsRun.indexOf('runtime-evidence/${{ matrix.tuple }}')).toBeGreaterThan( + windowsRun.indexOf("throw 'runtime reproducibility verification failed'") + ) expect(steps[uploadIndex].with.path).toBe('runtime-evidence/${{ matrix.tuple }}/') expect(source).not.toMatch(/releases\/|gh release|contents:\s*write/i) }) 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 53de05f1acd..f7075873c75 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29351557922 proves clean-build equality on Linux x64/arm64, macOS x64/arm64, and Windows x64, but fails closed because Windows arm64 first differs at `conpty_console_list.node`; the active slice is bounded diagnosis/correction of that copied artifact build only; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head runs 29351557922 and 29352510414 prove clean-build equality on Linux x64/arm64, macOS x64/arm64, and Windows x64, but both fail closed because Windows arm64 first differs at `conpty_console_list.node`; the active slice is a bounded PE diagnostic for that copied artifact before any producer correction; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -108,8 +108,11 @@ same change as the work it records. exact commit `f864d3fa6`. Exact-head run 29351557922 then parses and builds on all six native runners: Linux x64/arm64, macOS x64/arm64, and Windows x64 compare exactly and upload, while Windows arm64 fully builds, verifies, and smokes both outputs but fails closed before upload when - `conpty_console_list.node` differs (E-M3-REPRODUCIBILITY-WINDOWS-ARM64-CI-RED-001). Native Windows - arm64 equality remains mandatory; oldest-baseline, native-trust, SSH, and musl cells remain open. + `conpty_console_list.node` differs (E-M3-REPRODUCIBILITY-WINDOWS-ARM64-CI-RED-001). Documentation- + only exact-head repeat run 29352510414 reproduces the same isolated arm64 drift while all five + controls compare and upload under E-M3-REPRODUCIBILITY-WINDOWS-ARM64-REPEAT-CI-RED-001. Native + Windows arm64 equality remains mandatory; oldest-baseline, native-trust, SSH, and musl cells + remain open. - Windows input correction: E-M3-WINDOWS-INPUT-GAP-001 proved the official Windows ZIP lacks headers and `node.lib`. Both artifacts now require the exact signed headers archive and tuple import library as explicit inputs. The schema, signed-checksum verifier, bounded ZIP/header extraction, @@ -149,12 +152,12 @@ 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: commit the exact local evidence record for `f864d3fa6`, push the bounded - correction and evidence commits, and rerun all six target-native jobs. Require Linux and macOS to - remain equal and Windows `/Brepro` outputs to build, smoke, compare equal, and upload. Keep - oldest-baseline, cross-family remote infrastructure, signing/trust, and measured legacy baseline - gates open; do not introduce publication, resolver, transfer, rollout, tuple enablement, or - default behavior. +- Next required action: finish the bounded Windows PE diagnostic that compares the two rejected + `conpty_console_list.node` outputs before cleanup and + reports hashes, sizes, differing byte ranges, and relevant PE headers without uploading either + failed binary. Prove the diagnostic contract locally and on both native Windows runners before + making any copied-artifact producer correction. Keep the strict comparator, x64 behavior, + repository-wide node-pty patch, legacy/default path, and all other release gates unchanged. ## Non-Negotiable Invariants @@ -631,19 +634,17 @@ isolated the first native differences to macOS `pty.node` and Windows `conpty_co under E-M3-REPRODUCIBILITY-DIAGNOSTIC-CI-RED-001. Artifact consumers, publication, the repository-wide node-pty patch, and the legacy/default path remain unchanged. -**Active correction — 2026-07-14, Codex implementation owner:** implementation `a09b02ec4` requires -disjoint exclusive output/work directories, reuses one caller-owned work path across clean passes, -links macOS native outputs with `-Wl,-reproducible` while retaining the required `LC_UUID`, and -adds `/Brepro` only to the copied Windows node-pty source with an exact-source fail-closed guard. -Follow-up `3e433b343` removes the run ID from the work path so separate CI runs use one canonical -runner-local location while their outputs remain run-scoped. E-M3-REPRODUCIBILITY-LINKER-LOCAL-001 -proves two clean macOS passes plus a separate repeat session are byte-identical and loadable. -E-M3-REPRODUCIBILITY-LINKER-CI-RED-001 proves Linux and macOS x64/arm64 equality on exact head -`54df2cb99`; Windows stopped before artifact inputs because Vitest imported the builder's unused Unix -shebang. The active bounded correction removes only that shebang and syntax-checks the builder and -its test on POSIX and Windows before contract collection. Its exact implementation commit -`f864d3fa6` is locally green under E-M3-REPRODUCIBILITY-BUILDER-PARSER-LOCAL-001. Native Windows -proof and the six-runner equality gate remain open; no tuple or production consumer is enabled. +**Active correction — 2026-07-14, Codex implementation owner:** exact-head run 29351557922 proves +the parser and `/Brepro` correction on both Windows architectures, with complete equality on x64. +Windows arm64 builds, verifies, and smokes both complete outputs but first differs at copied-artifact +`conpty_console_list.node`, so the comparator rejects the cell and uploads neither output under +E-M3-REPRODUCIBILITY-WINDOWS-ARM64-CI-RED-001. The active bounded package adds a test-covered PE +mismatch diagnostic before cleanup: hashes, sizes, coalesced differing byte ranges, and relevant PE +headers only, with bounded reads/output and no failed-binary upload. Exact-head repeat run +29352510414 reproduces the same arm64-only drift while all five controls compare and upload under +E-M3-REPRODUCIBILITY-WINDOWS-ARM64-REPEAT-CI-RED-001. No comparator weakening, post-build +normalization, producer correction, repository-wide node-pty change, tuple enablement, or production +consumer is authorized without the diagnostic evidence. Each runtime must contain only the executable closure required by the relay. @@ -4117,6 +4118,62 @@ or unexpected token`; the first logs did not identify a source location. and headers, then correct only the copied artifact build if evidence identifies a safe source. Do not weaken comparison, upload failed outputs, or change repository-wide/legacy node-pty. +### E-M3-REPRODUCIBILITY-WINDOWS-ARM64-REPEAT-CI-RED-001 — Independent repeat of arm64-only drift + +- Date: 2026-07-14 +- Commit SHA / PR: documentation-only exact head `a7151f9750fd9bfcdcff8c01c1fd6caff2e6116a`; + implementation bytes remain `de541efd11989d2e6dce0402307912afffae3510`; stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Run and jobs: [run 29352510414](https://github.com/stablyai/orca/actions/runs/29352510414), + conclusion `failure`; Linux x64 `87151980265`, Linux arm64 `87151980298`, macOS arm64 + `87151980396`, macOS x64 `87151980327`, Windows x64 `87151980324`, and failing Windows arm64 + `87151980264` +- Runners: the same six native labels/toolchain family recorded in + E-M3-REPRODUCIBILITY-WINDOWS-ARM64-CI-RED-001. The failing cell was Windows 11 Enterprise + 10.0.26200 arm64, image `windows-11-arm64` `20260706.102.1`, runner `2.335.1`, provisioner + `20260624.560`, Node v24.18.0, MSVC 19.44.35228 / tools 14.44.35207, Windows SDK 10.0.26100.0, + and Python 3.13.14. +- Remote and transport: none; independent target-native artifact assembly/execution and unpublished + Actions artifact upload only +- Exact evidence commands: + + ```sh + gh run view 29352510414 --repo stablyai/orca \ + --json databaseId,headSha,status,conclusion,url,updatedAt,jobs + gh api repos/stablyai/orca/actions/jobs/87151980264/logs + gh api 'repos/stablyai/orca/actions/runs/29352510414/artifacts?per_page=100' + ``` + +- Result: FAIL as expected at the unchanged native boundary. All 14 contract suites passed on + Windows arm64 with 44 tests passed and three POSIX-only skips. Linux x64/arm64, macOS x64/arm64, + and Windows x64 independently built twice, inspected, smoked, compared exactly, and uploaded + unpublished artifacts `8318818618`, `8318833832`, `8318812505`, `8318846378`, and `8318866333`. + Windows arm64 again built, inspected, and smoked two complete 60-entry, 42-file, + 86,189,895-byte outputs, then first differed at + `runtime/node_modules/node-pty/build/Release/conpty_console_list.node`; upload was skipped. +- Windows arm64 rejected outputs: first content ID + `e13f39ae96eba36c3ed41054da7429533e2eebbdd7d845a712d802d002906bc8`, ZIP 33,261,548 bytes + with SHA-256 `ca1be18b572e353ac2eb3a390d91e5d0acb93100735f387ce74300cb48ba39d1`; + second content ID `4989a282c6529df6066aefbb5667ca96777907d54f1d9a48041f8ff98555b9c0`, + ZIP 33,261,548 bytes with SHA-256 + `e91664074aa2f434ea0e2603596410c119b89b2a4a4d7eab453bfb7407ff91d0`. +- Duration and resource metrics: jobs completed in 3m9s Linux x64, 3m39s Linux arm64, 2m56s macOS + arm64, 4m9s macOS x64, 4m53s Windows x64, and 8m48s Windows arm64. Arm64 native builds took + 145,700 ms and 124,010 ms; smoke took 6,042 ms and 5,564 ms at 53,075,968 and 52,977,664 bytes + RSS. Build peak RSS, open files/channels, and cancellation settlement were not instrumented. +- Artifact/log/trace link: run/job above and the five unpublished seven-day artifacts; no failed + arm64 bytes were uploaded +- Oracle proved: the Windows arm64 difference repeats across an independent run while the same-head + Windows x64 and four POSIX controls remain reproducible; the strict gate consistently prevents a + rejected output from upload. The drift is not a transient failure from run 29351557922. +- Does not prove: differing PE byte ranges/headers, a producer correction, arm64 equality, + cross-run equality, oldest baselines, native trust/signing, SSH, publication, transfer, fallback, + UI, or any enabled tuple. +- Checklist items satisfied: no new completion checkbox; this repeat evidence justifies the bounded + PE diagnostic before any producer change. +- Follow-up: run the bounded diagnostic on both native Windows runners, require x64 to remain equal, + and use only the rejected arm64 headers/ranges to choose or reject a copied-artifact correction. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 27c6a5718c544cd3f559d6adc24cc0beca2ed59e Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 13:21:13 -0400 Subject: [PATCH 033/267] docs(ssh): record bounded PE diagnostic evidence Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 122 ++++++++++++++---- 1 file changed, 99 insertions(+), 23 deletions(-) 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 f7075873c75..97ea10ca895 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head runs 29351557922 and 29352510414 prove clean-build equality on Linux x64/arm64, macOS x64/arm64, and Windows x64, but both fail closed because Windows arm64 first differs at `conpty_console_list.node`; the active slice is a bounded PE diagnostic for that copied artifact before any producer correction; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head runs 29351557922 and 29352510414 prove clean-build equality on Linux x64/arm64, macOS x64/arm64, and Windows x64, but both fail closed because Windows arm64 first differs at `conpty_console_list.node`; bounded PE diagnostic implementation `39ee3451b` is locally green and awaiting native evidence before any producer correction; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -112,7 +112,9 @@ same change as the work it records. only exact-head repeat run 29352510414 reproduces the same isolated arm64 drift while all five controls compare and upload under E-M3-REPRODUCIBILITY-WINDOWS-ARM64-REPEAT-CI-RED-001. Native Windows arm64 equality remains mandatory; oldest-baseline, native-trust, SSH, and musl cells - remain open. + remain open. E-M3-WINDOWS-PE-DIAGNOSTIC-LOCAL-001 proves the bounded diagnostic and fail-closed + workflow ordering locally at exact implementation commit `39ee3451b`; both native Windows cells + remain required before interpreting or correcting the producer. - Windows input correction: E-M3-WINDOWS-INPUT-GAP-001 proved the official Windows ZIP lacks headers and `node.lib`. Both artifacts now require the exact signed headers archive and tuple import library as explicit inputs. The schema, signed-checksum verifier, bounded ZIP/header extraction, @@ -152,11 +154,10 @@ 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: finish the bounded Windows PE diagnostic that compares the two rejected - `conpty_console_list.node` outputs before cleanup and - reports hashes, sizes, differing byte ranges, and relevant PE headers without uploading either - failed binary. Prove the diagnostic contract locally and on both native Windows runners before - making any copied-artifact producer correction. Keep the strict comparator, x64 behavior, +- Next required action: push exact implementation commit `39ee3451b` and run both native Windows + cells. Require x64 to remain exactly reproducible and arm64 to emit bounded hashes, sizes, + differing byte ranges, and relevant PE headers before failing with no upload. Make no + copied-artifact producer correction until that evidence is recorded. Keep the strict comparator, repository-wide node-pty patch, legacy/default path, and all other release gates unchanged. ## Non-Negotiable Invariants @@ -644,7 +645,8 @@ headers only, with bounded reads/output and no failed-binary upload. Exact-head 29352510414 reproduces the same arm64-only drift while all five controls compare and upload under E-M3-REPRODUCIBILITY-WINDOWS-ARM64-REPEAT-CI-RED-001. No comparator weakening, post-build normalization, producer correction, repository-wide node-pty change, tuple enablement, or production -consumer is authorized without the diagnostic evidence. +consumer is authorized without the diagnostic evidence. Exact implementation commit `39ee3451b` is +locally green under E-M3-WINDOWS-PE-DIAGNOSTIC-LOCAL-001; native Windows execution remains open. Each runtime must contain only the executable closure required by the relay. @@ -1244,16 +1246,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; Windows arm64 equality diagnosis active | `de541efd1`; E-M3-REPRODUCIBILITY-WINDOWS-ARM64-CI-RED-001, E-M3-CI-001, E-M3-WINDOWS-CI-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; PE diagnostic awaiting native evidence | `39ee3451b`; E-M3-WINDOWS-PE-DIAGNOSTIC-LOCAL-001; arm64 repeat-red evidence | +| 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 | — | Every PR must document: @@ -4174,6 +4176,80 @@ or unexpected token`; the first logs did not identify a source location. - Follow-up: run the bounded diagnostic on both native Windows runners, require x64 to remain equal, and use only the rejected arm64 headers/ranges to choose or reject a copied-artifact correction. +### E-M3-WINDOWS-PE-DIAGNOSTIC-LOCAL-001 — Bounded rejected-binary diagnostics + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `39ee3451b8cc38b5311a0cb1085ad48a1f302185`; stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741), push/native execution pending +- Runner: macOS 26.2 build 25C56, native Apple M4 arm64; Node v26.0.0 and pnpm 10.24.0. The + repository requires Node 24, so target-native Windows jobs remain authoritative. +- Remote and transport: none; synthetic PE32+ arm64 fixtures and workflow source contracts only +- Exact commands: + + ```sh + node --check config/scripts/ssh-relay-runtime-windows-pe-diagnostic.mjs + node --check config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.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 \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-build.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs + pnpm run typecheck + pnpm exec oxlint config/scripts/ssh-relay-runtime-windows-pe-diagnostic.mjs \ + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check .github/workflows/ssh-relay-runtime-artifacts.yml \ + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.mjs \ + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md + git diff --check + ``` + +- Result: PASS at the exact implementation commit. Both syntax checks exited zero; the purpose + command passed 5/5 tests across two suites; all 15 artifact suites passed 50/50 tests. Typecheck, + focused oxlint, the 355-entry max-lines ratchet, full repository + lint/reliability/localization, formatting, staged hooks, and diff checks exited zero. Full lint + emitted only pre-existing warnings outside this package. +- Duration and resource metrics: syntax checks 50 ms and 40 ms; purpose suite 142 ms Vitest / 0.76s + wall; complete artifact suite 470 ms Vitest / 1.15s wall; typecheck 2.68s; focused oxlint 0.69s; + max-lines 1.58s; full lint 11.92s; formatting check 1.82s. The implementation rejects inputs over + 64 MiB, reads in 64 KiB chunks, caps the header at 1 MiB, the section/debug tables at 96/32 + entries, byte/header differences at 128 each, and the whole diagnostic at 60 seconds. Synthetic + peak RSS/open files were not instrumented. +- Artifact/log/trace link: exact source commit; no binary artifact was created or uploaded +- Oracle proved: valid PE32+ fixtures report whole-file sizes/SHA-256, coalesced file-offset ranges, + COFF/optional/data-directory/section/debug headers, and hashed—not printed—CodeView paths; + oversized, malformed, same-file, unknown-argument, and pre-aborted inputs fail boundedly. Workflow + contracts require diagnostic syntax/tests on Windows, invoke it only after the strict comparator + rejects the two retained `conpty_console_list.node` files, throw afterward, and leave evidence + copying unreachable, so rejected binaries are not uploaded. No max-lines bypass was added. +- Does not prove: parsing the real x64/arm64 linker outputs, native Node 24 behavior, the actual + differing ranges/headers, x64 equality after the workflow change, a producer correction, arm64 + equality, oldest baselines, native trust, SSH, publication, transfer, fallback, UI, or an enabled + tuple. +- Checklist items satisfied: local bounded diagnostic and fail-closed workflow ordering only; no + native, tuple, or production checkbox. +- Follow-up: push the exact implementation, require native x64 equality and arm64 diagnostic output + with no upload, then record the exact PE evidence before changing the producer. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -4231,11 +4307,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Add a bounded Windows PE diagnostic that compares the two native arm64 -`conpty_console_list.node` outputs before cleanup, reports hashes, differing ranges, and relevant -headers without uploading failed bytes, and proves x64 stays unchanged. Use that evidence to make -the narrowest copied-artifact build correction, then rerun local gates and all six target-native -jobs without weakening the comparator. Cross-family Layer B targets, the protected +Push exact implementation commit `39ee3451b` and require native Windows x64 to remain exactly +reproducible while arm64 emits bounded hashes, sizes, differing ranges, and relevant PE headers +before failing with no upload. Use only that evidence to choose or reject the narrowest +copied-artifact build correction, then rerun local gates and all six target-native jobs without +weakening the comparator. 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 publication, desktop resolver, SSH transfer/ install, per-target Beta, fallback, tuple enablement, or default behavior may be connected by this From cd7f941365bf6e631cf0f9947f517ecef02afc8e Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 13:37:48 -0400 Subject: [PATCH 034/267] test(ssh): summarize complete Windows PE drift Co-authored-by: Orca --- ...sh-relay-runtime-windows-pe-diagnostic.mjs | 111 +++++++++++++++--- ...lay-runtime-windows-pe-diagnostic.test.mjs | 56 +++++++-- 2 files changed, 144 insertions(+), 23 deletions(-) diff --git a/config/scripts/ssh-relay-runtime-windows-pe-diagnostic.mjs b/config/scripts/ssh-relay-runtime-windows-pe-diagnostic.mjs index 795ae1dceaf..df567a823cd 100644 --- a/config/scripts/ssh-relay-runtime-windows-pe-diagnostic.mjs +++ b/config/scripts/ssh-relay-runtime-windows-pe-diagnostic.mjs @@ -8,6 +8,8 @@ const MAX_SECTIONS = 96 const MAX_DEBUG_ENTRIES = 32 const MAX_DIFF_RANGES = 128 const MAX_HEADER_DIFFERENCES = 128 +const MAX_RANGE_SAMPLE_BYTES = 32 +const MAX_REGION_SAMPLES = 8 const READ_CHUNK_BYTES = 64 * 1024 const DIAGNOSTIC_TIMEOUT_MS = 60_000 @@ -146,11 +148,76 @@ function parsePeHeader(buffer) { start: section.headerOffset, end: section.headerOffset + 40, label: `section header ${section.name}` - })) + })), + ...sections + .filter((section) => section.rawSize > 0) + .map((section) => ({ + start: section.rawPointer, + end: section.rawPointer + section.rawSize, + label: `section ${section.name}` + })) ] return { header, regions, sections } } +function regionAt(regions, offset) { + const matches = regions.filter((region) => offset >= region.start && offset < region.end) + matches.sort((left, right) => left.end - left.start - (right.end - right.start)) + return matches[0]?.label ?? 'unmapped file data' +} + +function bytesHex(bytes) { + return bytes + .map((byte) => (byte === undefined ? '--' : byte.toString(16).padStart(2, '0'))) + .join('') +} + +function rangeRecord(state, firstRegions, secondRegions) { + const firstRegion = regionAt(firstRegions, state.start) + const secondRegion = regionAt(secondRegions, state.start) + return { + start: state.start, + endExclusive: state.end, + startHex: hex(state.start), + endExclusiveHex: hex(state.end), + length: state.end - state.start, + firstRegion, + secondRegion, + firstBytes: bytesHex(state.firstBytes), + secondBytes: bytesHex(state.secondBytes), + bytesTruncated: state.end - state.start > state.firstBytes.length + } +} + +function addRegionSummary(state, range) { + const key = `${range.firstRegion}\0${range.secondRegion}` + let summary = state.regionSummaries.get(key) + if (!summary) { + summary = { + firstRegion: range.firstRegion, + secondRegion: range.secondRegion, + differingBytes: 0, + rangeCount: 0, + firstOffset: range.start, + lastEndExclusive: range.endExclusive, + samples: [] + } + state.regionSummaries.set(key, summary) + } + summary.differingBytes += range.length + summary.rangeCount += 1 + summary.lastEndExclusive = range.endExclusive + if (summary.samples.length < MAX_REGION_SAMPLES) { + summary.samples.push({ + start: range.start, + endExclusive: range.endExclusive, + firstBytes: range.firstBytes, + secondBytes: range.secondBytes, + bytesTruncated: range.bytesTruncated + }) + } +} + function rvaToFileOffset(rva, header, sections) { if (rva < header.optional.sizeOfHeaders) { return rva @@ -275,27 +342,29 @@ function finishRange(state, firstRegions, secondRegions) { return } state.rangeCount += 1 + const range = rangeRecord(state, firstRegions, secondRegions) + addRegionSummary(state, range) if (state.ranges.length < MAX_DIFF_RANGES) { - const regionAt = (regions) => - regions.find((region) => state.start >= region.start && state.start < region.end)?.label ?? - 'unmapped file data' - state.ranges.push({ - start: state.start, - endExclusive: state.end, - startHex: hex(state.start), - endExclusiveHex: hex(state.end), - length: state.end - state.start, - firstRegion: regionAt(firstRegions), - secondRegion: regionAt(secondRegions) - }) + state.ranges.push(range) } state.start = undefined + state.firstBytes = [] + state.secondBytes = [] } async function compareBytes(first, second, signal) { const firstHash = createHash('sha256') const secondHash = createHash('sha256') - const state = { differingBytes: 0, end: 0, rangeCount: 0, ranges: [], start: undefined } + const state = { + differingBytes: 0, + end: 0, + firstBytes: [], + rangeCount: 0, + ranges: [], + regionSummaries: new Map(), + secondBytes: [], + start: undefined + } const length = Math.max(first.metadata.size, second.metadata.size) for (let offset = 0; offset < length; offset += READ_CHUNK_BYTES) { signal.throwIfAborted() @@ -313,7 +382,15 @@ async function compareBytes(first, second, signal) { index >= firstLength || index >= secondLength || firstBytes[index] !== secondBytes[index] if (different) { state.differingBytes += 1 - state.start ??= offset + index + if (state.start === undefined) { + state.start = offset + index + state.firstBytes = [] + state.secondBytes = [] + } + if (state.firstBytes.length < MAX_RANGE_SAMPLE_BYTES) { + state.firstBytes.push(index < firstLength ? firstBytes[index] : undefined) + state.secondBytes.push(index < secondLength ? secondBytes[index] : undefined) + } state.end = offset + index + 1 } else { finishRange(state, first.parsed.regions, second.parsed.regions) @@ -327,7 +404,8 @@ async function compareBytes(first, second, signal) { differingBytes: state.differingBytes, rangeCount: state.rangeCount, ranges: state.ranges, - rangesTruncated: state.rangeCount > state.ranges.length + rangesTruncated: state.rangeCount > state.ranges.length, + regionSummaries: [...state.regionSummaries.values()] } } @@ -390,6 +468,7 @@ export async function diagnoseWindowsPeMismatch({ rangeCount: comparison.rangeCount, ranges: comparison.ranges, rangesTruncated: comparison.rangesTruncated, + regionSummaries: comparison.regionSummaries, headerDifferences: headerState.differences, headerDifferencesTruncated: headerState.truncated } diff --git a/config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs b/config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs index c11ac8d0b46..5e2944f7f42 100644 --- a/config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs +++ b/config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs @@ -74,10 +74,10 @@ async function fixturePair(firstBytes, secondBytes) { describe('SSH relay Windows PE mismatch diagnostic', () => { it('reports bounded byte ranges and relevant header differences without file paths', async () => { - const fixture = await fixturePair( - peFixture(), - peFixture({ identifierByte: 0x22, timestamp: 0x87654321 }) - ) + const first = peFixture() + const second = peFixture({ identifierByte: 0x22, timestamp: 0x87654321 }) + second[0x300] = 0xff + const fixture = await fixturePair(first, second) const result = await diagnoseWindowsPeMismatch(fixture) @@ -90,8 +90,8 @@ describe('SSH relay Windows PE mismatch diagnostic', () => { sha256: expect.stringMatching(/^sha256:/) }), difference: expect.objectContaining({ - differingBytes: 24, - rangeCount: 3, + differingBytes: 25, + rangeCount: 4, rangesTruncated: false }) }) @@ -101,8 +101,22 @@ describe('SSH relay Windows PE mismatch diagnostic', () => { expect(result.difference.ranges.map((range) => range.firstRegion)).toEqual([ 'COFF header', 'debug directory', - 'CodeView data 0' + 'CodeView data 0', + 'section .rdata' + ]) + expect(result.difference.regionSummaries).toEqual([ + expect.objectContaining({ firstRegion: 'COFF header', differingBytes: 4, rangeCount: 1 }), + expect.objectContaining({ firstRegion: 'debug directory', differingBytes: 4, rangeCount: 1 }), + expect.objectContaining({ + firstRegion: 'CodeView data 0', + differingBytes: 16, + rangeCount: 1 + }), + expect.objectContaining({ firstRegion: 'section .rdata', differingBytes: 1, rangeCount: 1 }) ]) + expect(result.difference.regionSummaries[3].samples[0]).toEqual( + expect.objectContaining({ firstBytes: '00', secondBytes: 'ff', bytesTruncated: false }) + ) expect(result.difference.headerDifferences.map((entry) => entry.field)).toEqual([ 'coff.timeDateStamp', 'debugDirectory.0.codeView.identifier', @@ -131,6 +145,34 @@ describe('SSH relay Windows PE mismatch diagnostic', () => { ).rejects.toThrow('aborted') }) + it('caps per-region samples and per-range byte excerpts', async () => { + const first = peFixture() + const second = peFixture() + for (let index = 0; index < 10; index += 1) { + second[0x280 + index * 2] = 0xff + } + second.fill(0xff, 0x340, 0x368) + const result = await diagnoseWindowsPeMismatch(await fixturePair(first, second)) + + expect(result.difference.regionSummaries).toEqual([ + expect.objectContaining({ + firstRegion: 'section .rdata', + differingBytes: 50, + rangeCount: 11, + samples: expect.any(Array) + }) + ]) + expect(result.difference.regionSummaries[0].samples).toHaveLength(8) + expect(result.difference.ranges.at(-1)).toEqual( + expect.objectContaining({ + length: 40, + bytesTruncated: true, + firstBytes: '00'.repeat(32), + secondBytes: 'ff'.repeat(32) + }) + ) + }) + it('parses only the two explicit CLI inputs', () => { expect( parseWindowsPeDiagnosticArguments(['--first-pe', 'first.node', '--second-pe', 'second.node']) From a51093f009a03a1105e9f0b86be14797a8046414 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 13:39:07 -0400 Subject: [PATCH 035/267] docs(ssh): record full PE scan gate Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 185 ++++++++++++++++-- 1 file changed, 170 insertions(+), 15 deletions(-) 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 97ea10ca895..c85ab900f18 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head runs 29351557922 and 29352510414 prove clean-build equality on Linux x64/arm64, macOS x64/arm64, and Windows x64, but both fail closed because Windows arm64 first differs at `conpty_console_list.node`; bounded PE diagnostic implementation `39ee3451b` is locally green and awaiting native evidence before any producer correction; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29353432240 keeps Linux x64/arm64, macOS x64/arm64, and Windows x64 exactly reproducible and proves the bounded PE diagnostic natively on Windows arm64; the rejected arm64 files have identical 956,928-byte layouts but differ in 2,946 bytes across 2,887 ranges beginning in `.text`, plus `/Brepro` timestamp/CodeView identity fields; bounded full-scan section summaries are locally green at exact implementation commit `cd7f94136` and await the six target-native cells before any producer correction; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -114,7 +114,11 @@ same change as the work it records. Windows arm64 equality remains mandatory; oldest-baseline, native-trust, SSH, and musl cells remain open. E-M3-WINDOWS-PE-DIAGNOSTIC-LOCAL-001 proves the bounded diagnostic and fail-closed workflow ordering locally at exact implementation commit `39ee3451b`; both native Windows cells - remain required before interpreting or correcting the producer. + remained required before interpreting or correcting the producer. Exact-head run 29353432240 + keeps x64 and all POSIX controls green, then reports the real arm64 PE hashes/layout/ranges and + fails without upload under E-M3-WINDOWS-PE-DIAGNOSTIC-CI-RED-001. Because the 128 detailed-range + cap is exhausted by `.text`, the active diagnostic-only correction adds raw-section labels and a + bounded summary across all 2,887 ranges before any producer change. - Windows input correction: E-M3-WINDOWS-INPUT-GAP-001 proved the official Windows ZIP lacks headers and `node.lib`. Both artifacts now require the exact signed headers archive and tuple import library as explicit inputs. The schema, signed-checksum verifier, bounded ZIP/header extraction, @@ -154,11 +158,12 @@ 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: push exact implementation commit `39ee3451b` and run both native Windows - cells. Require x64 to remain exactly reproducible and arm64 to emit bounded hashes, sizes, - differing byte ranges, and relevant PE headers before failing with no upload. Make no - copied-artifact producer correction until that evidence is recorded. Keep the strict comparator, - repository-wide node-pty patch, legacy/default path, and all other release gates unchanged. +- Next required action: push exact implementation commit `cd7f94136` plus its evidence-ledger head + and run all six target-native cells. Keep every current POSIX and Windows x64 control exactly + reproducible; require Windows arm64 to fail without upload after classifying all 2,887 ranges by + raw section and known PE metadata. Make no copied-artifact producer correction until that evidence + is recorded; keep the strict comparator, repository-wide node-pty patch, legacy/default path, and + all other release gates unchanged. ## Non-Negotiable Invariants @@ -535,8 +540,9 @@ SSH users or making all of a user's hosts depend on one experimental path. - [x] Keep `legacy` as the persisted and effective default for every existing and newly added SSH target. An absent or unknown configuration value must deserialize conservatively to `legacy`. (E-M1-ROLLOUT-DECISION-001) -- [x] Expose the bundled-preferred path as a Beta-tagged option on SSH target add and edit surfaces, - stored per target rather than as a global experiment. (E-M1-ROLLOUT-DECISION-001) +- [x] Expose the bundled-preferred path as an off-by-default option on SSH target add and edit + surfaces, using the same Beta-tag treatment as existing Beta features and storing the choice + per target rather than as a global experiment. (E-M1-ROLLOUT-DECISION-001) - [x] Represent the persisted choice as an extensible mode such as `legacy | bundled-auto`, not a boolean. Keep forced `bundled` as a separate engineering/support diagnostic and do not expose it as the normal Beta choice. Exact schema naming remains an implementation detail. @@ -646,7 +652,11 @@ headers only, with bounded reads/output and no failed-binary upload. Exact-head E-M3-REPRODUCIBILITY-WINDOWS-ARM64-REPEAT-CI-RED-001. No comparator weakening, post-build normalization, producer correction, repository-wide node-pty change, tuple enablement, or production consumer is authorized without the diagnostic evidence. Exact implementation commit `39ee3451b` is -locally green under E-M3-WINDOWS-PE-DIAGNOSTIC-LOCAL-001; native Windows execution remains open. +locally green under E-M3-WINDOWS-PE-DIAGNOSTIC-LOCAL-001. Exact-head run 29353432240 proves the x64 +control and real arm64 parser, but its 128 detailed ranges are exhausted by `.text`; the active +diagnostic-only correction adds bounded full-scan section totals/samples before any producer change. +Exact implementation commit `cd7f94136` is locally green under +E-M3-WINDOWS-PE-FULL-SCAN-LOCAL-001 and awaits native evidence. Each runtime must contain only the executable closure required by the relay. @@ -955,6 +965,11 @@ engineering/support control. opted in. - [ ] Add the Beta-tagged option to SSH target add and edit surfaces using the existing design-system Beta treatment; persist it only for that target. +- [ ] Prove the add-target control is visibly off, the edit-target control reflects only that + target's persisted mode, and saving or reconnecting one target cannot change another target. +- [ ] Prove an off, absent, unknown, malformed, existing, or imported setting routes directly through + the current legacy bootstrap without entering bundled manifest, cache, download, transfer, or + install work. - [ ] Prove toggling the option does not mutate or restart a live relay and takes effect only on the next connection or explicit reconnect. - [ ] For fail-closed Beta errors, provide an explicit local “disable Beta and reconnect” recovery @@ -4250,6 +4265,145 @@ or unexpected token`; the first logs did not identify a source location. - Follow-up: push the exact implementation, require native x64 equality and arm64 diagnostic output with no upload, then record the exact PE evidence before changing the producer. +### E-M3-WINDOWS-PE-DIAGNOSTIC-CI-RED-001 — Real arm64 PE drift with x64 control + +- Date: 2026-07-14 +- Commit SHA / PR: exact documentation head `27c6a5718c544cd3f559d6adc24cc0beca2ed59e`, + containing diagnostic implementation `39ee3451b8cc38b5311a0cb1085ad48a1f302185`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Run and jobs: [run 29353432240](https://github.com/stablyai/orca/actions/runs/29353432240), + conclusion `failure`; Windows arm64 `87155072734`, macOS x64 `87155072752`, Linux x64 + `87155072794`, macOS arm64 `87155072820`, Linux arm64 `87155072837`, and Windows x64 + `87155072849` +- Runners: the six target-native labels recorded in prior reproducibility evidence. The diagnostic + cell was Windows 11 Enterprise 10.0.26200 arm64, image `windows-11-arm64` `20260706.102.1`, + runner `2.335.1`, provisioner `20260624.560`, Node v24.18.0, MSVC 19.44.35228 / tools + 14.44.35207, Windows SDK 10.0.26100.0, and Python 3.13.14. +- Remote and transport: none; target-native artifact assembly/execution, rejected-file diagnostics, + and unpublished Actions artifact upload only +- Exact evidence commands: + + ```sh + gh run view 29353432240 --repo stablyai/orca \ + --json headSha,status,conclusion,url,createdAt,updatedAt,jobs + gh api repos/stablyai/orca/actions/jobs/87155072734/logs + gh api 'repos/stablyai/orca/actions/runs/29353432240/artifacts?per_page=100' + ``` + +- Result: FAIL as the intended evidence-producing gate. Both Windows jobs passed 15 suites with 47 + tests passed and three POSIX-only skips, including native Node 24 execution of the new diagnostic + contracts. Linux x64/arm64, macOS x64/arm64, and Windows x64 built twice, inspected, smoked, + compared exactly, and uploaded unpublished artifacts `8319201464`, `8319207054`, `8319191783`, + `8319227039`, and `8319248077`. Windows x64 therefore proves the diagnostic package does not + disturb the successful comparator/upload path. Windows arm64 again built, inspected, and smoked + both complete outputs, then failed at `conpty_console_list.node`; the diagnostic ran before the + fatal throw and artifact upload was skipped. +- Rejected arm64 runtime outputs: first content ID + `bb05f852159277dea617679f84304d2bc3b01d446ceffd07fb58c9469038bcf6`, ZIP 33,261,545 bytes + with SHA-256 `248a4962141e5b1b1c5b24433993923171c5c1d342b8db4de624b92a1019823c`; + second content ID `9d937233f63b142f03e504af2ffeeeb05b2de2b979047a369a79dd1d793b1786`, + ZIP 33,261,544 bytes with SHA-256 + `24f752754890704e991f5cc85663a6bb7a6934fc5d64f81ea2414ea12778c249`. +- PE diagnostic result: both native modules are exactly 956,928 bytes with identical machine + `0xaa64`, 12-section layout, PE32+ image/section/file alignment, 840,192-byte code section, + data directories, load config, imports, relocations, and linker version 14.44. Their SHA-256 values + are `7ef783c59d73cd6101fbed4ef634ef3d075448726c118b36cca4d17145e35afa` and + `196dc3373e52350abdf33c30f80a00e0b9b71c96b53b47ba337b1abe428ac8fc`. + Exactly 2,946 bytes differ across 2,887 coalesced ranges. The first code difference is at file + offset `0x41c`, followed by one-byte ranges at 16-byte intervals in the retained sample. Header + differences are confined to COFF/debug timestamps `dd34d0b3` versus `0211cbf1` and CodeView IDs + `383b9420683f05ced18d65c8e95b4a72` versus `459643a06a5ac306094361fd1788cbe5`; + CodeView age and the SHA-256 of the unprinted 92-byte PDB path are identical. The detailed-range + list correctly reports truncation after 128 of 2,887 ranges. +- Duration and resource metrics: jobs were 8m48s Windows arm64, 4m12s macOS x64, 3m15s Linux x64, + 2m57s macOS arm64, 3m24s Linux arm64, and 5m0s Windows x64. Arm64 native builds took 148,290 ms + and 120,068 ms; smoke took 5,871 ms and 5,421 ms at 51,417,088 and 52,633,600 bytes RSS. The PE + diagnostic completed between log timestamps 17:30:23.088 and 17:30:23.272 UTC. Build peak RSS, + open files/channels, and cancellation settlement were not instrumented. +- Artifact/log/trace link: run/job above and five unpublished seven-day artifacts; no rejected + arm64 bytes were uploaded +- Oracle proved: native Windows parses and bounds the real PE files, x64 remains fully reproducible, + arm64 output layout and PDB path are stable, and drift affects thousands of mostly single-byte + code ranges plus `/Brepro`-derived identity fields rather than only a timestamp. The comparator + and no-upload boundary remain strict. +- Does not prove: the full per-section distribution because the first diagnostic version labels raw + section data as unmapped and caps detailed ranges at 128; the responsible compiler/linker input, + a safe producer correction, arm64 equality, cross-run equality, oldest baselines, native trust, + SSH, publication, transfer, fallback, UI, or any enabled tuple. +- Checklist items satisfied: native PE diagnostic and Windows x64 control only; no tuple or + production checkbox. +- Follow-up: add raw-section attribution plus bounded per-section totals/samples across the complete + scan, rerun both Windows cells, and do not change the producer until that evidence is recorded. + +### E-M3-WINDOWS-PE-FULL-SCAN-LOCAL-001 — Bounded complete-region PE summary + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `cd7f941365bf6e631cf0f9947f517ecef02afc8e`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741), native execution pending +- Runner: macOS 26.2 build 25C56, native Apple M4 arm64; Node v26.0.0 and pnpm 10.24.0. The + repository requires Node 24, so the target-native jobs remain authoritative. +- Remote and transport: none; synthetic PE32+ fixtures and workflow-source contracts only +- Exact commands: + + ```sh + node --check config/scripts/ssh-relay-runtime-windows-pe-diagnostic.mjs + node --check config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.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 \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-build.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs + pnpm run typecheck + pnpm exec oxlint config/scripts/ssh-relay-runtime-windows-pe-diagnostic.mjs \ + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check .github/workflows/ssh-relay-runtime-artifacts.yml \ + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.mjs \ + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md + git diff --check + ``` + +- Result: PASS. Both syntax checks exited zero; the purpose command passed 6/6 tests across two + suites; all 15 artifact suites passed 51/51 tests. Typecheck, focused oxlint, the 355-entry + max-lines ratchet, full repository lint/reliability/localization, formatting, staged hooks, and + diff checks exited zero. Full lint emitted only pre-existing warnings outside this package. +- Duration and resource metrics: purpose suite 246 ms Vitest; complete artifact suite 963 ms Vitest + / 2.24s wall; typecheck 2.52s; focused oxlint 0.68s; max-lines 1.54s; full lint 11.09s; formatting + and diff check 1.06s. The full scan still reads in 64 KiB chunks; detailed ranges remain capped at + 128, each byte excerpt at 32 bytes, each region at eight samples, and the entire diagnostic at 60 + seconds. Synthetic peak RSS/open files were not instrumented. +- Artifact/log/trace link: exact source commit; no binary artifact was created or uploaded +- Oracle proved: every coalesced range contributes to bounded per-region differing-byte and range + totals even after the detailed-range cap is exhausted; raw PE section data is attributed by the + most specific overlapping region, preserving CodeView/debug labels; excerpts and region samples + remain bounded. The comparator, fatal failure, and no-upload ordering are unchanged. +- Does not prove: the real arm64 section distribution, Windows x64 control, arm64 reproducibility, + a producer correction, oldest baselines, native trust, SSH, publication, transfer, fallback, UI, + or any enabled tuple. +- Checklist items satisfied: diagnostic observability only; no artifact, tuple, or production box. +- Follow-up: push the exact implementation and ledger head, run all six target-native cells, require + five reproducible uploads plus a rejected Windows arm64 full-scan summary, and record that evidence + before changing any copied-artifact producer input. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -4307,11 +4461,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push exact implementation commit `39ee3451b` and require native Windows x64 to remain exactly -reproducible while arm64 emits bounded hashes, sizes, differing ranges, and relevant PE headers -before failing with no upload. Use only that evidence to choose or reject the narrowest -copied-artifact build correction, then rerun local gates and all six target-native jobs without -weakening the comparator. Cross-family Layer B targets, the protected +Push exact implementation commit `cd7f94136` plus its evidence-ledger head and run all six +target-native cells. Keep every current POSIX and Windows x64 control exactly reproducible; require +Windows arm64 to fail without upload after classifying all 2,887 ranges by raw section and known PE +metadata. Use only that evidence to choose or reject the narrowest copied-artifact build correction, +then rerun local gates and all six target-native jobs without weakening the comparator. 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 publication, desktop resolver, SSH transfer/ install, per-target Beta, fallback, tuple enablement, or default behavior may be connected by this From 6546f54d53446015f10681fffff9fe895e460232 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 13:57:00 -0400 Subject: [PATCH 036/267] build(ssh): make copied MSVC inputs deterministic Co-authored-by: Orca --- ...lay-node-pty-windows-build-determinism.mjs | 43 ++++++++++++++++--- ...ode-pty-windows-build-determinism.test.mjs | 15 ++++++- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs index 199375674d7..eefbd726877 100644 --- a/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs @@ -1,6 +1,30 @@ import { readFile, writeFile } from 'node:fs/promises' import { join } from 'node:path' +const COMPILER_OPTIONS = ` 'VCCLCompilerTool': { + 'AdditionalOptions': [ + '/guard:cf', + '/sdl', + '/W3', + '/w34244', + '/w34267', + '/ZH:SHA_256' + ] + }` + +const REPRODUCIBLE_COMPILER_OPTIONS = ` 'VCCLCompilerTool': { + 'AdditionalOptions': [ + '/guard:cf', + '/sdl', + '/W3', + '/w34244', + '/w34267', + '/ZH:SHA_256', + '/Brepro', + '/experimental:deterministic' + ] + }` + const LINKER_OPTIONS = ` 'VCLinkerTool': { 'AdditionalOptions': [ '/DYNAMICBASE', @@ -12,7 +36,8 @@ const REPRODUCIBLE_LINKER_OPTIONS = ` 'VCLinkerTool': { 'AdditionalOptions': [ '/DYNAMICBASE', '/guard:cf', - '/Brepro' + '/Brepro', + '/experimental:deterministic' ] }` @@ -22,10 +47,18 @@ export async function applyWindowsNodePtyBuildDeterminism({ nodePtyDirectory, tu } const bindingPath = join(nodePtyDirectory, 'binding.gyp') const source = await readFile(bindingPath, 'utf8') - if (source.split(LINKER_OPTIONS).length !== 2 || source.includes("'/Brepro'")) { - throw new Error('node-pty Windows linker settings do not match the reviewed source') + if ( + source.split(COMPILER_OPTIONS).length !== 2 || + source.split(LINKER_OPTIONS).length !== 2 || + source.includes("'/Brepro'") || + source.includes("'/experimental:deterministic'") + ) { + throw new Error('node-pty Windows build settings do not match the reviewed source') } - // Why: MSVC otherwise embeds per-build timestamps/identities in the copied artifact binaries. - await writeFile(bindingPath, source.replace(LINKER_OPTIONS, REPRODUCIBLE_LINKER_OPTIONS), 'utf8') + // Why: ARM64 compiler output changes even with a reproducible linker unless both stages opt in. + const reproducibleSource = source + .replace(COMPILER_OPTIONS, REPRODUCIBLE_COMPILER_OPTIONS) + .replace(LINKER_OPTIONS, REPRODUCIBLE_LINKER_OPTIONS) + await writeFile(bindingPath, reproducibleSource, 'utf8') return true } diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs index 24985dc889a..85ea7dff2ad 100644 --- a/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs @@ -19,13 +19,24 @@ async function copiedSource() { } describe('SSH relay Windows node-pty build determinism', () => { - it('adds deterministic MSVC linking only to the copied Windows source', async () => { + it('adds deterministic MSVC compilation and linking only to the copied Windows source', async () => { const directory = await copiedSource() + const repositorySource = await readFile(resolve('node_modules/node-pty/binding.gyp'), 'utf8') await expect( applyWindowsNodePtyBuildDeterminism({ nodePtyDirectory: directory, tuple: 'win32-arm64' }) ).resolves.toBe(true) const source = await readFile(join(directory, 'binding.gyp'), 'utf8') - expect(source.match(/'\/Brepro'/g)).toHaveLength(1) + const compilerStart = source.indexOf("'VCCLCompilerTool'") + const linkerStart = source.indexOf("'VCLinkerTool'") + const compilerOptions = source.slice(compilerStart, linkerStart) + const linkerOptions = source.slice(linkerStart) + expect(compilerOptions).toContain("'/Brepro'") + expect(compilerOptions).toContain("'/experimental:deterministic'") + expect(linkerOptions).toContain("'/Brepro'") + expect(linkerOptions).toContain("'/experimental:deterministic'") + expect(await readFile(resolve('node_modules/node-pty/binding.gyp'), 'utf8')).toBe( + repositorySource + ) }) it('leaves POSIX source untouched and rejects an unexpected Windows source shape', async () => { From a6b423f33f0f0f7b4e7975ed80d901f168563e61 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 13:58:15 -0400 Subject: [PATCH 037/267] docs(ssh): gate MSVC determinism correction Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 190 ++++++++++++++++-- 1 file changed, 176 insertions(+), 14 deletions(-) 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 c85ab900f18..cfd3fba0633 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29353432240 keeps Linux x64/arm64, macOS x64/arm64, and Windows x64 exactly reproducible and proves the bounded PE diagnostic natively on Windows arm64; the rejected arm64 files have identical 956,928-byte layouts but differ in 2,946 bytes across 2,887 ranges beginning in `.text`, plus `/Brepro` timestamp/CodeView identity fields; bounded full-scan section summaries are locally green at exact implementation commit `cd7f94136` and await the six target-native cells before any producer correction; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29354676731 keeps Linux x64/arm64, macOS x64/arm64, and Windows x64 exactly reproducible, while Windows arm64 fails closed with no upload after a complete bounded scan; 2,879 of 2,947 differing bytes are one-byte changes every 16 bytes in the first 46,049 bytes of `.text`, and the remaining 68 bytes are `/Brepro`-derived COFF/debug/CodeView data; exact implementation commit `6546f54d5` now applies `/Brepro` and `/experimental:deterministic` to compiler and linker inputs in the copied node-pty source only, is locally green, and awaits the six target-native cells; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -158,12 +158,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: push exact implementation commit `cd7f94136` plus its evidence-ledger head - and run all six target-native cells. Keep every current POSIX and Windows x64 control exactly - reproducible; require Windows arm64 to fail without upload after classifying all 2,887 ranges by - raw section and known PE metadata. Make no copied-artifact producer correction until that evidence - is recorded; keep the strict comparator, repository-wide node-pty patch, legacy/default path, and - all other release gates unchanged. +- Next required action: push exact implementation commit `6546f54d5` plus its evidence-ledger head + and run all six target-native cells. Require both Windows architectures to accept the compiler and + linker flags, all six outputs to compare exactly before upload, and no regression in native smoke; + keep the strict comparator, failed-binary no-upload boundary, repository-wide node-pty patch, + legacy/default path, and all other release gates unchanged. ## Non-Negotiable Invariants @@ -656,7 +655,12 @@ locally green under E-M3-WINDOWS-PE-DIAGNOSTIC-LOCAL-001. Exact-head run 2935343 control and real arm64 parser, but its 128 detailed ranges are exhausted by `.text`; the active diagnostic-only correction adds bounded full-scan section totals/samples before any producer change. Exact implementation commit `cd7f94136` is locally green under -E-M3-WINDOWS-PE-FULL-SCAN-LOCAL-001 and awaits native evidence. +E-M3-WINDOWS-PE-FULL-SCAN-LOCAL-001. Exact-head run 29354676731 then classifies the complete +arm64 drift under E-M3-WINDOWS-PE-FULL-SCAN-CI-RED-001: 2,879 one-byte `.text` differences at +16-byte intervals plus 68 `/Brepro`-derived metadata bytes, with five controls reproducible and +uploaded and no rejected arm64 upload. Exact implementation commit `6546f54d5` applies the native +toolchain's reproducible compiler and linker inputs only to the copied node-pty `binding.gyp`, is +locally green under E-M3-WINDOWS-COMPILER-DETERMINISM-LOCAL-001, and awaits native proof. Each runtime must contain only the executable closure required by the relay. @@ -4404,6 +4408,165 @@ or unexpected token`; the first logs did not identify a source location. five reproducible uploads plus a rejected Windows arm64 full-scan summary, and record that evidence before changing any copied-artifact producer input. +### E-M3-WINDOWS-PE-FULL-SCAN-CI-RED-001 — Complete native arm64 region classification + +- Date: 2026-07-14 +- Commit SHA / PR: exact head `a51093f009a03a1105e9f0b86be14797a8046414`, containing exact + diagnostic implementation `cd7f941365bf6e631cf0f9947f517ecef02afc8e`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Run and jobs: [run 29354676731](https://github.com/stablyai/orca/actions/runs/29354676731), + conclusion `failure`; Windows arm64 `87159223395`, Linux arm64 `87159223407`, macOS x64 + `87159223419`, macOS arm64 `87159223455`, Windows x64 `87159223488`, and Linux x64 + `87159223544` +- Runners: the six target-native labels recorded in prior reproducibility evidence. The diagnostic + cell was Windows 11 Enterprise 10.0.26200 arm64, image `windows-11-arm64` `20260706.102.1`, + runner `2.335.1`, provisioner `20260624.560`, Node v24.18.0, MSVC 19.44.35228 / tools + 14.44.35207, Windows SDK 10.0.26100.0, and Python 3.13.14. +- Remote and transport: none; target-native artifact assembly/execution, rejected-file diagnostics, + and unpublished Actions artifact upload only +- Exact evidence commands: + + ```sh + gh run view 29354676731 --repo stablyai/orca \ + --json headSha,status,conclusion,url,createdAt,updatedAt,jobs + gh api repos/stablyai/orca/actions/jobs/87159223395/logs + gh api 'repos/stablyai/orca/actions/runs/29354676731/artifacts?per_page=100' + ``` + +- Result: FAIL as the intended evidence-producing gate. All Windows contract suites passed before + execution; Linux x64/arm64, macOS x64/arm64, and Windows x64 built twice, inspected, smoked, + compared exactly, and uploaded. Windows arm64 built, inspected, and smoked both outputs, then the + strict comparator rejected `conpty_console_list.node`; the full-scan diagnostic completed before + the fatal throw and the upload step was skipped. +- Uploaded controls: `ssh-relay-runtime-linux-x64-glibc` artifact `8319697674` (29,282,973 bytes), + Linux arm64 `8319710845` (28,211,514), macOS x64 `8319759300` (26,374,729), macOS arm64 + `8319686475` (24,756,958), and Windows x64 `8319797990` (37,075,313). All are unpublished and + expire 2026-07-21; the run contains exactly five artifacts and no Windows arm64 artifact. +- Rejected arm64 outputs: content IDs + `75f048e2216c5a88b5dec886d7ce43f8c54768c42184835cfd0d9a22ea3f1e53` and + `dddc49d84bdafd5bef136be5050352555e99943b21f49781f1604bd9e37051bc`; ZIP sizes 33,261,549 + and 33,261,550 bytes with SHA-256 + `4c21eaba04e514dee246ee23ea6b4559369fc37eaf90dc0634f11146427bb45a` and + `aecde9c9982d972d194417cc2d076353fb808f39ba4f018b8d9e64cfdf5c3411`. +- PE diagnostic result: both modules are 956,928-byte machine `0xaa64` PE32+ files with identical + 12-section layout, data directories, imports, relocations, 840,192-byte code section, and linker + version 14.44. Their SHA-256 values are + `52d4909f106a4e8f85e7fc4c8cc0ccae93eaa74a83342b569d71f67619f15a1e` and + `6926210ec6208caa2a79655e2af544b74d8a0d6d1b8c7637d4ece017c89046de`. Exactly 2,947 bytes + differ across 2,886 ranges: + - `.text`: 2,879 one-byte ranges from file offsets 1,052 through 47,100, exactly 16 bytes apart; + all eight retained samples change `85` to `0e`. + - COFF header: one four-byte `/Brepro` timestamp range. + - Debug directory: three four-byte ranges carrying that timestamp. + - CodeView data: one 16-byte identifier range. + - `.rdata`: two ranges totaling 36 bytes, containing the derived CodeView/build identities and + timestamp. + + The CodeView path remains 92 bytes with identical SHA-256 + `401080e65f10d7483583537fc7394ddf416fa36febfd76e6d1c17c64bb36f3ea`; no other section + differs. The detailed list remains capped while every range contributes to the five bounded region + summaries. + +- Duration and resource metrics: jobs were 9m50s Windows arm64, 6m56s Windows x64, 5m33s macOS x64, + 3m44s Linux arm64, 3m18s Linux x64, and 2m55s macOS arm64. Arm64 builds took 152,517 ms and + 130,926 ms; smoke took 5,963 ms and 5,414 ms at 51,884,032 and 51,789,824 bytes RSS. The complete + smoke stages took 7,978 ms and 7,203 ms. Build peak RSS, open files/channels, and cancellation + settlement were not instrumented. +- Artifact/log/trace link: run/jobs and five unpublished seven-day artifacts above; rejected arm64 + bytes remained runner-local +- Oracle proved: the arm64 failure is copied-source producer nondeterminism concentrated in a + regular `.text` pattern, with only derived `/Brepro` identities elsewhere. It is not ZIP order, + tree metadata, comparator normalization, a changing PDB path, or a cross-platform regression. The + strict no-upload boundary remains effective. +- Does not prove: which compiler input causes the repeating `.text` byte, a safe producer + correction, arm64 equality, cross-run equality, oldest baselines, native trust, SSH, publication, + transfer, fallback, UI, or any enabled tuple. +- Checklist items satisfied: complete native PE region classification and five control cells only; + no tuple or production checkbox. +- Follow-up: evaluate a test-covered reproducible compiler input only in the copied node-pty source, + rerun all local gates and all six target-native cells, and require exact arm64 equality without + normalizing binaries or weakening the comparator. + +### E-M3-WINDOWS-COMPILER-DETERMINISM-LOCAL-001 — Copied-source MSVC reproducibility inputs + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `6546f54d53446015f10681fffff9fe895e460232`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741), native execution pending +- Runner: macOS 26.2 build 25C56, native Apple M4 arm64; Node v26.0.0 and pnpm 10.24.0. This + runner cannot execute MSVC, so target-native Windows jobs remain authoritative. +- Remote and transport: none; copied `binding.gyp` source-shape contracts only +- Exact red command: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs + ``` + +- Red result: FAIL as intended, one failed and one passed test. The copied compiler block lacked + `/Brepro`; no implementation change had yet been made. +- Exact green commands: + + ```sh + node --check config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs + node --check config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.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 \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-build.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs + pnpm run typecheck + pnpm exec oxlint config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check .github/workflows/ssh-relay-runtime-artifacts.yml \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md + git diff --check + ``` + +- Green result: PASS. Both syntax checks exited zero; the purpose command passed 6/6 tests across + three suites; all 15 artifact suites passed 51/51 tests. Typecheck, focused oxlint, the 355-entry + max-lines ratchet, full repository lint/reliability/localization, formatting, staged hooks, and + diff checks exited zero. Full lint emitted only pre-existing warnings outside this package. +- Duration and resource metrics: purpose suite 183 ms Vitest; complete artifact suite 1.27s Vitest + / 2.65s wall; typecheck 2.85s; focused oxlint 0.76s; max-lines 1.81s; full lint 10.91s; formatting + and diff check 1.10s. Synthetic peak RSS/open files were not instrumented. +- Artifact/log/trace link: exact source commit; no binary artifact was created or uploaded +- Oracle proved: Windows builds receive `/Brepro` and `/experimental:deterministic` in both the + compiler and linker option blocks after an exact reviewed-source match. POSIX tuples are untouched, + unexpected or already-modified source fails closed, and the installed repository + `node_modules/node-pty/binding.gyp` remains byte-identical because only the exclusive copied source + is rewritten. +- Does not prove: that MSVC 19.44 accepts the flags on both architectures, Windows arm64 equality, + native execution, cross-run equality, oldest baselines, native trust, SSH, publication, transfer, + fallback, UI, or any enabled tuple. +- Checklist items satisfied: copied-source compiler/linker input contract only; no tuple or + production checkbox. +- Follow-up: push the exact implementation and ledger head, run all six target-native cells, and + require exact equality and successful unpublished upload on both Windows architectures. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -4461,12 +4624,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push exact implementation commit `cd7f94136` plus its evidence-ledger head and run all six -target-native cells. Keep every current POSIX and Windows x64 control exactly reproducible; require -Windows arm64 to fail without upload after classifying all 2,887 ranges by raw section and known PE -metadata. Use only that evidence to choose or reject the narrowest copied-artifact build correction, -then rerun local gates and all six target-native jobs without weakening the comparator. Cross-family -Layer B targets, the protected +Push exact implementation commit `6546f54d5` plus its evidence-ledger head and run all six +target-native cells. Require both Windows architectures to accept the compiler and linker flags, +all six outputs to compare exactly before upload, and no regression in native smoke; keep the strict +comparator, failed-binary no-upload boundary, repository-wide node-pty patch, legacy/default path, +and all other release gates unchanged. 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 publication, desktop resolver, SSH transfer/ install, per-target Beta, fallback, tuple enablement, or default behavior may be connected by this From 0d3a0c9d3f0eee78d3106a646369cc3fc5db96b1 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 14:16:12 -0400 Subject: [PATCH 038/267] test(ssh): diagnose generated arm64 build inputs Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 16 +++ config/scripts/ssh-relay-node-pty-build.mjs | 14 +- ...lay-node-pty-windows-build-determinism.mjs | 59 ++++++++ ...ode-pty-windows-build-determinism.test.mjs | 60 +++++++- .../ssh-relay-runtime-workflow.test.mjs | 7 +- ...github-release-implementation-checklist.md | 131 ++++++++++++++---- 6 files changed, 254 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 394651d7686..6fe78588d0c 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -372,6 +372,22 @@ jobs: --first-pe $firstPe ` --second-pe $secondPe if ($LASTEXITCODE -ne 0) { Write-Output 'windows_pe_diagnostic=failed' } + if ('${{ matrix.tuple }}' -eq 'win32-arm64') { + $llvmObjdump = Get-Command llvm-objdump.exe -ErrorAction SilentlyContinue + if ($llvmObjdump) { + foreach ($candidate in @( + @{ Label = 'first'; Path = $firstPe }, + @{ Label = 'second'; Path = $secondPe } + )) { + "windows_arm64_disassembly=$($candidate.Label)" + # Why: the first 512 code bytes classify the repeated instruction without retaining rejected files. + & $llvmObjdump.Source --disassemble --start-address=0x180001000 --stop-address=0x180001200 $candidate.Path + if ($LASTEXITCODE -ne 0) { Write-Output 'windows_arm64_disassembly=failed' } + } + } else { + Write-Output 'windows_arm64_disassembly=tool-unavailable' + } + } } else { Write-Output 'windows_pe_diagnostic=skipped_missing_input' } diff --git a/config/scripts/ssh-relay-node-pty-build.mjs b/config/scripts/ssh-relay-node-pty-build.mjs index af277f207a7..6368b2af7d3 100644 --- a/config/scripts/ssh-relay-node-pty-build.mjs +++ b/config/scripts/ssh-relay-node-pty-build.mjs @@ -5,7 +5,10 @@ import { dirname, join, resolve } from 'node:path' import { promisify } from 'node:util' import { applyWindowsNodePtySettlement } from './ssh-relay-node-pty-windows-settlement.mjs' -import { applyWindowsNodePtyBuildDeterminism } from './ssh-relay-node-pty-windows-build-determinism.mjs' +import { + applyWindowsNodePtyBuildDeterminism, + assertWindowsNodePtyGeneratedBuildSettings +} from './ssh-relay-node-pty-windows-build-determinism.mjs' const require = createRequire(import.meta.url) const execFileAsync = promisify(execFile) @@ -184,6 +187,15 @@ export async function buildPatchedSshRelayNodePty({ env: buildEnvironment }) } + const generatedSettings = await assertWindowsNodePtyGeneratedBuildSettings({ + nodePtyDirectory: buildDirectory, + tuple + }) + if (generatedSettings) { + process.stdout.write( + `windows_node_pty_msbuild_settings=${JSON.stringify(generatedSettings)}\n` + ) + } } finally { clearTimeout(timeout) } diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs index eefbd726877..cf7b0be4d7e 100644 --- a/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs @@ -41,6 +41,48 @@ const REPRODUCIBLE_LINKER_OPTIONS = ` 'VCLinkerTool': { ] }` +const REQUIRED_GENERATED_OPTIONS = ['/Brepro', '/experimental:deterministic'] + +function decodeXmlText(value) { + return value + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('&', '&') +} + +function releaseDefinitionGroup(source, configuration) { + const groups = [ + ...source.matchAll(/]*)>([\s\S]*?)<\/ItemDefinitionGroup>/g) + ].filter((match) => decodeXmlText(match[1]).includes(`'Release|${configuration}'`)) + if (groups.length !== 1) { + throw new Error('generated MSBuild settings lack one exact Release configuration') + } + return groups[0][2] +} + +function requiredToolOptions(group, tool) { + const tools = [...group.matchAll(new RegExp(`<${tool}\\b[^>]*>([\\s\\S]*?)`, 'g'))] + const options = tools.flatMap((match) => [ + ...match[1].matchAll(/]*>([\s\S]*?)<\/AdditionalOptions>/g) + ]) + if (tools.length !== 1 || options.length !== 1) { + throw new Error(`generated MSBuild settings lack one exact ${tool} option block`) + } + const tokens = decodeXmlText(options[0][1]).trim().split(/\s+/) + if (!tokens.includes('%(AdditionalOptions)')) { + throw new Error(`generated MSBuild settings do not inherit ${tool} options`) + } + for (const required of REQUIRED_GENERATED_OPTIONS) { + const count = tokens.filter((token) => token.toLowerCase() === required.toLowerCase()).length + if (count !== 1) { + throw new Error(`generated MSBuild settings require exactly one ${tool} ${required}`) + } + } + return [...REQUIRED_GENERATED_OPTIONS] +} + export async function applyWindowsNodePtyBuildDeterminism({ nodePtyDirectory, tuple }) { if (!tuple.startsWith('win32-')) { return false @@ -62,3 +104,20 @@ export async function applyWindowsNodePtyBuildDeterminism({ nodePtyDirectory, tu await writeFile(bindingPath, reproducibleSource, 'utf8') return true } + +export async function assertWindowsNodePtyGeneratedBuildSettings({ nodePtyDirectory, tuple }) { + if (!tuple.startsWith('win32-')) { + return undefined + } + const project = 'conpty_console_list.vcxproj' + const configuration = `Release|${tuple.endsWith('-arm64') ? 'ARM64' : 'x64'}` + const source = await readFile(join(nodePtyDirectory, 'build', project), 'utf8') + const group = releaseDefinitionGroup(source, configuration.split('|')[1]) + // Why: the source rewrite is insufficient proof unless gyp propagates both stages into MSBuild. + return { + configuration, + compilerOptions: requiredToolOptions(group, 'ClCompile'), + linkerOptions: requiredToolOptions(group, 'Link'), + project + } +} diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs index 85ea7dff2ad..7b14811ce61 100644 --- a/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs @@ -1,10 +1,13 @@ -import { cp, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' -import { applyWindowsNodePtyBuildDeterminism } from './ssh-relay-node-pty-windows-build-determinism.mjs' +import { + applyWindowsNodePtyBuildDeterminism, + assertWindowsNodePtyGeneratedBuildSettings +} from './ssh-relay-node-pty-windows-build-determinism.mjs' const temporaryDirectories = [] afterEach(async () => { @@ -18,6 +21,21 @@ async function copiedSource() { return directory } +async function generatedProject({ + compilerOptions = '/Brepro /experimental:deterministic %(AdditionalOptions)', + linkerOptions = '/Brepro /experimental:deterministic %(AdditionalOptions)', + platform = 'ARM64' +} = {}) { + const directory = await mkdtemp(join(tmpdir(), 'orca-node-pty-windows-msbuild-')) + temporaryDirectories.push(directory) + await mkdir(join(directory, 'build')) + await writeFile( + join(directory, 'build', 'conpty_console_list.vcxproj'), + `${compilerOptions}${linkerOptions}` + ) + return directory +} + describe('SSH relay Windows node-pty build determinism', () => { it('adds deterministic MSVC compilation and linking only to the copied Windows source', async () => { const directory = await copiedSource() @@ -52,4 +70,42 @@ describe('SSH relay Windows node-pty build determinism', () => { applyWindowsNodePtyBuildDeterminism({ nodePtyDirectory: directory, tuple: 'win32-x64' }) ).rejects.toThrow('do not match the reviewed source') }) + + it('proves the generated Release project propagates compile and link settings', async () => { + const directory = await generatedProject() + await expect( + assertWindowsNodePtyGeneratedBuildSettings({ + nodePtyDirectory: directory, + tuple: 'win32-arm64' + }) + ).resolves.toEqual({ + configuration: 'Release|ARM64', + compilerOptions: ['/Brepro', '/experimental:deterministic'], + linkerOptions: ['/Brepro', '/experimental:deterministic'], + project: 'conpty_console_list.vcxproj' + }) + }) + + it('rejects missing, duplicate, non-inherited, or wrong-architecture generated settings', async () => { + for (const fixture of [ + { compilerOptions: '/Brepro %(AdditionalOptions)' }, + { linkerOptions: '/Brepro /Brepro /experimental:deterministic %(AdditionalOptions)' }, + { linkerOptions: '/Brepro /experimental:deterministic' }, + { platform: 'x64' } + ]) { + const directory = await generatedProject(fixture) + await expect( + assertWindowsNodePtyGeneratedBuildSettings({ + nodePtyDirectory: directory, + tuple: 'win32-arm64' + }) + ).rejects.toThrow(/generated MSBuild settings/i) + } + await expect( + assertWindowsNodePtyGeneratedBuildSettings({ + nodePtyDirectory: '/not-used-on-posix', + tuple: 'darwin-arm64' + }) + ).resolves.toBeUndefined() + }) }) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index cb6407a90ca..553a01217e1 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -63,6 +63,8 @@ describe('SSH relay runtime artifact workflow', () => { expect(source).toContain('ssh-relay-runtime-reproducibility.test.mjs') expect(source).toContain('ssh-relay-runtime-windows-pe-diagnostic.mjs') expect(source).toContain('ssh-relay-runtime-windows-pe-diagnostic.test.mjs') + expect(source).toContain('llvm-objdump.exe') + expect(source).toContain('--start-address=0x180001000 --stop-address=0x180001200') expect(source).toContain( 'node --check config/scripts/ssh-relay-runtime-reproducibility.test.mjs' ) @@ -108,9 +110,12 @@ describe('SSH relay runtime artifact workflow', () => { expect(windowsRun.indexOf('ssh-relay-runtime-windows-pe-diagnostic.mjs')).toBeGreaterThan( windowsRun.indexOf('ssh-relay-runtime-reproducibility.mjs') ) + expect(windowsRun.indexOf('llvm-objdump.exe')).toBeGreaterThan( + windowsRun.indexOf('ssh-relay-runtime-windows-pe-diagnostic.mjs') + ) expect( windowsRun.indexOf("throw 'runtime reproducibility verification failed'") - ).toBeGreaterThan(windowsRun.indexOf('ssh-relay-runtime-windows-pe-diagnostic.mjs')) + ).toBeGreaterThan(windowsRun.indexOf('llvm-objdump.exe')) expect(windowsRun.indexOf('runtime-evidence/${{ matrix.tuple }}')).toBeGreaterThan( windowsRun.indexOf("throw 'runtime reproducibility verification failed'") ) 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 cfd3fba0633..35f136c55d7 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29354676731 keeps Linux x64/arm64, macOS x64/arm64, and Windows x64 exactly reproducible, while Windows arm64 fails closed with no upload after a complete bounded scan; 2,879 of 2,947 differing bytes are one-byte changes every 16 bytes in the first 46,049 bytes of `.text`, and the remaining 68 bytes are `/Brepro`-derived COFF/debug/CodeView data; exact implementation commit `6546f54d5` now applies `/Brepro` and `/experimental:deterministic` to compiler and linker inputs in the copied node-pty source only, is locally green, and awaits the six target-native cells; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29355973362 keeps Linux x64/arm64, macOS x64/arm64, and Windows x64 exactly reproducible, while Windows arm64 again fails closed with no upload after both complete runtimes execute successfully; adding `/Brepro` and `/experimental:deterministic` to the copied compiler and linker settings did not change the 2,947-byte drift at all, so the next bounded package must prove the generated MSBuild inputs and classify the repeated ARM64 instructions before another producer correction; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -158,11 +158,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: push exact implementation commit `6546f54d5` plus its evidence-ledger head - and run all six target-native cells. Require both Windows architectures to accept the compiler and - linker flags, all six outputs to compare exactly before upload, and no regression in native smoke; - keep the strict comparator, failed-binary no-upload boundary, repository-wide node-pty patch, - legacy/default path, and all other release gates unchanged. +- Next required action: add a bounded diagnostic that fail-closed inspects the generated + `conpty_console_list.vcxproj` for the expected compiler/linker settings and reports paired ARM64 + instruction/disassembly context around the first differing ranges. Require both native Windows + cells while retaining all four POSIX controls, strict comparison, rejected-binary no-upload, + repository-wide node-pty patch, legacy/default path, and all other release gates unchanged. ## Non-Negotiable Invariants @@ -659,8 +659,12 @@ E-M3-WINDOWS-PE-FULL-SCAN-LOCAL-001. Exact-head run 29354676731 then classifies arm64 drift under E-M3-WINDOWS-PE-FULL-SCAN-CI-RED-001: 2,879 one-byte `.text` differences at 16-byte intervals plus 68 `/Brepro`-derived metadata bytes, with five controls reproducible and uploaded and no rejected arm64 upload. Exact implementation commit `6546f54d5` applies the native -toolchain's reproducible compiler and linker inputs only to the copied node-pty `binding.gyp`, is -locally green under E-M3-WINDOWS-COMPILER-DETERMINISM-LOCAL-001, and awaits native proof. +toolchain's reproducible compiler and linker settings only to the copied node-pty `binding.gyp` and +is locally green under E-M3-WINDOWS-COMPILER-DETERMINISM-LOCAL-001. Exact-head run 29355973362 then +kept five controls reproducible but reproduced the identical Windows arm64 PE drift under +E-M3-WINDOWS-COMPILER-DETERMINISM-CI-RED-001. The next diagnostic must prove how those settings +reach the generated `conpty_console_list.vcxproj` and report bounded ARM64 instruction/disassembly +context; no second producer correction is justified before that evidence. Each runtime must contain only the executable closure required by the relay. @@ -4567,23 +4571,93 @@ or unexpected token`; the first logs did not identify a source location. - Follow-up: push the exact implementation and ledger head, run all six target-native cells, and require exact equality and successful unpublished upload on both Windows architectures. +### E-M3-WINDOWS-COMPILER-DETERMINISM-CI-RED-001 — Compiler-setting correction does not remove arm64 drift + +- Date: 2026-07-14 +- Commit SHA / PR: exact head `a6b423f33f0f0f7b4e7975ed80d901f168563e61`, containing exact + implementation commit `6546f54d53446015f10681fffff9fe895e460232`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Run and jobs: [run 29355973362](https://github.com/stablyai/orca/actions/runs/29355973362), + conclusion `failure`; Linux arm64 `87163591039`, Windows x64 `87163591059`, macOS x64 + `87163591076`, Windows arm64 `87163591141`, macOS arm64 `87163591145`, and Linux x64 + `87163591207` +- Runners: the six target-native labels recorded in prior reproducibility evidence. The failing cell + was GitHub-hosted `windows-11-arm`, resolved image `win11-arm64` `20260706.102.1`, native + `ARM64`, Node v24.18.0, MSVC 19.44.35228 / tools 14.44.35207, Windows SDK 10.0.26100.0, and + Python 3.13.14. +- Remote and transport: none; target-native artifact assembly/execution, rejected-file diagnostics, + and unpublished Actions artifact upload only +- Exact evidence commands: + + ```sh + gh run view 29355973362 --repo stablyai/orca \ + --json headSha,status,conclusion,url,createdAt,updatedAt,jobs + gh run view 29355973362 --repo stablyai/orca --job 87163591141 --log-failed + gh api repos/stablyai/orca/actions/runs/29355973362/artifacts --paginate + ``` + +- Result: FAIL as the intended strict evidence gate. All 15 Windows contract suites passed with + 48 passing and three intentionally skipped tests. Linux x64/arm64, macOS x64/arm64, and Windows + x64 built twice, inspected, executed, compared exactly, and uploaded. Windows arm64 built and + executed both complete outputs, then the comparator rejected + `runtime/node_modules/node-pty/build/Release/conpty_console_list.node`; the diagnostic ran, the + job failed, and no Windows arm64 artifact was uploaded. +- Uploaded controls: Linux x64 artifact `8320207112` (29,282,626 bytes), Linux arm64 + `8320217472` (28,211,075), macOS x64 `8320277866` (26,390,709), macOS arm64 `8320191810` + (24,741,051), and Windows x64 `8320261436` (37,075,310). All are unpublished seven-day + artifacts; the run contains exactly five artifacts. +- Rejected arm64 outputs: content IDs + `5523ec111f63a53edb3da090cf90bedb0421c2c9862dc0356e106fdf5a6519f2` and + `faadd0f5285a449fd0aad0960918edc72d565dc150171e8e292c160931340235`; ZIP sizes + 33,261,550 and 33,261,546 bytes with SHA-256 + `769fc2b8a4761b8d19b9d4b02adc2add6b4232ed73a1ee154e6e073760f52eda` and + `217e9f8b500837130e0978a7aa03dba2790a926b42410752c7d804e6d5a38143`. +- PE diagnostic result: both rejected modules remain 956,928-byte machine `0xaa64` PE32+ files + with the identical 12-section layout. Their SHA-256 values are + `548ff529facca76eb3a30c79f96e2170336b7cf01a3e55aabc2a5cd615cd1111` and + `e0039271e09a898d3edf9eb4506f3d895cd3282a893a1e15acb4abec2f9cb69c`. Exactly the + same 2,947 bytes differ across 2,886 ranges: 2,879 one-byte `.text` ranges every 16 bytes from + offsets 1,052 through 47,100, plus 68 bytes of derived COFF/debug/CodeView identity. The retained + `.text` samples all change `45` to `2d`; the 92-byte PDB path hash remains identical. +- Duration and resource metrics: jobs were 8m25s Windows arm64, 5m50s macOS x64, 5m15s Windows + x64, 3m37s Linux arm64, 3m16s Linux x64, and 2m41s macOS arm64. Arm64 builds took 141,484 ms + and 116,582 ms; smoke took 5,784 ms and 5,439 ms at 53,014,528 and 53,047,296 bytes RSS. The + complete smoke stages took 7,448 ms and 7,383 ms. Build peak RSS, open files/channels, and + cancellation settlement were not instrumented. +- Artifact/log/trace link: run/jobs and the five unpublished artifacts above; rejected arm64 bytes + remained runner-local +- Oracle proved: adding the reviewed compiler/linker settings did not alter the arm64 failure + signature, while all five controls and both arm64 runtime executions remain healthy. The strict + comparator and no-upload boundary still work. This falsifies the hypothesis that the copied + settings alone are sufficient. +- Does not prove: whether the generated MSBuild project actually propagates both settings to every + `conpty_console_list` compile/link invocation, what the repeated ARM64 instruction represents, a + safe producer correction, arm64 or cross-run equality, oldest baselines, native trust, SSH, + publication, transfer, fallback, UI, or any enabled tuple. +- Checklist items satisfied: native negative evidence and five reproducible control cells only; no + tuple or production checkbox. +- Follow-up: inspect the generated `conpty_console_list.vcxproj` fail-closed for the expected + compiler/linker inputs and emit bounded paired ARM64 instruction/disassembly context around the + first differing ranges. Do not change the producer, normalize binaries, weaken comparison, or + upload rejected bytes before that evidence. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit owner and promotion condition. -| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | -| ------------------------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------ | -| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | -| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | -| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | -| Native clean-rebuild identity unproved | Five native cells pass; Windows arm64 `conpty_console_list.node` drifts | Toolchain drift may change native content IDs | Codex implementation owner | All six same-head, same-runner clean builds match without weakening the oracle | Open | -| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | -| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | -| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | -| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | -| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | -| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | +| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | +| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | +| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | +| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | +| Native clean-rebuild identity unproved | Five native cells pass; copied MSVC settings do not remove Windows arm64 `conpty_console_list.node` drift | Toolchain drift may change native content IDs | Codex implementation owner | Generated-build-input and ARM64 instruction diagnostics identify a safe producer correction; all six clean builds then match without weakening the oracle | Open | +| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | +| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | +| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | +| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | +| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | +| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | ## Final Definition of Done @@ -4624,12 +4698,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push exact implementation commit `6546f54d5` plus its evidence-ledger head and run all six -target-native cells. Require both Windows architectures to accept the compiler and linker flags, -all six outputs to compare exactly before upload, and no regression in native smoke; keep the strict -comparator, failed-binary no-upload boundary, repository-wide node-pty patch, legacy/default path, -and all other release gates unchanged. 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 publication, desktop resolver, SSH transfer/ -install, per-target Beta, fallback, tuple enablement, or default behavior may be connected by this -package. +Add a bounded diagnostic that fail-closed inspects generated `conpty_console_list.vcxproj` +compiler/linker settings and reports paired ARM64 instruction/disassembly context around the first +differing ranges. Run both native Windows cells with all four POSIX controls, require the x64 and +POSIX outputs to remain exact, and retain the fatal comparator plus rejected-arm64 no-upload +boundary. 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 publication, desktop resolver, SSH transfer/install, per-target Beta, +fallback, tuple enablement, or default behavior may be connected by this package. From 65d44d72f7a5da382ffddc194f302b4386b2391c Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 14:18:47 -0400 Subject: [PATCH 039/267] docs(ssh): gate arm64 build-input diagnostic Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 124 +++++++++++++++--- 1 file changed, 107 insertions(+), 17 deletions(-) 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 35f136c55d7..c27b482dd08 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29355973362 keeps Linux x64/arm64, macOS x64/arm64, and Windows x64 exactly reproducible, while Windows arm64 again fails closed with no upload after both complete runtimes execute successfully; adding `/Brepro` and `/experimental:deterministic` to the copied compiler and linker settings did not change the 2,947-byte drift at all, so the next bounded package must prove the generated MSBuild inputs and classify the repeated ARM64 instructions before another producer correction; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29355973362 keeps Linux x64/arm64, macOS x64/arm64, and Windows x64 exactly reproducible, while Windows arm64 again fails closed with no upload after both complete runtimes execute successfully; adding `/Brepro` and `/experimental:deterministic` to the copied compiler and linker settings did not change the 2,947-byte drift at all; exact implementation commit `0d3a0c9d3` now fail-closed verifies the generated Release MSBuild inputs and adds bounded paired ARM64 disassembly after a mismatch, is locally green, and awaits the six target-native cells; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -158,11 +158,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: add a bounded diagnostic that fail-closed inspects the generated - `conpty_console_list.vcxproj` for the expected compiler/linker settings and reports paired ARM64 - instruction/disassembly context around the first differing ranges. Require both native Windows - cells while retaining all four POSIX controls, strict comparison, rejected-binary no-upload, - repository-wide node-pty patch, legacy/default path, and all other release gates unchanged. +- Next required action: push exact diagnostic implementation commit `0d3a0c9d3` plus its + evidence-ledger head and run all six target-native cells. Require both Windows jobs to prove the + generated Release compiler/linker settings; if arm64 still differs, record both bounded + disassemblies while retaining all four POSIX controls, strict comparison, rejected-binary + no-upload, repository-wide node-pty patch, legacy/default path, and all other release gates. ## Non-Negotiable Invariants @@ -662,9 +662,10 @@ uploaded and no rejected arm64 upload. Exact implementation commit `6546f54d5` a toolchain's reproducible compiler and linker settings only to the copied node-pty `binding.gyp` and is locally green under E-M3-WINDOWS-COMPILER-DETERMINISM-LOCAL-001. Exact-head run 29355973362 then kept five controls reproducible but reproduced the identical Windows arm64 PE drift under -E-M3-WINDOWS-COMPILER-DETERMINISM-CI-RED-001. The next diagnostic must prove how those settings -reach the generated `conpty_console_list.vcxproj` and report bounded ARM64 instruction/disassembly -context; no second producer correction is justified before that evidence. +E-M3-WINDOWS-COMPILER-DETERMINISM-CI-RED-001. Exact implementation commit `0d3a0c9d3` now +fail-closed verifies how those settings reach the generated `conpty_console_list.vcxproj` and adds +bounded paired ARM64 disassembly after a mismatch under E-M3-WINDOWS-MSBUILD-DISASSEMBLY-LOCAL-001; +no second producer correction is justified before the target-native evidence. Each runtime must contain only the executable closure required by the relay. @@ -4641,6 +4642,95 @@ or unexpected token`; the first logs did not identify a source location. first differing ranges. Do not change the producer, normalize binaries, weaken comparison, or upload rejected bytes before that evidence. +### E-M3-WINDOWS-MSBUILD-DISASSEMBLY-LOCAL-001 — Generated-project and bounded disassembly contracts + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `0d3a0c9d3f0eee78d3106a646369cc3fc5db96b1`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741), native execution pending +- Runner: macOS 26.2 build 25C56, native Apple M4 arm64; Node v26.0.0 and pnpm 10.24.0. This + runner cannot generate or execute native MSBuild/ARM64 PE inputs, so the target-native Windows + jobs remain authoritative. +- Remote and transport: none; synthetic generated-project fixtures and workflow-source contracts + only +- Exact red command: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs + ``` + +- Red result: FAIL as intended, two failed and two passed tests because no generated-project + verifier existed. +- Exact green commands: + + ```sh + node --check config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs + node --check config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs + node --check config/scripts/ssh-relay-node-pty-build.mjs + node --check config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.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 \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-build.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs + pnpm run typecheck + pnpm exec oxlint config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check .github/workflows/ssh-relay-runtime-artifacts.yml \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md + pnpm exec lint-staged + git diff --check + ``` + +- Green result: PASS. All syntax checks exited zero; the purpose command passed 8/8 tests across + three suites; all 15 artifact suites passed 53/53 tests. Typecheck, focused oxlint, the + 355-entry max-lines ratchet, full repository lint/reliability/localization, formatting, staged + hooks, and diff checks exited zero. Full lint emitted only pre-existing warnings outside this + package. +- Duration and resource metrics: purpose suite 156 ms Vitest; complete artifact suite 1.11s + Vitest. Static-gate wall time, synthetic peak RSS, open files/channels, and cancellation + settlement were not instrumented. +- Artifact/log/trace link: exact source commit and local command output; no binary artifact was + created or uploaded +- Oracle proved: Windows artifact builds now reject a missing, duplicate, non-inherited, or + wrong-architecture Release `ClCompile`/`Link` option in the generated + `conpty_console_list.vcxproj` before runtime staging, and log the exact accepted setting summary. + On an arm64 mismatch the workflow retains the existing structured PE diagnostic, then requests + only absolute image addresses `0x180001000` through `0x180001200` from native + `llvm-objdump.exe` before the fatal throw. Rejected bytes still cannot reach artifact staging. +- Does not prove: the real generated XML shape, native `llvm-objdump` availability/output, which + repeated ARM64 instruction differs, a safe producer correction, arm64 equality, oldest + baselines, native trust, SSH, publication, transfer, fallback, UI, or any enabled tuple. +- Checklist items satisfied: local generated-project and bounded-disassembly contracts only; no + tuple or production checkbox. +- Follow-up: push the exact implementation and ledger head, run all six target-native cells, and + require both Windows jobs to prove generated settings. If arm64 still differs, record the two + bounded disassemblies and retain strict failure/no-upload before any producer correction. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -4698,11 +4788,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Add a bounded diagnostic that fail-closed inspects generated `conpty_console_list.vcxproj` -compiler/linker settings and reports paired ARM64 instruction/disassembly context around the first -differing ranges. Run both native Windows cells with all four POSIX controls, require the x64 and -POSIX outputs to remain exact, and retain the fatal comparator plus rejected-arm64 no-upload -boundary. 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 publication, desktop resolver, SSH transfer/install, per-target Beta, -fallback, tuple enablement, or default behavior may be connected by this package. +Push exact diagnostic implementation commit `0d3a0c9d3` plus its evidence-ledger head and run all +six target-native cells. Require both Windows jobs to prove the generated Release compiler/linker +settings; if arm64 still differs, record both bounded disassemblies while retaining all four POSIX +controls, strict comparison, and rejected-arm64 no-upload. 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 publication, desktop resolver, SSH +transfer/install, per-target Beta, fallback, tuple enablement, or default behavior may be connected +by this package. From 4207c207ab932ef67522b4f73b98d63fa32d0924 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 14:30:15 -0400 Subject: [PATCH 040/267] fix(ssh): accept native arm64 MSBuild casing Co-authored-by: Orca --- ...lay-node-pty-windows-build-determinism.mjs | 6 +- ...ode-pty-windows-build-determinism.test.mjs | 2 +- ...github-release-implementation-checklist.md | 127 ++++++++++++++---- 3 files changed, 110 insertions(+), 25 deletions(-) diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs index cf7b0be4d7e..a92bc3b08a1 100644 --- a/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs @@ -53,9 +53,13 @@ function decodeXmlText(value) { } function releaseDefinitionGroup(source, configuration) { + const expectedCondition = `'Release|${configuration}'`.toLowerCase() const groups = [ ...source.matchAll(/]*)>([\s\S]*?)<\/ItemDefinitionGroup>/g) - ].filter((match) => decodeXmlText(match[1]).includes(`'Release|${configuration}'`)) + ].filter((match) => { + // Why: native arm64 MSBuild uses lowercase `arm64`; platform identities are case-insensitive. + return decodeXmlText(match[1]).toLowerCase().includes(expectedCondition) + }) if (groups.length !== 1) { throw new Error('generated MSBuild settings lack one exact Release configuration') } diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs index 7b14811ce61..29444fde8e2 100644 --- a/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs @@ -72,7 +72,7 @@ describe('SSH relay Windows node-pty build determinism', () => { }) it('proves the generated Release project propagates compile and link settings', async () => { - const directory = await generatedProject() + const directory = await generatedProject({ platform: 'arm64' }) await expect( assertWindowsNodePtyGeneratedBuildSettings({ nodePtyDirectory: directory, 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 c27b482dd08..aa48e88f3bf 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29355973362 keeps Linux x64/arm64, macOS x64/arm64, and Windows x64 exactly reproducible, while Windows arm64 again fails closed with no upload after both complete runtimes execute successfully; adding `/Brepro` and `/experimental:deterministic` to the copied compiler and linker settings did not change the 2,947-byte drift at all; exact implementation commit `0d3a0c9d3` now fail-closed verifies the generated Release MSBuild inputs and adds bounded paired ARM64 disassembly after a mismatch, is locally green, and awaits the six target-native cells; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29357355064 keeps Linux x64/arm64, macOS x64/arm64, and Windows x64 exactly reproducible and uploads exactly those five unpublished artifacts; Windows x64 proves the generated compiler/linker flags in both clean builds, while native Windows arm64 fails closed before staging because the generated Release-condition verifier does not accept the runner's lowercase `arm64` platform spelling; no arm64 artifact is uploaded and no disassembly or producer correction is yet justified; the active bounded package makes only that architecture comparison case-insensitive while retaining exact Release/configuration cardinality and option checks; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -118,7 +118,12 @@ same change as the work it records. keeps x64 and all POSIX controls green, then reports the real arm64 PE hashes/layout/ranges and fails without upload under E-M3-WINDOWS-PE-DIAGNOSTIC-CI-RED-001. Because the 128 detailed-range cap is exhausted by `.text`, the active diagnostic-only correction adds raw-section labels and a - bounded summary across all 2,887 ranges before any producer change. + bounded summary across all 2,887 ranges before any producer change. Exact-head run 29357355064 + keeps all five controls reproducible and proves both generated x64 option blocks, but the native + arm64 build fails closed at the new Release-condition verifier before runtime staging under + E-M3-WINDOWS-MSBUILD-PLATFORM-CASE-CI-RED-001. The runner exposes `Platform=arm64`; the active + correction accepts only a case-insensitive exact match for the expected architecture and retains + every option/cardinality check. No producer, comparator, upload, or production behavior changes. - Windows input correction: E-M3-WINDOWS-INPUT-GAP-001 proved the official Windows ZIP lacks headers and `node.lib`. Both artifacts now require the exact signed headers archive and tuple import library as explicit inputs. The schema, signed-checksum verifier, bounded ZIP/header extraction, @@ -158,11 +163,12 @@ 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: push exact diagnostic implementation commit `0d3a0c9d3` plus its - evidence-ledger head and run all six target-native cells. Require both Windows jobs to prove the - generated Release compiler/linker settings; if arm64 still differs, record both bounded - disassemblies while retaining all four POSIX controls, strict comparison, rejected-binary - no-upload, repository-wide node-pty patch, legacy/default path, and all other release gates. +- Next required action: test and implement the bounded case-insensitive architecture comparison for + generated MSBuild Release conditions, retain exact group and option cardinality, then rerun all + six target-native cells. Require both Windows jobs to prove their generated compiler/linker + settings; only if arm64 reaches the known comparator mismatch may paired bounded disassembly + justify a producer correction. Preserve all five controls, rejected-binary no-upload, the + repository-wide node-pty patch, legacy/default path, and every other release gate. ## Non-Negotiable Invariants @@ -665,7 +671,16 @@ kept five controls reproducible but reproduced the identical Windows arm64 PE dr E-M3-WINDOWS-COMPILER-DETERMINISM-CI-RED-001. Exact implementation commit `0d3a0c9d3` now fail-closed verifies how those settings reach the generated `conpty_console_list.vcxproj` and adds bounded paired ARM64 disassembly after a mismatch under E-M3-WINDOWS-MSBUILD-DISASSEMBLY-LOCAL-001; -no second producer correction is justified before the target-native evidence. +no second producer correction is justified before the target-native evidence. Exact-head run +29357355064 proves both generated option blocks twice on Windows x64 but fails closed on native +Windows arm64 before staging because the Release group lookup does not accept the runner's lowercase +`arm64` platform spelling (E-M3-WINDOWS-MSBUILD-PLATFORM-CASE-CI-RED-001). + +**In progress — 2026-07-14, Codex implementation owner:** make only the expected generated Release +architecture comparison case-insensitive, while retaining one exact Release group, the expected +tuple architecture, one inherited compiler/linker option block, and exactly one instance of each +required flag. Add the lowercase-arm64 purpose fixture before rerunning all six native cells. Do not +change a producer, normalize output, weaken reproducibility, or connect a production consumer. Each runtime must contain only the executable closure required by the relay. @@ -4731,23 +4746,88 @@ or unexpected token`; the first logs did not identify a source location. require both Windows jobs to prove generated settings. If arm64 still differs, record the two bounded disassemblies and retain strict failure/no-upload before any producer correction. +### E-M3-WINDOWS-MSBUILD-PLATFORM-CASE-CI-RED-001 — Native arm64 Release-group lookup fails closed + +- Date: 2026-07-14 +- Commit SHA / PR: exact head `65d44d72f7a5da382ffddc194f302b4386b2391c`, containing exact + diagnostic implementation commit `0d3a0c9d3f0eee78d3106a646369cc3fc5db96b1`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Run and jobs: [run 29357355064](https://github.com/stablyai/orca/actions/runs/29357355064), + conclusion `failure`; Linux x64 `87168238852`, Windows x64 `87168238867`, macOS x64 + `87168238889`, Windows arm64 `87168238935`, macOS arm64 `87168238945`, and Linux arm64 + `87168238954` +- Runners: the six target-native labels recorded in prior reproducibility evidence. The failing cell + was GitHub-hosted `windows-11-arm`, image `win11-arm64` release `20260706.102`, native `ARM64`, + runner 2.335.1, Node v24.18.0, MSVC tools 14.44.35207, Windows SDK 10.0.26100.0, and an arm64 + developer environment exposing `Platform=arm64`. +- Remote and transport: none; target-native artifact assembly, generated-project verification, and + unpublished Actions artifact upload only +- Exact evidence commands: + + ```sh + gh run view 29357355064 --repo stablyai/orca \ + --json headSha,status,conclusion,url,createdAt,updatedAt,jobs + gh run view 29357355064 --repo stablyai/orca --job 87168238867 --log + gh run view 29357355064 --repo stablyai/orca --job 87168238935 --log-failed + gh api repos/stablyai/orca/actions/runs/29357355064/artifacts --paginate + ``` + +- Result: FAIL as the intended strict evidence gate. Linux x64/arm64, macOS x64/arm64, and Windows + x64 built twice, inspected, executed, compared exactly, and uploaded. Windows x64 logged the + required `/Brepro` and `/experimental:deterministic` compiler and linker settings for both clean + builds. Native Windows arm64 completed compilation of the first copied node-pty tree, then the + generated-project verifier rejected it with `generated MSBuild settings lack one exact Release +configuration` before runtime staging, verification, smoke, comparison, or diagnostics. Its + artifact upload was skipped. +- Uploaded controls: Linux x64 artifact `8320756625` (29,278,781 bytes, + `sha256:50bab609edc8e4eb776f462622c5e1310e926049e5ec0e759692ea4ae74142a4`), Linux arm64 + `8320758083` (28,215,186, + `sha256:0b2770363a95717b0193c58ee066242ab08a9fa80eb0060049e912f6314776a8`), macOS x64 + `8320845963` (26,407,443, + `sha256:70b4217dc54ff2015638e7407714208f7e399acadf0cbcd0d8d5340025235d34`), macOS arm64 + `8320753136` (24,774,283, + `sha256:8f5c1a65e88d79b036cabf24d80656e6b987335c5d0a908d4fff241026d74467`), and Windows x64 + `8320806874` (37,075,312, + `sha256:a325d8e09c0391ee971328da397ac23412024ce2e4bf0dc633f897f8b923add2`). The run contains + exactly five unpublished seven-day artifacts and no Windows arm64 artifact. +- Duration and resource metrics: jobs were 6m43s macOS x64, 6m24s Windows arm64, 5m15s Windows + x64, 3m24s Linux x64, 3m23s Linux arm64, and 3m16s macOS arm64. The arm64 verifier failure + occurred 2m24s into the build/inspect step before runtime smoke, so no new arm64 smoke RSS, + channel/file count, cancellation, or fallback metric exists for this run. +- Artifact/log/trace link: run/jobs and the five unpublished artifacts above; no rejected arm64 + runtime or binary was staged or uploaded +- Oracle proved: the generated-setting verifier executes after native compilation and fails closed + before staging when its Release-group architecture spelling does not match. Windows x64 proves + both required settings in both generated projects. The native arm64 developer environment uses a + lowercase platform spelling, while all five architecture controls and the no-upload boundary + remain intact. +- Does not prove: the exact native arm64 generated XML condition, that a case-insensitive exact + architecture match reaches its option blocks, a complete arm64 runtime, paired disassembly, a + safe producer correction, arm64 equality, oldest baselines, native trust, SSH, publication, + transfer, fallback, UI, or any enabled tuple. +- Checklist items satisfied: native negative verifier evidence and five reproducible control cells + only; no tuple or production checkbox. +- Follow-up: cover lowercase `arm64` with a purpose fixture and compare only the expected Release + architecture case-insensitively, retaining exact group and option cardinality. Rerun both Windows + cells and all four POSIX controls before interpreting the underlying PE drift. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit owner and promotion condition. -| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | -| ------------------------------------------ | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | -| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | -| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | -| Native clean-rebuild identity unproved | Five native cells pass; copied MSVC settings do not remove Windows arm64 `conpty_console_list.node` drift | Toolchain drift may change native content IDs | Codex implementation owner | Generated-build-input and ARM64 instruction diagnostics identify a safe producer correction; all six clean builds then match without weakening the oracle | Open | -| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | -| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | -| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | -| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | -| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | -| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | +| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | +| ------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | +| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | +| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | +| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | +| Native clean-rebuild identity unproved | Five native cells pass; arm64 generated-setting verification currently stops before the known PE drift | Toolchain drift may change native content IDs | Codex implementation owner | Generated-build-input and ARM64 instruction diagnostics identify a safe producer correction; all six clean builds then match without weakening the oracle | Open | +| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | +| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | +| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | +| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | +| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | +| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | ## Final Definition of Done @@ -4788,9 +4868,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push exact diagnostic implementation commit `0d3a0c9d3` plus its evidence-ledger head and run all -six target-native cells. Require both Windows jobs to prove the generated Release compiler/linker -settings; if arm64 still differs, record both bounded disassemblies while retaining all four POSIX +Add a purpose fixture for lowercase native arm64 MSBuild platform spelling, implement only an exact +case-insensitive expected-architecture comparison, and rerun all six target-native cells. Require +both Windows jobs to prove the generated Release compiler/linker settings; if arm64 then reaches the +known comparator mismatch, record both bounded disassemblies while retaining all four POSIX controls, strict comparison, and rejected-arm64 no-upload. 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 publication, desktop resolver, SSH From e1a63930b4d350e8677e2be3a35b0bdec95f268c Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 14:31:32 -0400 Subject: [PATCH 041/267] docs(ssh): record native platform case gate Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 117 +++++++++++++++--- 1 file changed, 99 insertions(+), 18 deletions(-) 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 aa48e88f3bf..5928be7905f 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29357355064 keeps Linux x64/arm64, macOS x64/arm64, and Windows x64 exactly reproducible and uploads exactly those five unpublished artifacts; Windows x64 proves the generated compiler/linker flags in both clean builds, while native Windows arm64 fails closed before staging because the generated Release-condition verifier does not accept the runner's lowercase `arm64` platform spelling; no arm64 artifact is uploaded and no disassembly or producer correction is yet justified; the active bounded package makes only that architecture comparison case-insensitive while retaining exact Release/configuration cardinality and option checks; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29357355064 keeps Linux x64/arm64, macOS x64/arm64, and Windows x64 exactly reproducible and uploads exactly those five unpublished artifacts; Windows x64 proves the generated compiler/linker flags in both clean builds, while native Windows arm64 fails closed before staging because the generated Release-condition verifier does not accept the runner's lowercase `arm64` platform spelling; no arm64 artifact is uploaded and no disassembly or producer correction is yet justified; exact implementation commit `4207c207a` makes only that architecture comparison case-insensitive while retaining exact Release/configuration cardinality and option checks, is locally green under E-M3-WINDOWS-MSBUILD-PLATFORM-CASE-LOCAL-001, and awaits all six target-native cells; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -163,12 +163,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: test and implement the bounded case-insensitive architecture comparison for - generated MSBuild Release conditions, retain exact group and option cardinality, then rerun all - six target-native cells. Require both Windows jobs to prove their generated compiler/linker - settings; only if arm64 reaches the known comparator mismatch may paired bounded disassembly - justify a producer correction. Preserve all five controls, rejected-binary no-upload, the - repository-wide node-pty patch, legacy/default path, and every other release gate. +- Next required action: push exact implementation commit `4207c207a` plus its evidence-ledger head + and rerun all six target-native cells. Require both Windows jobs to prove their generated + compiler/linker settings; only if arm64 reaches the known comparator mismatch may paired bounded + disassembly justify a producer correction. Preserve all five controls, rejected-binary no-upload, + the repository-wide node-pty patch, legacy/default path, and every other release gate. ## Non-Negotiable Invariants @@ -679,8 +678,9 @@ Windows arm64 before staging because the Release group lookup does not accept th **In progress — 2026-07-14, Codex implementation owner:** make only the expected generated Release architecture comparison case-insensitive, while retaining one exact Release group, the expected tuple architecture, one inherited compiler/linker option block, and exactly one instance of each -required flag. Add the lowercase-arm64 purpose fixture before rerunning all six native cells. Do not -change a producer, normalize output, weaken reproducibility, or connect a production consumer. +required flag. Exact implementation commit `4207c207a` and its lowercase-arm64 fixture are locally +green under E-M3-WINDOWS-MSBUILD-PLATFORM-CASE-LOCAL-001; all six native cells remain required. Do +not change a producer, normalize output, weaken reproducibility, or connect a production consumer. Each runtime must contain only the executable closure required by the relay. @@ -4811,6 +4811,88 @@ configuration` before runtime staging, verification, smoke, comparison, or diagn architecture case-insensitively, retaining exact group and option cardinality. Rerun both Windows cells and all four POSIX controls before interpreting the underlying PE drift. +### E-M3-WINDOWS-MSBUILD-PLATFORM-CASE-LOCAL-001 — Lowercase native platform contract + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `4207c207ab932ef67522b4f73b98d63fa32d0924`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741), native execution pending +- Runner: macOS 26.2 build 25C56, native Apple M4 arm64; Node v26.0.0 and pnpm 10.24.0. This + runner cannot generate or execute native MSBuild/ARM64 PE inputs, so the target-native Windows + jobs remain authoritative. +- Remote and transport: none; synthetic generated-project fixtures and workflow/source contracts + only +- Exact red command: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs + ``` + +- Red result: FAIL as intended, one failed and three passed tests because a generated + `Release|arm64` condition was rejected for the `win32-arm64` tuple. +- Exact green commands: + + ```sh + node --check config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs + node --check config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.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 \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-build.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs + pnpm run typecheck + pnpm exec oxlint config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md + pnpm exec lint-staged + git diff --check + ``` + +- Green result: PASS. Both syntax checks exited zero; the purpose command passed 8/8 tests across + three suites; all 15 artifact suites passed 53/53 tests. Typecheck, focused oxlint, the + 355-entry max-lines ratchet, full repository lint/reliability/localization, formatting, staged + hooks, and diff checks exited zero. Full lint emitted only pre-existing warnings outside this + package. +- Duration and resource metrics: red suite 382 ms Vitest; purpose suite 181 ms Vitest; complete + artifact suite 1.51 s Vitest; typecheck 3.41 s; focused lint/line ratchet 3.28 s; full lint + 13.30 s. Synthetic peak RSS, open files/channels, and cancellation settlement were not + instrumented. +- Artifact/log/trace link: exact source commit and local command output; no binary artifact was + created or uploaded +- Oracle proved: `Release|arm64` is accepted for only the expected `win32-arm64` architecture while + the quoted Release condition remains exact apart from case. Wrong architecture, missing or + duplicate group/option blocks, missing inheritance, and missing or duplicated required flags + still fail closed. No source rewrite, producer, comparator, staging, or production path changed. +- Does not prove: the exact native generated XML, native flag propagation, complete arm64 build, + paired disassembly, a safe producer correction, arm64 equality, oldest baselines, native trust, + SSH, publication, transfer, fallback, UI, or any enabled tuple. +- Checklist items satisfied: local generated-project platform-casing contract only; no tuple or + production checkbox. +- Follow-up: push the exact implementation and ledger head, then run all six target-native cells and + require both Windows architectures to log the accepted generated settings before interpreting any + later comparator mismatch. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -4868,12 +4950,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Add a purpose fixture for lowercase native arm64 MSBuild platform spelling, implement only an exact -case-insensitive expected-architecture comparison, and rerun all six target-native cells. Require -both Windows jobs to prove the generated Release compiler/linker settings; if arm64 then reaches the -known comparator mismatch, record both bounded disassemblies while retaining all four POSIX -controls, strict comparison, and rejected-arm64 no-upload. 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 publication, desktop resolver, SSH -transfer/install, per-target Beta, fallback, tuple enablement, or default behavior may be connected -by this package. +Push exact implementation commit `4207c207a` plus its evidence-ledger head and rerun all six +target-native cells. Require both Windows jobs to prove the generated Release compiler/linker +settings; if arm64 then reaches the known comparator mismatch, record both bounded disassemblies +while retaining all four POSIX controls, strict comparison, and rejected-arm64 no-upload. +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 publication, desktop resolver, SSH transfer/install, per-target +Beta, fallback, tuple enablement, or default behavior may be connected by this package. From 36a19f525cc63e488f49daa5c3a6aafc590acc08 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 14:55:29 -0400 Subject: [PATCH 042/267] test(ssh): evaluate Windows incremental link state Co-authored-by: Orca --- config/scripts/ssh-relay-node-pty-build.mjs | 21 +- ...lay-node-pty-windows-build-determinism.mjs | 26 +++ ...ode-pty-windows-build-determinism.test.mjs | 40 +++- ...github-release-implementation-checklist.md | 194 ++++++++++++++---- 4 files changed, 239 insertions(+), 42 deletions(-) diff --git a/config/scripts/ssh-relay-node-pty-build.mjs b/config/scripts/ssh-relay-node-pty-build.mjs index 6368b2af7d3..403f4815aea 100644 --- a/config/scripts/ssh-relay-node-pty-build.mjs +++ b/config/scripts/ssh-relay-node-pty-build.mjs @@ -7,7 +7,9 @@ import { promisify } from 'node:util' import { applyWindowsNodePtySettlement } from './ssh-relay-node-pty-windows-settlement.mjs' import { applyWindowsNodePtyBuildDeterminism, - assertWindowsNodePtyGeneratedBuildSettings + assertWindowsNodePtyGeneratedBuildSettings, + parseWindowsNodePtyLinkIncremental, + windowsNodePtyLinkIncrementalCommand } from './ssh-relay-node-pty-windows-build-determinism.mjs' const require = createRequire(import.meta.url) @@ -192,8 +194,23 @@ export async function buildPatchedSshRelayNodePty({ tuple }) if (generatedSettings) { + const evaluation = windowsNodePtyLinkIncrementalCommand({ + nodePtyDirectory: buildDirectory, + tuple + }) + // Why: the emitted ARM64 thunks cannot justify a producer change without the effective link state. + const evaluated = await runCommand(evaluation.command, evaluation.args, { + cwd: buildDirectory, + signal: controller.signal, + timeout: BUILD_TIMEOUT_MS, + windowsHide: true, + env: buildEnvironment + }) process.stdout.write( - `windows_node_pty_msbuild_settings=${JSON.stringify(generatedSettings)}\n` + `windows_node_pty_msbuild_settings=${JSON.stringify({ + ...generatedSettings, + linkIncremental: parseWindowsNodePtyLinkIncremental(evaluated.stdout) + })}\n` ) } } finally { diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs index a92bc3b08a1..1fc1481ff73 100644 --- a/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs @@ -125,3 +125,29 @@ export async function assertWindowsNodePtyGeneratedBuildSettings({ nodePtyDirect project } } + +export function windowsNodePtyLinkIncrementalCommand({ nodePtyDirectory, tuple }) { + if (!tuple.startsWith('win32-')) { + return undefined + } + const platform = tuple.endsWith('-arm64') ? 'ARM64' : 'x64' + return { + command: 'MSBuild.exe', + args: [ + join(nodePtyDirectory, 'build', 'conpty_console_list.vcxproj'), + '-nologo', + '-verbosity:quiet', + '-property:Configuration=Release', + `-property:Platform=${platform}`, + '-getProperty:LinkIncremental' + ] + } +} + +export function parseWindowsNodePtyLinkIncremental(output) { + const value = output.trim().toLowerCase() + if (value !== 'true' && value !== 'false') { + throw new Error('unexpected LinkIncremental evaluation from MSBuild') + } + return value === 'true' +} diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs index 29444fde8e2..82a376e563d 100644 --- a/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs @@ -6,7 +6,9 @@ import { afterEach, describe, expect, it } from 'vitest' import { applyWindowsNodePtyBuildDeterminism, - assertWindowsNodePtyGeneratedBuildSettings + assertWindowsNodePtyGeneratedBuildSettings, + parseWindowsNodePtyLinkIncremental, + windowsNodePtyLinkIncrementalCommand } from './ssh-relay-node-pty-windows-build-determinism.mjs' const temporaryDirectories = [] @@ -86,6 +88,42 @@ describe('SSH relay Windows node-pty build determinism', () => { }) }) + it('evaluates the effective Release incremental-link state with strict output parsing', () => { + expect( + windowsNodePtyLinkIncrementalCommand({ + nodePtyDirectory: 'C:\\artifact-node-pty', + tuple: 'win32-arm64' + }) + ).toEqual({ + command: 'MSBuild.exe', + args: [ + join('C:\\artifact-node-pty', 'build', 'conpty_console_list.vcxproj'), + '-nologo', + '-verbosity:quiet', + '-property:Configuration=Release', + '-property:Platform=ARM64', + '-getProperty:LinkIncremental' + ] + }) + expect(parseWindowsNodePtyLinkIncremental('TRUE\r\n')).toBe(true) + expect(parseWindowsNodePtyLinkIncremental('false\n')).toBe(false) + expect(() => parseWindowsNodePtyLinkIncremental('true\nwarning')).toThrow( + 'unexpected LinkIncremental evaluation' + ) + expect( + windowsNodePtyLinkIncrementalCommand({ + nodePtyDirectory: 'C:\\artifact-node-pty', + tuple: 'win32-x64' + }).args + ).toContain('-property:Platform=x64') + expect( + windowsNodePtyLinkIncrementalCommand({ + nodePtyDirectory: '/not-used-on-posix', + tuple: 'linux-arm64-glibc' + }) + ).toBeUndefined() + }) + it('rejects missing, duplicate, non-inherited, or wrong-architecture generated settings', async () => { for (const fixture of [ { compilerOptions: '/Brepro %(AdditionalOptions)' }, 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 5928be7905f..36ef97317ba 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29357355064 keeps Linux x64/arm64, macOS x64/arm64, and Windows x64 exactly reproducible and uploads exactly those five unpublished artifacts; Windows x64 proves the generated compiler/linker flags in both clean builds, while native Windows arm64 fails closed before staging because the generated Release-condition verifier does not accept the runner's lowercase `arm64` platform spelling; no arm64 artifact is uploaded and no disassembly or producer correction is yet justified; exact implementation commit `4207c207a` makes only that architecture comparison case-insensitive while retaining exact Release/configuration cardinality and option checks, is locally green under E-M3-WINDOWS-MSBUILD-PLATFORM-CASE-LOCAL-001, and awaits all six target-native cells; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29358223742 proves the lowercase-arm64 generated-setting correction on the real runner, keeps Linux x64/arm64, macOS x64/arm64, and Windows x64 exactly reproducible, and uploads exactly those five unpublished artifacts; Windows arm64 builds, verifies, and executes both complete runtimes, then fails closed with no upload because 2,879 linker-emitted 16-byte function thunks differ only in their unreachable fourth `udf` instruction while each thunk's `adrp`/`add`/`br` control flow is byte-identical; the remaining 68 bytes are derived `/Brepro` COFF/debug identities; the active bounded diagnostic must prove the generated incremental-link state that owns the thunks before any producer correction; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -123,7 +123,13 @@ same change as the work it records. arm64 build fails closed at the new Release-condition verifier before runtime staging under E-M3-WINDOWS-MSBUILD-PLATFORM-CASE-CI-RED-001. The runner exposes `Platform=arm64`; the active correction accepts only a case-insensitive exact match for the expected architecture and retains - every option/cardinality check. No producer, comparator, upload, or production behavior changes. + every option/cardinality check. Exact-head run 29358223742 proves that correction on the real + arm64 runner, preserves five exact controls, and reaches paired disassembly under + E-M3-WINDOWS-ARM64-THUNK-DISASSEMBLY-CI-RED-001. The arm64 outputs have 2,879 linker-emitted + 16-byte function thunks whose three control-flow instructions match and whose unreachable fourth + `udf` instruction differs, plus 68 derived identity bytes. Strict comparison and rejected-output + no-upload remain intact. The active diagnostic must prove the generated incremental-link state + before any producer change. No comparator, publication, or production behavior changes. - Windows input correction: E-M3-WINDOWS-INPUT-GAP-001 proved the official Windows ZIP lacks headers and `node.lib`. Both artifacts now require the exact signed headers archive and tuple import library as explicit inputs. The schema, signed-checksum verifier, bounded ZIP/header extraction, @@ -163,11 +169,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: push exact implementation commit `4207c207a` plus its evidence-ledger head - and rerun all six target-native cells. Require both Windows jobs to prove their generated - compiler/linker settings; only if arm64 reaches the known comparator mismatch may paired bounded - disassembly justify a producer correction. Preserve all five controls, rejected-binary no-upload, - the repository-wide node-pty patch, legacy/default path, and every other release gate. +- Next required action: add a bounded generated-project diagnostic for the exact Windows Release + `LinkIncremental` state and rerun all six target-native cells. Require both Windows clean builds + to report the same state before considering one copied-artifact producer correction. Preserve all + five controls, strict comparison, rejected-arm64 no-upload, the repository-wide node-pty patch, + legacy/default path, and every other release gate. ## Non-Negotiable Invariants @@ -675,12 +681,21 @@ no second producer correction is justified before the target-native evidence. Ex Windows arm64 before staging because the Release group lookup does not accept the runner's lowercase `arm64` platform spelling (E-M3-WINDOWS-MSBUILD-PLATFORM-CASE-CI-RED-001). -**In progress — 2026-07-14, Codex implementation owner:** make only the expected generated Release -architecture comparison case-insensitive, while retaining one exact Release group, the expected -tuple architecture, one inherited compiler/linker option block, and exactly one instance of each -required flag. Exact implementation commit `4207c207a` and its lowercase-arm64 fixture are locally -green under E-M3-WINDOWS-MSBUILD-PLATFORM-CASE-LOCAL-001; all six native cells remain required. Do -not change a producer, normalize output, weaken reproducibility, or connect a production consumer. +Exact-head run 29358223742 then proves that correction on native Windows arm64 and retains all five +reproducible controls. Both arm64 clean builds log the required compiler/linker options, assemble, +verify, and execute, but strict comparison still rejects `conpty_console_list.node` and uploads no +arm64 artifact. The complete PE scan reports 5,826 differing bytes: 5,758 bytes across 2,879 +two-byte `.text` ranges exactly 16 bytes apart plus 68 derived COFF/debug/CodeView bytes. Paired +disassembly proves each 16-byte function thunk has byte-identical `adrp x16`, `add x16`, and +`br x16` instructions; only the unreachable fourth instruction changes from `udf #0x24d` to +`udf #0x16d` (E-M3-WINDOWS-ARM64-THUNK-DISASSEMBLY-CI-RED-001). + +**In progress — 2026-07-14, Codex implementation owner:** classify the linker-owned thunk producer +before changing it. Add a bounded generated-project diagnostic for the exact Release +`LinkIncremental` state while retaining the existing architecture, option, cardinality, strict +comparison, and no-upload gates. The thunk layout is consistent with an incremental-link table, but +that setting is not yet proved on the native runner. Do not normalize rejected bytes, weaken the +comparator, modify repository-wide node-pty, or connect a production consumer. Each runtime must contain only the executable closure required by the relay. @@ -1285,16 +1300,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; PE diagnostic awaiting native evidence | `39ee3451b`; E-M3-WINDOWS-PE-DIAGNOSTIC-LOCAL-001; arm64 repeat-red evidence | -| 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; arm64 linker-thunk producer diagnostic active | `e1a63930b`; E-M3-WINDOWS-ARM64-THUNK-DISASSEMBLY-CI-RED-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 | — | Every PR must document: @@ -4893,23 +4908,123 @@ configuration` before runtime staging, verification, smoke, comparison, or diagn require both Windows architectures to log the accepted generated settings before interpreting any later comparator mismatch. +### E-M3-WINDOWS-ARM64-THUNK-DISASSEMBLY-CI-RED-001 — Native linker-thunk drift isolated + +- Date: 2026-07-14 +- Commit SHA / PR: exact head `e1a63930b4d350e8677e2be3a35b0bdec95f268c`, containing exact + lowercase-platform implementation commit `4207c207ab932ef67522b4f73b98d63fa32d0924`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Run and jobs: [run 29358223742](https://github.com/stablyai/orca/actions/runs/29358223742), + conclusion `failure`; Linux arm64 `87171219009` success, Windows arm64 `87171219067` expected + strict failure, Linux x64 `87171219080` success, Windows x64 `87171219167` success, macOS arm64 + `87171219192` success, and macOS x64 `87171219210` success +- Runners: GitHub-hosted `ubuntu-24.04-arm` / `ubuntu24-arm64` `20260706.52.2` native ARM64; + `windows-11-arm` / `win11-arm64` `20260706.102.1` native ARM64; `ubuntu-24.04` / `ubuntu24` + `20260705.232.1` native X64; `windows-2022` / `win22` `20260706.237.1` native X64; `macos-15` / + `macos15` `20260706.0213.1` native ARM64; and `macos-15-intel` / `macos15` + `20260629.0276.1` native X64. Every job recorded Node v24.18.0 and the exact source commit. +- Remote and transport: none; target-native artifact assembly, generated-project verification, + bundled-Node/native-module execution, strict comparison, and unpublished Actions artifacts only +- Exact evidence commands: + + ```sh + gh pr view 8741 --repo stablyai/orca \ + --json number,state,isDraft,headRefName,headRefOid,url,statusCheckRollup,updatedAt + gh api repos/stablyai/orca/actions/runs/29358223742 + gh api 'repos/stablyai/orca/actions/runs/29358223742/jobs?per_page=100' + gh api 'repos/stablyai/orca/actions/runs/29358223742/artifacts?per_page=100' + gh run view 29358223742 --repo stablyai/orca --job 87171219167 --log + gh run view 29358223742 --repo stablyai/orca --job 87171219067 --log-failed + gh api repos/stablyai/orca/actions/jobs/87171219067/logs | \ + sed -n '/windows_arm64_disassembly=first/,$p' + gh run download 29358223742 --repo stablyai/orca --dir + jq -c '{tupleId,contentId,nodeVersion,archive,fileCount}' \ + /*/*.identity.json + ``` + +- Result: FAIL as the intended strict reproducibility gate. The lowercase `arm64` correction reaches + the one expected Release group on both native arm64 clean builds. Both log exactly one inherited + compiler and linker option block containing `/Brepro` and `/experimental:deterministic`, then + assemble, inspect, verify, and execute the complete runtime. Each bundled Node v24.18.0 smoke + proves ABI 137, patched PTY input/resize/exit code 23, watcher create/update/delete, and settled + Windows resources. Strict comparison then rejects only + `runtime/node_modules/node-pty/build/Release/conpty_console_list.node`; no arm64 output is staged + for upload. +- Windows arm64 outputs: both PE files are 956,928 bytes. The first content identity is + `sha256:a6adc716ec39503f68f4e322624ed9193b9343ca4b5f74d22843c47635e81519`, with + 33,261,547-byte ZIP + `sha256:5aebde73ef6cad605d13c5c27977f1229729aa28a6959649989d8ecd4a6d2a9e`; the second is + `sha256:2aef13722dd084941069d260591c529d1ca5ebf0b1e2defa34da4c43f44c2d24`, with the same + ZIP size and digest + `sha256:974612580fa7a008bcf3377ddd13ce5f99b78cc31c99c7b9b720066b3f9dc91c`. Both archives + contain 42 files and expand to 86,189,895 bytes. +- PE oracle: the complete scan reports 5,826 differing bytes across 2,886 coalesced ranges. The + `.text` contribution is 5,758 bytes across 2,879 two-byte ranges from file offsets 1,052 through + 47,102, exactly 16 bytes apart. The remaining 68 bytes are one four-byte COFF timestamp, three + four-byte debug timestamps, one 16-byte CodeView identifier, and 36 derived `.rdata` bytes. + Section layout, file size, executable control-flow bytes, and all other bytes match. +- Paired disassembly oracle: the first 512 `.text` bytes contain repeated 16-byte function thunks. + For every displayed thunk, `adrp x16, target`, `add x16, x16, offset`, and `br x16` are + byte-identical. Only the unreachable fourth instruction differs: first build `0000024d` + (`udf #0x24d`), second build `0000016d` (`udf #0x16d`). This classifies the changing bytes as + linker-emitted thunk padding; it does not yet prove the generated linker setting responsible. +- Uploaded controls: exactly five unpublished seven-day artifacts and no Windows arm64 artifact: + Linux x64 `8321096098`, 29,281,909 bytes, + `sha256:cbe4d1be7664227fa34fb977f568ba0fc9409d5b5f5139fd4937013e46501074`, content ID + `sha256:960546cd96c67fcf9bb0a61e96ecdbecbffd9104d3a495578f8bb19dd810649a`, archive + `sha256:c87306e069af8c849b7679ccdf0504cc48c51b0ac0586edcdbf698972156bab5`; Linux arm64 + `8321099340`, 28,212,775 bytes, + `sha256:0502a9710a9612c44fc2aa4792ee1f30859158fcac40668b59c35ca670eb83f2`, content ID + `sha256:aa3aa8ae8b42334ba7b0dbe5c43fd1184e36b3f4f4a9bec0e990e9b78f090756`, archive + `sha256:f017fcd7808cd8ba86175b2fad63103e880d57ccbec70412938e0cc145310eda`; macOS x64 + `8321127611`, 26,376,349 bytes, + `sha256:9297b99e9605c065cbf04d3431edadf8312ee19dd1b60d8790cba95f090bd68e`, content ID + `sha256:585ea6034cdd07487d8667059f975a877c795a45dc0d6eeee1617f2e3749faa2`, archive + `sha256:5669d69085d268e01fe5ece6e4372832c3f61c89bcefa9acaf732c81eb460775`; macOS arm64 + `8321085541`, 24,740,934 bytes, + `sha256:8ab194c0d0c08ab40aa8f7821738fd5c2e3b1821fb83df6d59992b6106ba46f2`, content ID + `sha256:40ff5d2036784b794e7b09f78596409f63f3145280c530bece5280d40897f6cb`, archive + `sha256:b51e5a6a53aeff2ae47e4156900981395516c9b246ab1ae3fff5c4c34500ee0a`; and Windows x64 + `8321159937`, 37,075,308 bytes, + `sha256:dcadebbcbb7d3c6f308badeefa7940350022b22bb76fd31baed15bb304af56c1`, content ID + `sha256:2a6bfa06b445fb78d6e4a5abba6ec379cbed6f7830c805e82c7145d18a1c3d8b`, archive + `sha256:ee1a0cc8bdf55d783ed1c1232dedd788710a1d88edd7e89a2ddfa84e0986d7ca`. +- Duration and resource metrics: jobs ran 3m07s Linux arm64, 9m15s Windows arm64, 3m02s Linux x64, + 5m22s Windows x64, 2m41s macOS arm64, and 4m11s macOS x64. Windows arm64 clean builds took + 149,415.066 ms and 130,574.341 ms; verification took 7,688.862 ms and 8,012.846 ms, with smoke + taking 5,865.144 ms at 53,235,712-byte RSS and 5,553.779 ms at 53,133,312-byte RSS. Cancellation, + SSH channel/file counts, and fallback delay are outside this artifact-only run. +- Artifact/log/trace link: run/jobs and the five unpublished artifacts above; rejected arm64 bytes + exist only in the bounded job workspace and log diagnostics and were not uploaded +- Oracle proved: real native lowercase platform acceptance, exact generated compiler/linker option + propagation, two complete executable arm64 candidates, strict rejection, exact five-control + reproducibility, bounded linker-thunk classification, and rejected-output no-upload. +- Does not prove: whether MSBuild enabled incremental linking, that disabling it is the safe producer + correction, arm64 equality, oldest baselines, native trust, SSH, publication, transfer, fallback, + UI, or any enabled tuple. No executable byte may be normalized from this evidence. +- Checklist items satisfied: target-native platform-case gate and bounded ARM64 instruction + diagnostic only; no tuple or production checkbox. +- Follow-up: add a purpose-tested bounded parser for the exact generated Release + `LinkIncremental` state, require both Windows clean builds to report it, and rerun all six native + controls before considering one copied-artifact producer correction. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit owner and promotion condition. -| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | -| ------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | -| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | -| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | -| Native clean-rebuild identity unproved | Five native cells pass; arm64 generated-setting verification currently stops before the known PE drift | Toolchain drift may change native content IDs | Codex implementation owner | Generated-build-input and ARM64 instruction diagnostics identify a safe producer correction; all six clean builds then match without weakening the oracle | Open | -| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | -| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | -| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | -| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | -| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | -| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | +| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | +| ------------------------------------------ | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | +| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | +| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | +| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | +| Native clean-rebuild identity unproved | Five native cells pass; arm64 builds and executes twice but strict comparison rejects linker-thunk drift | Toolchain drift may change native content IDs | Codex implementation owner | Prove the generated incremental-link state, apply only an evidenced producer correction, then make all six clean builds match without weakening the oracle | Open | +| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | +| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | +| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | +| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | +| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | +| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | ## Final Definition of Done @@ -4950,10 +5065,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push exact implementation commit `4207c207a` plus its evidence-ledger head and rerun all six -target-native cells. Require both Windows jobs to prove the generated Release compiler/linker -settings; if arm64 then reaches the known comparator mismatch, record both bounded disassemblies -while retaining all four POSIX controls, strict comparison, and rejected-arm64 no-upload. +Add a bounded, purpose-tested generated-project diagnostic for the exact Windows Release +`LinkIncremental` state, then rerun both Windows architectures and all four POSIX controls at the +exact implementation head. Require both Windows clean builds to report the same setting. Only if +that target-native evidence classifies the 2,879 function thunks may one copied-artifact producer +correction be considered; retain strict comparison, all five controls, and rejected-arm64 no-upload. 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 publication, desktop resolver, SSH transfer/install, per-target From 2d4433bbd0d2481d3f91c56d10155989cd6e7e44 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 14:57:07 -0400 Subject: [PATCH 043/267] docs(ssh): record native linker thunk gate Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 112 ++++++++++++++++-- 1 file changed, 100 insertions(+), 12 deletions(-) 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 36ef97317ba..dad31cbdb46 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29358223742 proves the lowercase-arm64 generated-setting correction on the real runner, keeps Linux x64/arm64, macOS x64/arm64, and Windows x64 exactly reproducible, and uploads exactly those five unpublished artifacts; Windows arm64 builds, verifies, and executes both complete runtimes, then fails closed with no upload because 2,879 linker-emitted 16-byte function thunks differ only in their unreachable fourth `udf` instruction while each thunk's `adrp`/`add`/`br` control flow is byte-identical; the remaining 68 bytes are derived `/Brepro` COFF/debug identities; the active bounded diagnostic must prove the generated incremental-link state that owns the thunks before any producer correction; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29358223742 proves the lowercase-arm64 generated-setting correction on the real runner, keeps Linux x64/arm64, macOS x64/arm64, and Windows x64 exactly reproducible, and uploads exactly those five unpublished artifacts; Windows arm64 builds, verifies, and executes both complete runtimes, then fails closed with no upload because 2,879 linker-emitted 16-byte function thunks differ only in their unreachable fourth `udf` instruction while each thunk's `adrp`/`add`/`br` control flow is byte-identical; the remaining 68 bytes are derived `/Brepro` COFF/debug identities; exact implementation commit `36a19f525` adds a bounded pre-staging MSBuild evaluation of the effective Release `LinkIncremental` state and is locally green under E-M3-WINDOWS-LINK-INCREMENTAL-DIAGNOSTIC-LOCAL-001; all six native cells remain required before any producer correction; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -169,11 +169,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: add a bounded generated-project diagnostic for the exact Windows Release - `LinkIncremental` state and rerun all six target-native cells. Require both Windows clean builds - to report the same state before considering one copied-artifact producer correction. Preserve all - five controls, strict comparison, rejected-arm64 no-upload, the repository-wide node-pty patch, - legacy/default path, and every other release gate. +- Next required action: push exact implementation commit `36a19f525` plus its evidence-ledger head + and rerun all six target-native cells. Require both Windows clean builds to report the same + evaluated `LinkIncremental` state before considering one copied-artifact producer correction. + Preserve all five controls, strict comparison, rejected-arm64 no-upload, the repository-wide + node-pty patch, legacy/default path, and every other release gate. ## Non-Negotiable Invariants @@ -694,7 +694,9 @@ disassembly proves each 16-byte function thunk has byte-identical `adrp x16`, `a before changing it. Add a bounded generated-project diagnostic for the exact Release `LinkIncremental` state while retaining the existing architecture, option, cardinality, strict comparison, and no-upload gates. The thunk layout is consistent with an incremental-link table, but -that setting is not yet proved on the native runner. Do not normalize rejected bytes, weaken the +that setting is not yet proved on the native runner. Exact implementation commit `36a19f525` is +locally red/green and passes the complete artifact/static gate under +E-M3-WINDOWS-LINK-INCREMENTAL-DIAGNOSTIC-LOCAL-001. Do not normalize rejected bytes, weaken the comparator, modify repository-wide node-pty, or connect a production consumer. Each runtime must contain only the executable closure required by the relay. @@ -5008,6 +5010,92 @@ configuration` before runtime staging, verification, smoke, comparison, or diagn `LinkIncremental` state, require both Windows clean builds to report it, and rerun all six native controls before considering one copied-artifact producer correction. +### E-M3-WINDOWS-LINK-INCREMENTAL-DIAGNOSTIC-LOCAL-001 — Effective MSBuild link-state gate + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `36a19f525cc63e488f49daa5c3a6aafc590acc08`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741), target-native execution pending +- Runner: macOS 26.2 build 25C56, native Apple M4 arm64; Node v26.0.0 and pnpm 10.24.0. This runner + cannot evaluate a native MSBuild project, so the two Windows jobs remain authoritative. +- Remote and transport: none; strict command/output contracts and builder ordering only +- Exact red command: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs + ``` + +- Red result: FAIL as intended, one failed and four passed tests because + `windowsNodePtyLinkIncrementalCommand` was not implemented. +- Exact green commands: + + ```sh + node --check config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs + node --check config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs + node --check config/scripts/ssh-relay-node-pty-build.mjs + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.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 \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-build.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs + pnpm run typecheck + pnpm exec oxlint \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.mjs + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.mjs \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md + pnpm exec lint-staged + git diff --check + ``` + +- Green result: PASS. All syntax checks exited zero; the purpose command passed 9/9 tests across + three suites; all 15 artifact suites passed 54/54 tests. Typecheck, focused oxlint, the 355-entry + max-lines ratchet, full repository lint/reliability/localization, formatting, staged hooks, and + diff checks exited zero. Full lint emitted only pre-existing warnings outside this package; + `lint-staged` reported no staged files in its standalone run and the commit hook subsequently ran + oxlint, React Doctor, and oxfmt successfully over all four staged files. +- Duration and resource metrics: purpose gate 1.93 s wall / 459 ms Vitest; complete artifact gate + 2.87 s wall / 1.35 s Vitest; typecheck 3.10 s; focused lint and line ratchet 3.09 s; full lint + 12.91 s. Synthetic peak RSS, open files/channels, cancellation settlement, and fallback delay + were not instrumented. +- Artifact/log/trace link: exact implementation commit and local command output; no binary artifact + was created or uploaded +- Oracle proved: Windows builds form a bounded `MSBuild.exe` query for the exact generated + `conpty_console_list.vcxproj`, Release configuration, and tuple architecture; request only the + evaluated `LinkIncremental` property; accept exactly one case-insensitive boolean result; and + fail before staging on missing, noisy, or non-boolean output. x64 and arm64 commands are covered, + POSIX creates no query, and the result is appended to the existing generated-setting record. +- Does not prove: `MSBuild.exe -getProperty` availability or result on either native Windows image, + the actual effective incremental-link state, the thunk owner, a safe producer correction, arm64 + equality, oldest baselines, native trust, SSH, publication, transfer, fallback, UI, or any enabled + tuple. +- Checklist items satisfied: local bounded effective-link-state diagnostic only; no tuple or + production checkbox. +- Follow-up: push the exact implementation and ledger head, run all six target-native cells, and + require both Windows clean builds to report `linkIncremental` before changing the producer. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -5065,11 +5153,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Add a bounded, purpose-tested generated-project diagnostic for the exact Windows Release -`LinkIncremental` state, then rerun both Windows architectures and all four POSIX controls at the -exact implementation head. Require both Windows clean builds to report the same setting. Only if -that target-native evidence classifies the 2,879 function thunks may one copied-artifact producer -correction be considered; retain strict comparison, all five controls, and rejected-arm64 no-upload. +Push exact implementation commit `36a19f525` plus its evidence-ledger head, then rerun both Windows +architectures and all four POSIX controls. Require both Windows clean builds to report the same +evaluated `LinkIncremental` setting. Only if that target-native evidence classifies the 2,879 +function thunks may one copied-artifact producer correction be considered; retain strict +comparison, all five controls, and rejected-arm64 no-upload. 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 publication, desktop resolver, SSH transfer/install, per-target From 4a66435b71b158624645c614543d139a9ea45d51 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 15:22:44 -0400 Subject: [PATCH 044/267] build(ssh): inspect native linker command tracking Co-authored-by: Orca --- config/scripts/ssh-relay-node-pty-build.mjs | 18 +- ...lay-node-pty-windows-build-determinism.mjs | 118 +++++++-- ...ode-pty-windows-build-determinism.test.mjs | 112 ++++++-- ...github-release-implementation-checklist.md | 242 +++++++++++++++--- 4 files changed, 395 insertions(+), 95 deletions(-) diff --git a/config/scripts/ssh-relay-node-pty-build.mjs b/config/scripts/ssh-relay-node-pty-build.mjs index 403f4815aea..59f0b06f369 100644 --- a/config/scripts/ssh-relay-node-pty-build.mjs +++ b/config/scripts/ssh-relay-node-pty-build.mjs @@ -8,8 +8,8 @@ import { applyWindowsNodePtySettlement } from './ssh-relay-node-pty-windows-sett import { applyWindowsNodePtyBuildDeterminism, assertWindowsNodePtyGeneratedBuildSettings, - parseWindowsNodePtyLinkIncremental, - windowsNodePtyLinkIncrementalCommand + inspectWindowsNodePtyLinkCommandTracking, + windowsNodePtyLinkCommandTrackingPath } from './ssh-relay-node-pty-windows-build-determinism.mjs' const require = createRequire(import.meta.url) @@ -194,22 +194,16 @@ export async function buildPatchedSshRelayNodePty({ tuple }) if (generatedSettings) { - const evaluation = windowsNodePtyLinkIncrementalCommand({ + const trackingPath = windowsNodePtyLinkCommandTrackingPath({ nodePtyDirectory: buildDirectory, tuple }) - // Why: the emitted ARM64 thunks cannot justify a producer change without the effective link state. - const evaluated = await runCommand(evaluation.command, evaluation.args, { - cwd: buildDirectory, - signal: controller.signal, - timeout: BUILD_TIMEOUT_MS, - windowsHide: true, - env: buildEnvironment - }) + // Why: an unset property cannot classify ARM64 thunks; the post-build command is authoritative. + const linkCommand = await inspectWindowsNodePtyLinkCommandTracking(trackingPath) process.stdout.write( `windows_node_pty_msbuild_settings=${JSON.stringify({ ...generatedSettings, - linkIncremental: parseWindowsNodePtyLinkIncremental(evaluated.stdout) + linkCommand })}\n` ) } diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs index 1fc1481ff73..1df9fca9a5e 100644 --- a/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs @@ -1,4 +1,4 @@ -import { readFile, writeFile } from 'node:fs/promises' +import { open, readFile, writeFile } from 'node:fs/promises' import { join } from 'node:path' const COMPILER_OPTIONS = ` 'VCCLCompilerTool': { @@ -42,6 +42,7 @@ const REPRODUCIBLE_LINKER_OPTIONS = ` 'VCLinkerTool': { }` const REQUIRED_GENERATED_OPTIONS = ['/Brepro', '/experimental:deterministic'] +const MAX_LINK_COMMAND_TRACKING_BYTES = 256 * 1024 function decodeXmlText(value) { return value @@ -126,28 +127,109 @@ export async function assertWindowsNodePtyGeneratedBuildSettings({ nodePtyDirect } } -export function windowsNodePtyLinkIncrementalCommand({ nodePtyDirectory, tuple }) { +export function windowsNodePtyLinkCommandTrackingPath({ nodePtyDirectory, tuple }) { if (!tuple.startsWith('win32-')) { return undefined } - const platform = tuple.endsWith('-arm64') ? 'ARM64' : 'x64' - return { - command: 'MSBuild.exe', - args: [ - join(nodePtyDirectory, 'build', 'conpty_console_list.vcxproj'), - '-nologo', - '-verbosity:quiet', - '-property:Configuration=Release', - `-property:Platform=${platform}`, - '-getProperty:LinkIncremental' - ] + return join( + nodePtyDirectory, + 'build', + 'Release', + 'obj', + 'conpty_console_list', + 'conpty_console_list.tlog', + 'link.command.1.tlog' + ) +} + +async function readBoundedLinkCommandTracking(path) { + const handle = await open(path, 'r') + try { + // Why: build-tool tracking output must not bypass the diagnostic's explicit memory ceiling. + const bytes = Buffer.allocUnsafe(MAX_LINK_COMMAND_TRACKING_BYTES + 1) + let length = 0 + while (length < bytes.length) { + const result = await handle.read(bytes, length, bytes.length - length, null) + if (result.bytesRead === 0) { + break + } + length += result.bytesRead + } + return bytes.subarray(0, length) + } finally { + await handle.close() + } +} + +function decodeLinkCommandTracking(bytes) { + if (bytes.length < 2 || bytes.length > MAX_LINK_COMMAND_TRACKING_BYTES) { + throw new Error('MSBuild linker tracking record is outside the bounded size') + } + let encoding = 'utf8' + let offset = 0 + if (bytes[0] === 0xff && bytes[1] === 0xfe) { + encoding = 'utf16le' + offset = 2 + } else if (bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) { + offset = 3 + } + let text + try { + const decoderEncoding = encoding === 'utf16le' ? 'utf-16le' : 'utf-8' + text = new TextDecoder(decoderEncoding, { fatal: true }).decode(bytes.subarray(offset)) + } catch { + throw new Error('MSBuild linker tracking record has invalid text encoding') + } + if (text.includes('\u0000') || text.includes('\ufffd')) { + throw new Error('MSBuild linker tracking record has invalid text encoding') } + return { encoding, text } } -export function parseWindowsNodePtyLinkIncremental(output) { - const value = output.trim().toLowerCase() - if (value !== 'true' && value !== 'false') { - throw new Error('unexpected LinkIncremental evaluation from MSBuild') +export function parseWindowsNodePtyLinkCommandTracking(bytes) { + const { encoding, text } = decodeLinkCommandTracking(bytes) + const commandRecords = text.split(/\r?\n/).filter((line) => line.startsWith('^')).length + if (commandRecords !== 1) { + throw new Error('MSBuild linker tracking record must contain exactly one command record') } - return value === 'true' + const switches = [ + ...text.matchAll( + /(?:^|\s)(\/(?:brepro|debug(?::[^\s"]+)?|experimental:deterministic|guard:[^\s"]+|incremental(?::(?:no|yes))?|opt:[^\s"]+))(?=\s|$)/gim + ) + ].map((match) => match[1].toLowerCase()) + const incrementalTokens = [ + ...text.matchAll(/(?:^|\s)(\/incremental(?::[^\s"]*)?)(?=\s|$)/gim) + ].map((match) => match[1].toLowerCase()) + if (incrementalTokens.some((token) => !switches.includes(token))) { + throw new Error('MSBuild linker tracking record has malformed incremental-link switch') + } + if (new Set(switches).size !== switches.length) { + throw new Error('MSBuild linker tracking record has duplicate allowlisted switches') + } + for (const required of ['/brepro', '/experimental:deterministic', '/guard:cf']) { + if (!switches.includes(required)) { + throw new Error(`MSBuild linker tracking record is missing ${required}`) + } + } + const incrementalSwitches = switches.filter((value) => value.startsWith('/incremental')) + if (incrementalSwitches.length > 1) { + throw new Error('MSBuild linker tracking record has ambiguous incremental-link switches') + } + const incremental = + incrementalSwitches.length === 0 + ? 'unspecified' + : incrementalSwitches[0] === '/incremental:no' + ? 'disabled' + : 'enabled' + return { + bytes: bytes.length, + commandRecords, + encoding, + incremental, + switches: switches.toSorted() + } +} + +export async function inspectWindowsNodePtyLinkCommandTracking(path) { + return parseWindowsNodePtyLinkCommandTracking(await readBoundedLinkCommandTracking(path)) } diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs index 82a376e563d..0d440eec041 100644 --- a/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs @@ -7,8 +7,9 @@ import { afterEach, describe, expect, it } from 'vitest' import { applyWindowsNodePtyBuildDeterminism, assertWindowsNodePtyGeneratedBuildSettings, - parseWindowsNodePtyLinkIncremental, - windowsNodePtyLinkIncrementalCommand + inspectWindowsNodePtyLinkCommandTracking, + parseWindowsNodePtyLinkCommandTracking, + windowsNodePtyLinkCommandTrackingPath } from './ssh-relay-node-pty-windows-build-determinism.mjs' const temporaryDirectories = [] @@ -88,42 +89,105 @@ describe('SSH relay Windows node-pty build determinism', () => { }) }) - it('evaluates the effective Release incremental-link state with strict output parsing', () => { + it('parses one bounded UTF-16 linker command into an allowlisted switch summary', () => { expect( - windowsNodePtyLinkIncrementalCommand({ + windowsNodePtyLinkCommandTrackingPath({ nodePtyDirectory: 'C:\\artifact-node-pty', tuple: 'win32-arm64' }) + ).toBe( + join( + 'C:\\artifact-node-pty', + 'build', + 'Release', + 'obj', + 'conpty_console_list', + 'conpty_console_list.tlog', + 'link.command.1.tlog' + ) + ) + const command = [ + '^C:\\artifact-node-pty\\build\\Release\\obj\\conpty_console_list.obj', + '/OUT:conpty_console_list.node /INCREMENTAL:NO /GUARD:CF /DEBUG:FULL', + '/OPT:REF /OPT:ICF /Brepro /experimental:deterministic' + ].join('\r\n') + expect( + parseWindowsNodePtyLinkCommandTracking(Buffer.from(`\ufeff${command}`, 'utf16le')) ).toEqual({ - command: 'MSBuild.exe', - args: [ - join('C:\\artifact-node-pty', 'build', 'conpty_console_list.vcxproj'), - '-nologo', - '-verbosity:quiet', - '-property:Configuration=Release', - '-property:Platform=ARM64', - '-getProperty:LinkIncremental' + bytes: Buffer.byteLength(`\ufeff${command}`, 'utf16le'), + commandRecords: 1, + encoding: 'utf16le', + incremental: 'disabled', + switches: [ + '/brepro', + '/debug:full', + '/experimental:deterministic', + '/guard:cf', + '/incremental:no', + '/opt:icf', + '/opt:ref' ] }) - expect(parseWindowsNodePtyLinkIncremental('TRUE\r\n')).toBe(true) - expect(parseWindowsNodePtyLinkIncremental('false\n')).toBe(false) - expect(() => parseWindowsNodePtyLinkIncremental('true\nwarning')).toThrow( - 'unexpected LinkIncremental evaluation' - ) expect( - windowsNodePtyLinkIncrementalCommand({ - nodePtyDirectory: 'C:\\artifact-node-pty', - tuple: 'win32-x64' - }).args - ).toContain('-property:Platform=x64') - expect( - windowsNodePtyLinkIncrementalCommand({ + windowsNodePtyLinkCommandTrackingPath({ nodePtyDirectory: '/not-used-on-posix', tuple: 'linux-arm64-glibc' }) ).toBeUndefined() }) + it('rejects oversized, malformed, duplicate, or ambiguous linker tracking input', () => { + expect(() => parseWindowsNodePtyLinkCommandTracking(Buffer.alloc(300_000))).toThrow( + 'outside the bounded size' + ) + expect(() => + parseWindowsNodePtyLinkCommandTracking(Buffer.from([0xff, 0xfe, 0x00, 0xd8])) + ).toThrow('invalid text encoding') + expect(() => + parseWindowsNodePtyLinkCommandTracking( + Buffer.from('^first\n/INCREMENTAL\n^second\n/INCREMENTAL', 'utf8') + ) + ).toThrow('exactly one command record') + expect(() => + parseWindowsNodePtyLinkCommandTracking( + Buffer.from( + '^one\n/INCREMENTAL /INCREMENTAL:NO /Brepro /GUARD:CF /experimental:deterministic', + 'utf8' + ) + ) + ).toThrow('ambiguous incremental-link switches') + expect(() => + parseWindowsNodePtyLinkCommandTracking( + Buffer.from('^one\n/INCREMENTAL:MAYBE /Brepro /GUARD:CF /experimental:deterministic') + ) + ).toThrow('malformed incremental-link switch') + expect(() => + parseWindowsNodePtyLinkCommandTracking( + Buffer.from('^one\n/Brepro /BREPRO /GUARD:CF /experimental:deterministic') + ) + ).toThrow('duplicate allowlisted switches') + expect(() => + parseWindowsNodePtyLinkCommandTracking(Buffer.from('^one\n/Brepro /GUARD:CF', 'utf8')) + ).toThrow('missing /experimental:deterministic') + }) + + it('bounds linker tracking bytes while reading the generated file', async () => { + const directory = await mkdtemp(join(tmpdir(), 'orca-node-pty-link-tracking-')) + temporaryDirectories.push(directory) + const trackingPath = join(directory, 'link.command.1.tlog') + await writeFile( + trackingPath, + '^one\n/INCREMENTAL:NO /Brepro /GUARD:CF /experimental:deterministic' + ) + await expect(inspectWindowsNodePtyLinkCommandTracking(trackingPath)).resolves.toMatchObject({ + incremental: 'disabled' + }) + await writeFile(trackingPath, Buffer.alloc(300_000)) + await expect(inspectWindowsNodePtyLinkCommandTracking(trackingPath)).rejects.toThrow( + 'outside the bounded size' + ) + }) + it('rejects missing, duplicate, non-inherited, or wrong-architecture generated settings', async () => { for (const fixture of [ { compilerOptions: '/Brepro %(AdditionalOptions)' }, 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 dad31cbdb46..4aaf408a73b 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29358223742 proves the lowercase-arm64 generated-setting correction on the real runner, keeps Linux x64/arm64, macOS x64/arm64, and Windows x64 exactly reproducible, and uploads exactly those five unpublished artifacts; Windows arm64 builds, verifies, and executes both complete runtimes, then fails closed with no upload because 2,879 linker-emitted 16-byte function thunks differ only in their unreachable fourth `udf` instruction while each thunk's `adrp`/`add`/`br` control flow is byte-identical; the remaining 68 bytes are derived `/Brepro` COFF/debug identities; exact implementation commit `36a19f525` adds a bounded pre-staging MSBuild evaluation of the effective Release `LinkIncremental` state and is locally green under E-M3-WINDOWS-LINK-INCREMENTAL-DIAGNOSTIC-LOCAL-001; all six native cells remain required before any producer correction; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29358223742 isolates 2,879 Windows arm64 linker-emitted 16-byte function thunks whose `adrp`/`add`/`br` control flow is byte-identical and whose unreachable fourth `udf` instruction differs, plus 68 derived `/Brepro` identity bytes; exact-head run 29359948742 then proves MSBuild evaluates Release `LinkIncremental` as empty on both native Windows architectures, so the diagnostic fails closed before staging while all four POSIX controls remain reproducible and upload; this disproves the assumed boolean property oracle and does not authorize `/INCREMENTAL:NO`; the replacement post-build linker-command tracking gate is locally bounded and green under E-M3-WINDOWS-LINK-COMMAND-TRACKING-LOCAL-001, and the next exact-head run must prove its native record shape and allowlisted switch summary before any producer correction; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -169,11 +169,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: push exact implementation commit `36a19f525` plus its evidence-ledger head - and rerun all six target-native cells. Require both Windows clean builds to report the same - evaluated `LinkIncremental` state before considering one copied-artifact producer correction. - Preserve all five controls, strict comparison, rejected-arm64 no-upload, the repository-wide - node-pty patch, legacy/default path, and every other release gate. +- Next required action: replace the empty-property oracle with a bounded parser for the actual + post-build MSBuild linker-command tracking record, then rerun all six target-native cells. Require + both Windows clean builds to report the same allowlisted linker-switch summary before considering + one copied-artifact producer correction. Preserve strict comparison, no rejected upload, the + repository-wide node-pty patch, legacy/default path, and every other release gate. ## Non-Negotiable Invariants @@ -690,14 +690,21 @@ disassembly proves each 16-byte function thunk has byte-identical `adrp x16`, `a `br x16` instructions; only the unreachable fourth instruction changes from `udf #0x24d` to `udf #0x16d` (E-M3-WINDOWS-ARM64-THUNK-DISASSEMBLY-CI-RED-001). -**In progress — 2026-07-14, Codex implementation owner:** classify the linker-owned thunk producer -before changing it. Add a bounded generated-project diagnostic for the exact Release -`LinkIncremental` state while retaining the existing architecture, option, cardinality, strict -comparison, and no-upload gates. The thunk layout is consistent with an incremental-link table, but -that setting is not yet proved on the native runner. Exact implementation commit `36a19f525` is -locally red/green and passes the complete artifact/static gate under -E-M3-WINDOWS-LINK-INCREMENTAL-DIAGNOSTIC-LOCAL-001. Do not normalize rejected bytes, weaken the -comparator, modify repository-wide node-pty, or connect a production consumer. +Exact-head run 29359948742 proves `MSBuild.exe -getProperty:LinkIncremental` returns an empty value +after each native Windows build. The strict parser rejects that value before staging on x64 and +arm64; all four POSIX controls compare exactly and upload, and neither Windows job uploads. This is +valid negative evidence under E-M3-WINDOWS-LINK-INCREMENTAL-CI-RED-001, but an unset evaluated +property does not classify the actual linker command or authorize a producer change. + +**In progress — 2026-07-14, Codex implementation owner:** the disproved property oracle is replaced +locally by a bounded, post-build parser for the target's actual MSBuild linker-command tracking +record under E-M3-WINDOWS-LINK-COMMAND-TRACKING-LOCAL-001. The file read is capped at 256 KiB plus +one rejection byte; logs contain only an allowlisted summary of `/INCREMENTAL`, `/GUARD`, `/DEBUG`, +`/OPT`, `/Brepro`, and `/experimental:deterministic` switches; missing, duplicate, oversized, +malformed, or ambiguous inputs fail before staging. The active gate is the exact-head six-cell +native run. Retain the existing architecture, generated-option, strict comparison, and no-upload +gates. Do not normalize bytes, change the producer, modify repository-wide node-pty, or connect a +production consumer. Each runtime must contain only the executable closure required by the relay. @@ -1302,16 +1309,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; arm64 linker-thunk producer diagnostic active | `e1a63930b`; E-M3-WINDOWS-ARM64-THUNK-DISASSEMBLY-CI-RED-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 linker-command tracking gate pending | `e1a63930b`; E-M3-WINDOWS-LINK-COMMAND-TRACKING-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 | — | Every PR must document: @@ -5096,23 +5103,176 @@ configuration` before runtime staging, verification, smoke, comparison, or diagn - Follow-up: push the exact implementation and ledger head, run all six target-native cells, and require both Windows clean builds to report `linkIncremental` before changing the producer. +### E-M3-WINDOWS-LINK-INCREMENTAL-CI-RED-001 — Native property oracle is empty and fails closed + +- Date: 2026-07-14 +- Commit SHA / PR: exact head `2d4433bbd0d2481d3f91c56d10155989cd6e7e44`, containing exact + diagnostic implementation commit `36a19f525cc63e488f49daa5c3a6aafc590acc08`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Run and jobs: [run 29359948742](https://github.com/stablyai/orca/actions/runs/29359948742), + conclusion `failure`; macOS x64 `87177167723` success, Linux x64 `87177167732` success, Windows + x64 `87177167740` expected diagnostic failure, Windows arm64 `87177167757` expected diagnostic + failure, macOS arm64 `87177167776` success, and Linux arm64 `87177167801` success +- Runners: GitHub-hosted Windows x64 used `windows-2022` / `win22` `20260706.237.1`, native X64; + Windows arm64 used `windows-11-arm` / `win11-arm64` `20260706.102.1`, native ARM64. Both recorded + Node v24.18.0 and exact source head `2d4433bbd`. The four POSIX cells used the same target-native + labels as E-M3-WINDOWS-ARM64-THUNK-DISASSEMBLY-CI-RED-001. +- Remote and transport: none; target-native artifact assembly, strict pre-staging diagnostic, and + unpublished Actions artifacts only +- Exact evidence commands: + + ```sh + gh run view 29359948742 --repo stablyai/orca \ + --json headSha,status,conclusion,url,createdAt,updatedAt,jobs + gh api repos/stablyai/orca/actions/jobs/87177167740/logs + gh api repos/stablyai/orca/actions/jobs/87177167757/logs + gh api 'repos/stablyai/orca/actions/runs/29359948742/artifacts?per_page=100' + ``` + +- Result: FAIL as the intended diagnostic boundary. Each native Windows job completes the first + copied node-pty build and validates the generated Release compiler/linker option blocks. The + subsequent `MSBuild.exe -getProperty:LinkIncremental` query exits zero but writes only an empty + value. The strict parser rejects it with `unexpected LinkIncremental evaluation from MSBuild` + before runtime staging, archive creation, verification, smoke, comparison, or upload. This + proves the property oracle is not usable and does not prove that incremental linking is enabled + or disabled. +- Uploaded controls: exactly four unpublished seven-day POSIX artifacts and no Windows artifacts: + Linux x64 `8321792649`, 29,280,250 bytes, + `sha256:81b21c5f0670cd0337a9b8c666a154c474f52780d19d745497e25bbe69adcfe3`; Linux arm64 + `8321797660`, 28,211,154 bytes, + `sha256:deca7d86e20442287878c6d17f6d533ba6be626dd7ff765f7cd841986272ffc8`; macOS x64 + `8321944562`, 26,386,865 bytes, + `sha256:045573de326e8f9532ac31d5449f4aed4a201bbf152c98d971aa761c76e5ec37`; and macOS arm64 + `8321811571`, 24,727,700 bytes, + `sha256:8fc66890341f548671ec15d0875186ca5ea128cb1ea959a2a69cacc14b9c0864`. +- Duration and resource metrics: jobs ran 9m08s macOS x64, 3m13s Linux x64, 2m50s Windows x64, + 6m31s Windows arm64, 3m56s macOS arm64, and 3m21s Linux arm64. Both Windows failures occur after + one native build but before runtime smoke, so no new Windows RSS, channel/file count, + cancellation, or fallback metric exists. +- Artifact/log/trace link: run/jobs and the four unpublished POSIX artifacts above; no Windows + runtime or binary was staged or uploaded +- Oracle proved: both native Windows architectures return an empty evaluated Release + `LinkIncremental` property and the bounded diagnostic rejects it before staging. All four POSIX + controls remain reproducible and upload. +- Does not prove: the actual linker command, whether `/INCREMENTAL` was passed, the thunk owner, a + safe producer correction, Windows runtime equality, oldest baselines, native trust, SSH, + publication, transfer, fallback, UI, or any enabled tuple. +- Checklist items satisfied: native negative property-oracle evidence only; no tuple or production + checkbox. +- Follow-up: inspect the actual post-build `link.command.1.tlog` under a byte cap, emit only an + allowlisted switch summary, and retain all existing strict comparison/no-upload controls. + +### E-M3-WINDOWS-LINK-COMMAND-TRACKING-LOCAL-001 — Bounded native linker-command gate + +- Date: 2026-07-14 +- Commit SHA / PR: implementation and ledger are in the current artifact-only checkpoint; record + the exact implementation SHA in the immediate documentation evidence head after commit; stacked + draft PR [#8741](https://github.com/stablyai/orca/pull/8741), target-native execution pending +- Runner: macOS 26.2 build 25C56, native Apple M4 arm64; Node v26.0.0 and pnpm 10.24.0. This runner + cannot generate the native MSBuild tracking file, so Windows x64/arm64 jobs remain authoritative. +- Remote and transport: none; bounded local file/parser and builder-ordering contracts only +- Exact red commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs + /usr/bin/time -lp pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + ``` + +- Red result: the pre-implementation purpose suite failed two and passed four tests because the + tracking-path/parser contracts did not exist. The later bounded-reader audit deliberately added + malformed UTF-16 input and failed one of eleven tests because `Buffer.toString('utf16le')` + preserved a lone surrogate instead of rejecting it. Fatal `TextDecoder` decoding closed that + gap; the audit red run took 0.99 s wall with 131,973,120-byte maximum RSS and + 96,196,032-byte peak memory footprint. +- Exact green commands: + + ```sh + node --check config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs + node --check config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs + node --check config/scripts/ssh-relay-node-pty-build.mjs + /usr/bin/time -lp pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + /usr/bin/time -lp 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 \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-build.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs + pnpm run typecheck + pnpm exec oxlint \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.mjs + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.mjs \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md + git diff --check + ``` + +- Green result: PASS. The purpose suites pass 11/11 tests and all 15 artifact suites pass 56/56. + Syntax checks, typecheck, focused oxlint, the 355-entry max-lines ratchet, full repository + lint/reliability/localization, formatting, and diff checks exit zero. Full lint emits only + pre-existing warnings outside this artifact package. +- Duration and resource metrics: purpose gate 0.89 s wall / 166 ms Vitest with 132,513,792-byte + maximum RSS and 96,720,296-byte peak footprint; complete artifact gate 1.35 s wall / 550 ms Vitest + with 137,068,544-byte maximum RSS and 95,819,104-byte peak footprint; typecheck 3.10 s, focused + lint 0.34 s, max-lines 2.01 s, full lint 10.89 s, and format 2.24 s. No SSH channels/files, + cancellation, fallback, or runtime latency is exercised by this diagnostic-only package. +- Artifact/log/trace link: current exact source/tests and local command output; no runtime artifact + was created, staged, published, or uploaded +- Oracle proved: the builder locates the target-specific post-build + `conpty_console_list.tlog/link.command.1.tlog`, reads at most 256 KiB plus one rejection byte, + accepts fatal UTF-16LE-BOM or UTF-8 decoding, requires exactly one command record and one each of + `/Brepro`, `/guard:cf`, and `/experimental:deterministic`, classifies incremental linking as + enabled/disabled/unspecified, and emits only the allowlisted switch summary. Oversize, malformed + encoding/switches, duplicate records/switches, ambiguous incremental states, and missing required + switches fail before runtime staging. POSIX paths create no tracking-file lookup. +- Does not prove: the file path/encoding/record shape on either native Windows runner, the emitted + switch summary, the incremental-link state, thunk ownership, a safe producer correction, arm64 + equality, oldest baselines, native trust, SSH, publication, transfer, fallback, UI, or any enabled + tuple. +- Checklist items satisfied: local bounded post-build tracking diagnostic only; no tuple or + production checkbox. +- Follow-up: push the exact implementation and ledger head, run all six target-native cells, and + require both Windows clean builds to emit the same accepted summary before changing the producer. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit owner and promotion condition. -| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | -| ------------------------------------------ | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | -| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | -| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | -| Native clean-rebuild identity unproved | Five native cells pass; arm64 builds and executes twice but strict comparison rejects linker-thunk drift | Toolchain drift may change native content IDs | Codex implementation owner | Prove the generated incremental-link state, apply only an evidenced producer correction, then make all six clean builds match without weakening the oracle | Open | -| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | -| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | -| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | -| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | -| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | -| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | +| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | +| ------------------------------------------ | ------------------------------------------------------------------------ | ---------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | +| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | +| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | +| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | +| Native clean-rebuild identity unproved | Five native cells pass; bounded linker-command gate awaits native output | Toolchain drift may change native content IDs | Codex implementation owner | Prove the actual linker-command state, apply only an evidenced producer correction, then make all six clean builds match without weakening the oracle | Open | +| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | +| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | +| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | +| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | +| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | +| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | ## Final Definition of Done @@ -5153,11 +5313,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push exact implementation commit `36a19f525` plus its evidence-ledger head, then rerun both Windows -architectures and all four POSIX controls. Require both Windows clean builds to report the same -evaluated `LinkIncremental` setting. Only if that target-native evidence classifies the 2,879 -function thunks may one copied-artifact producer correction be considered; retain strict -comparison, all five controls, and rejected-arm64 no-upload. +Commit and push the bounded, purpose-tested post-build MSBuild linker-command tracking gate, then +rerun both Windows architectures and all four POSIX controls at the exact head. Require both +Windows clean builds to report the same allowlisted switch summary. Only if that target-native +evidence classifies the 2,879 function thunks may one copied-artifact producer correction be +considered; retain strict comparison and rejected-output no-upload. 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 publication, desktop resolver, SSH transfer/install, per-target From dafca2060f2daf4ba27b54a0eb18bb9700136f5b Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 15:23:58 -0400 Subject: [PATCH 045/267] docs(ssh): record linker tracking gate evidence Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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 4aaf408a73b..9511863372f 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29358223742 isolates 2,879 Windows arm64 linker-emitted 16-byte function thunks whose `adrp`/`add`/`br` control flow is byte-identical and whose unreachable fourth `udf` instruction differs, plus 68 derived `/Brepro` identity bytes; exact-head run 29359948742 then proves MSBuild evaluates Release `LinkIncremental` as empty on both native Windows architectures, so the diagnostic fails closed before staging while all four POSIX controls remain reproducible and upload; this disproves the assumed boolean property oracle and does not authorize `/INCREMENTAL:NO`; the replacement post-build linker-command tracking gate is locally bounded and green under E-M3-WINDOWS-LINK-COMMAND-TRACKING-LOCAL-001, and the next exact-head run must prove its native record shape and allowlisted switch summary before any producer correction; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29358223742 isolates 2,879 Windows arm64 linker-emitted 16-byte function thunks whose `adrp`/`add`/`br` control flow is byte-identical and whose unreachable fourth `udf` instruction differs, plus 68 derived `/Brepro` identity bytes; exact-head run 29359948742 then proves MSBuild evaluates Release `LinkIncremental` as empty on both native Windows architectures, so the diagnostic fails closed before staging while all four POSIX controls remain reproducible and upload; this disproves the assumed boolean property oracle and does not authorize `/INCREMENTAL:NO`; exact implementation commit `4a66435b7` replaces it with a locally bounded post-build linker-command tracking gate under E-M3-WINDOWS-LINK-COMMAND-TRACKING-LOCAL-001, and the next exact-head run must prove its native record shape and allowlisted switch summary before any producer correction; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -1313,7 +1313,7 @@ Update status and evidence as work begins. Do not combine these into one large b | ------------------------- | ------------------------------------------------------------------------------------------ | --------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------- | | 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 linker-command tracking gate pending | `e1a63930b`; E-M3-WINDOWS-LINK-COMMAND-TRACKING-LOCAL-001 | +| 2. Runtime builds | Per-tuple assembly, native smoke, SBOM/provenance/signing | None | Draft PR #8741; native linker-command tracking gate pending | `4a66435b7`; E-M3-WINDOWS-LINK-COMMAND-TRACKING-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 | — | @@ -5165,9 +5165,9 @@ configuration` before runtime staging, verification, smoke, comparison, or diagn ### E-M3-WINDOWS-LINK-COMMAND-TRACKING-LOCAL-001 — Bounded native linker-command gate - Date: 2026-07-14 -- Commit SHA / PR: implementation and ledger are in the current artifact-only checkpoint; record - the exact implementation SHA in the immediate documentation evidence head after commit; stacked - draft PR [#8741](https://github.com/stablyai/orca/pull/8741), target-native execution pending +- Commit SHA / PR: exact implementation commit + `4a66435b71b158624645c614543d139a9ea45d51`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741), target-native execution pending - Runner: macOS 26.2 build 25C56, native Apple M4 arm64; Node v26.0.0 and pnpm 10.24.0. This runner cannot generate the native MSBuild tracking file, so Windows x64/arm64 jobs remain authoritative. - Remote and transport: none; bounded local file/parser and builder-ordering contracts only @@ -5313,11 +5313,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the bounded, purpose-tested post-build MSBuild linker-command tracking gate, then -rerun both Windows architectures and all four POSIX controls at the exact head. Require both -Windows clean builds to report the same allowlisted switch summary. Only if that target-native -evidence classifies the 2,879 function thunks may one copied-artifact producer correction be -considered; retain strict comparison and rejected-output no-upload. +Push exact implementation commit `4a66435b7` plus its evidence-ledger head, then rerun both Windows +architectures and all four POSIX controls at the exact head. Require both Windows clean builds to +report the same allowlisted switch summary. Only if that target-native evidence classifies the +2,879 function thunks may one copied-artifact producer correction be considered; retain strict +comparison and rejected-output no-upload. 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 publication, desktop resolver, SSH transfer/install, per-target From 6198f0ddd5b31704ab72902e9a21e78412da870d Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 15:38:24 -0400 Subject: [PATCH 046/267] build(ssh): discover native linker tracking record Co-authored-by: Orca --- config/scripts/ssh-relay-node-pty-build.mjs | 8 +- ...lay-node-pty-windows-build-determinism.mjs | 78 +++++-- ...ode-pty-windows-build-determinism.test.mjs | 117 +++++++--- ...github-release-implementation-checklist.md | 209 +++++++++++++++--- 4 files changed, 330 insertions(+), 82 deletions(-) diff --git a/config/scripts/ssh-relay-node-pty-build.mjs b/config/scripts/ssh-relay-node-pty-build.mjs index 59f0b06f369..f41e1c04b19 100644 --- a/config/scripts/ssh-relay-node-pty-build.mjs +++ b/config/scripts/ssh-relay-node-pty-build.mjs @@ -8,8 +8,7 @@ import { applyWindowsNodePtySettlement } from './ssh-relay-node-pty-windows-sett import { applyWindowsNodePtyBuildDeterminism, assertWindowsNodePtyGeneratedBuildSettings, - inspectWindowsNodePtyLinkCommandTracking, - windowsNodePtyLinkCommandTrackingPath + inspectWindowsNodePtyLinkCommandTracking } from './ssh-relay-node-pty-windows-build-determinism.mjs' const require = createRequire(import.meta.url) @@ -194,12 +193,11 @@ export async function buildPatchedSshRelayNodePty({ tuple }) if (generatedSettings) { - const trackingPath = windowsNodePtyLinkCommandTrackingPath({ + // Why: MSBuild tracking layout differs by runner, so selection is bounded by target content. + const linkCommand = await inspectWindowsNodePtyLinkCommandTracking({ nodePtyDirectory: buildDirectory, tuple }) - // Why: an unset property cannot classify ARM64 thunks; the post-build command is authoritative. - const linkCommand = await inspectWindowsNodePtyLinkCommandTracking(trackingPath) process.stdout.write( `windows_node_pty_msbuild_settings=${JSON.stringify({ ...generatedSettings, diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs index 1df9fca9a5e..1bb093f59f3 100644 --- a/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs @@ -1,4 +1,4 @@ -import { open, readFile, writeFile } from 'node:fs/promises' +import { open, readFile, readdir, writeFile } from 'node:fs/promises' import { join } from 'node:path' const COMPILER_OPTIONS = ` 'VCCLCompilerTool': { @@ -43,6 +43,10 @@ const REPRODUCIBLE_LINKER_OPTIONS = ` 'VCLinkerTool': { const REQUIRED_GENERATED_OPTIONS = ['/Brepro', '/experimental:deterministic'] const MAX_LINK_COMMAND_TRACKING_BYTES = 256 * 1024 +const MAX_LINK_COMMAND_TRACKING_CANDIDATES = 32 +const MAX_LINK_COMMAND_TRACKING_DEPTH = 8 +const MAX_LINK_COMMAND_TRACKING_ENTRIES = 10_000 +const TARGET_LINK_OUTPUT_PATTERN = /(?:^|[\\/:"'\s])conpty_console_list\.node(?=$|[\\/:"'\s])/i function decodeXmlText(value) { return value @@ -127,21 +131,6 @@ export async function assertWindowsNodePtyGeneratedBuildSettings({ nodePtyDirect } } -export function windowsNodePtyLinkCommandTrackingPath({ nodePtyDirectory, tuple }) { - if (!tuple.startsWith('win32-')) { - return undefined - } - return join( - nodePtyDirectory, - 'build', - 'Release', - 'obj', - 'conpty_console_list', - 'conpty_console_list.tlog', - 'link.command.1.tlog' - ) -} - async function readBoundedLinkCommandTracking(path) { const handle = await open(path, 'r') try { @@ -161,6 +150,39 @@ async function readBoundedLinkCommandTracking(path) { } } +async function discoverLinkCommandTracking(nodePtyDirectory) { + const queue = [{ depth: 0, path: join(nodePtyDirectory, 'build') }] + const candidates = [] + let entries = 0 + for (let index = 0; index < queue.length; index += 1) { + const current = queue[index] + const children = await readdir(current.path, { withFileTypes: true }) + children.sort((left, right) => left.name.localeCompare(right.name)) + for (const child of children) { + entries += 1 + if (entries > MAX_LINK_COMMAND_TRACKING_ENTRIES) { + throw new Error('MSBuild linker tracking discovery exceeds the bounded entry count') + } + if (child.isSymbolicLink()) { + throw new Error('MSBuild linker tracking discovery rejects symbolic links') + } + const childPath = join(current.path, child.name) + if (child.isDirectory()) { + if (current.depth >= MAX_LINK_COMMAND_TRACKING_DEPTH) { + throw new Error('MSBuild linker tracking discovery exceeds the bounded depth') + } + queue.push({ depth: current.depth + 1, path: childPath }) + } else if (child.isFile() && child.name.toLowerCase() === 'link.command.1.tlog') { + candidates.push(childPath) + if (candidates.length > MAX_LINK_COMMAND_TRACKING_CANDIDATES) { + throw new Error('MSBuild linker tracking discovery has too many candidates') + } + } + } + } + return { candidates, entries } +} + function decodeLinkCommandTracking(bytes) { if (bytes.length < 2 || bytes.length > MAX_LINK_COMMAND_TRACKING_BYTES) { throw new Error('MSBuild linker tracking record is outside the bounded size') @@ -230,6 +252,26 @@ export function parseWindowsNodePtyLinkCommandTracking(bytes) { } } -export async function inspectWindowsNodePtyLinkCommandTracking(path) { - return parseWindowsNodePtyLinkCommandTracking(await readBoundedLinkCommandTracking(path)) +export async function inspectWindowsNodePtyLinkCommandTracking({ nodePtyDirectory, tuple }) { + if (!tuple.startsWith('win32-')) { + return undefined + } + const discovery = await discoverLinkCommandTracking(nodePtyDirectory) + const matching = [] + for (const path of discovery.candidates) { + const bytes = await readBoundedLinkCommandTracking(path) + if (TARGET_LINK_OUTPUT_PATTERN.test(decodeLinkCommandTracking(bytes).text)) { + matching.push(bytes) + } + } + if (matching.length !== 1) { + throw new Error( + `MSBuild linker tracking discovery requires exactly one target command file (candidates=${discovery.candidates.length}, matches=${matching.length}, entries=${discovery.entries})` + ) + } + return { + ...parseWindowsNodePtyLinkCommandTracking(matching[0]), + candidateFiles: discovery.candidates.length, + searchedEntries: discovery.entries + } } diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs index 0d440eec041..6e9cdc48b12 100644 --- a/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs @@ -8,8 +8,7 @@ import { applyWindowsNodePtyBuildDeterminism, assertWindowsNodePtyGeneratedBuildSettings, inspectWindowsNodePtyLinkCommandTracking, - parseWindowsNodePtyLinkCommandTracking, - windowsNodePtyLinkCommandTrackingPath + parseWindowsNodePtyLinkCommandTracking } from './ssh-relay-node-pty-windows-build-determinism.mjs' const temporaryDirectories = [] @@ -90,22 +89,6 @@ describe('SSH relay Windows node-pty build determinism', () => { }) it('parses one bounded UTF-16 linker command into an allowlisted switch summary', () => { - expect( - windowsNodePtyLinkCommandTrackingPath({ - nodePtyDirectory: 'C:\\artifact-node-pty', - tuple: 'win32-arm64' - }) - ).toBe( - join( - 'C:\\artifact-node-pty', - 'build', - 'Release', - 'obj', - 'conpty_console_list', - 'conpty_console_list.tlog', - 'link.command.1.tlog' - ) - ) const command = [ '^C:\\artifact-node-pty\\build\\Release\\obj\\conpty_console_list.obj', '/OUT:conpty_console_list.node /INCREMENTAL:NO /GUARD:CF /DEBUG:FULL', @@ -128,12 +111,6 @@ describe('SSH relay Windows node-pty build determinism', () => { '/opt:ref' ] }) - expect( - windowsNodePtyLinkCommandTrackingPath({ - nodePtyDirectory: '/not-used-on-posix', - tuple: 'linux-arm64-glibc' - }) - ).toBeUndefined() }) it('rejects oversized, malformed, duplicate, or ambiguous linker tracking input', () => { @@ -174,18 +151,96 @@ describe('SSH relay Windows node-pty build determinism', () => { it('bounds linker tracking bytes while reading the generated file', async () => { const directory = await mkdtemp(join(tmpdir(), 'orca-node-pty-link-tracking-')) temporaryDirectories.push(directory) - const trackingPath = join(directory, 'link.command.1.tlog') + const trackingPath = join( + directory, + 'build', + 'unexpected', + 'target.tlog', + 'link.command.1.tlog' + ) + const otherPath = join(directory, 'build', 'other', 'link.command.1.tlog') + await mkdir(join(directory, 'build', 'unexpected', 'target.tlog'), { recursive: true }) + await mkdir(join(directory, 'build', 'other'), { recursive: true }) await writeFile( trackingPath, - '^one\n/INCREMENTAL:NO /Brepro /GUARD:CF /experimental:deterministic' + '^one\n/OUT:conpty_console_list.node /INCREMENTAL:NO /Brepro /GUARD:CF /experimental:deterministic' ) - await expect(inspectWindowsNodePtyLinkCommandTracking(trackingPath)).resolves.toMatchObject({ - incremental: 'disabled' - }) + await writeFile(otherPath, '^other\n/OUT:not_conpty_console_list.node /INCREMENTAL:NO') + await expect( + inspectWindowsNodePtyLinkCommandTracking({ + nodePtyDirectory: directory, + tuple: 'win32-x64' + }) + ).resolves.toMatchObject({ candidateFiles: 2, incremental: 'disabled' }) + await expect( + inspectWindowsNodePtyLinkCommandTracking({ + nodePtyDirectory: '/not-used-on-posix', + tuple: 'linux-arm64-glibc' + }) + ).resolves.toBeUndefined() await writeFile(trackingPath, Buffer.alloc(300_000)) - await expect(inspectWindowsNodePtyLinkCommandTracking(trackingPath)).rejects.toThrow( - 'outside the bounded size' + await expect( + inspectWindowsNodePtyLinkCommandTracking({ + nodePtyDirectory: directory, + tuple: 'win32-x64' + }) + ).rejects.toThrow('outside the bounded size') + }) + + it('rejects missing or duplicate target linker tracking candidates', async () => { + const directory = await mkdtemp(join(tmpdir(), 'orca-node-pty-link-tracking-')) + temporaryDirectories.push(directory) + await mkdir(join(directory, 'build', 'first'), { recursive: true }) + await expect( + inspectWindowsNodePtyLinkCommandTracking({ + nodePtyDirectory: directory, + tuple: 'win32-arm64' + }) + ).rejects.toThrow('exactly one target command file') + const command = + '^one\n/OUT:conpty_console_list.node /Brepro /GUARD:CF /experimental:deterministic' + await writeFile(join(directory, 'build', 'first', 'link.command.1.tlog'), command) + await mkdir(join(directory, 'build', 'second'), { recursive: true }) + await writeFile(join(directory, 'build', 'second', 'link.command.1.tlog'), command) + await expect( + inspectWindowsNodePtyLinkCommandTracking({ + nodePtyDirectory: directory, + tuple: 'win32-arm64' + }) + ).rejects.toThrow('exactly one target command file') + }) + + it('rejects tracking discovery beyond its depth or candidate budgets', async () => { + const deepDirectory = await mkdtemp(join(tmpdir(), 'orca-node-pty-link-depth-')) + temporaryDirectories.push(deepDirectory) + await mkdir( + join(deepDirectory, 'build', ...Array.from({ length: 10 }, (_, index) => `${index}`)), + { + recursive: true + } ) + await expect( + inspectWindowsNodePtyLinkCommandTracking({ + nodePtyDirectory: deepDirectory, + tuple: 'win32-x64' + }) + ).rejects.toThrow('exceeds the bounded depth') + + const wideDirectory = await mkdtemp(join(tmpdir(), 'orca-node-pty-link-candidates-')) + temporaryDirectories.push(wideDirectory) + await Promise.all( + Array.from({ length: 33 }, async (_, index) => { + const candidateDirectory = join(wideDirectory, 'build', `${index}`) + await mkdir(candidateDirectory, { recursive: true }) + await writeFile(join(candidateDirectory, 'link.command.1.tlog'), '^candidate') + }) + ) + await expect( + inspectWindowsNodePtyLinkCommandTracking({ + nodePtyDirectory: wideDirectory, + tuple: 'win32-arm64' + }) + ).rejects.toThrow('too many candidates') }) it('rejects missing, duplicate, non-inherited, or wrong-architecture generated settings', async () => { 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 9511863372f..dc58549f152 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29358223742 isolates 2,879 Windows arm64 linker-emitted 16-byte function thunks whose `adrp`/`add`/`br` control flow is byte-identical and whose unreachable fourth `udf` instruction differs, plus 68 derived `/Brepro` identity bytes; exact-head run 29359948742 then proves MSBuild evaluates Release `LinkIncremental` as empty on both native Windows architectures, so the diagnostic fails closed before staging while all four POSIX controls remain reproducible and upload; this disproves the assumed boolean property oracle and does not authorize `/INCREMENTAL:NO`; exact implementation commit `4a66435b7` replaces it with a locally bounded post-build linker-command tracking gate under E-M3-WINDOWS-LINK-COMMAND-TRACKING-LOCAL-001, and the next exact-head run must prove its native record shape and allowlisted switch summary before any producer correction; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29358223742 isolates 2,879 Windows arm64 linker-emitted 16-byte function thunks whose `adrp`/`add`/`br` control flow is byte-identical and whose unreachable fourth `udf` instruction differs, plus 68 derived `/Brepro` identity bytes; exact-head run 29359948742 disproves the empty `LinkIncremental` property oracle; exact-head run 29361673339 then proves the locally assumed `link.command.1.tlog` path is absent after successful native compilation on both Windows architectures, so each fails closed before staging/upload while all four POSIX controls remain reproducible and upload; the bounded, target-matching tracking-file discovery correction is locally green under E-M3-WINDOWS-LINK-COMMAND-DISCOVERY-LOCAL-001 and requires a new six-cell exact-head run; no evidence authorizes `/INCREMENTAL:NO` or any producer change; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -706,6 +706,19 @@ native run. Retain the existing architecture, generated-option, strict compariso gates. Do not normalize bytes, change the producer, modify repository-wide node-pty, or connect a production consumer. +Exact-head run 29361673339 proves the assumed fixed tracking path is absent on both native Windows +architectures after the target compiles successfully. The active correction must discover +`link.command.1.tlog` candidates under a bounded build-tree entry/depth/cardinality budget, select +exactly one whose bounded decoded record names `conpty_console_list.node`, and retain the existing +strict parser and no-upload boundary. It must not log command contents or authorize a producer +change (E-M3-WINDOWS-LINK-COMMAND-PATH-CI-RED-001). + +The bounded discovery correction is locally green under +E-M3-WINDOWS-LINK-COMMAND-DISCOVERY-LOCAL-001: it scans at most 10,000 entries and eight levels, +rejects symbolic links and more than 32 tracking candidates, reads each candidate under the +existing 256 KiB cap, and requires exactly one exact target-output match before strict parsing. The +active gate is now a new exact-head native run; no producer change is authorized locally. + Each runtime must contain only the executable closure required by the relay. - [ ] Replace or extend `config/scripts/build-relay.mjs` without weakening its existing relay and @@ -1309,16 +1322,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 linker-command tracking gate pending | `4a66435b7`; E-M3-WINDOWS-LINK-COMMAND-TRACKING-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 target-record discovery pending | E-M3-WINDOWS-LINK-COMMAND-DISCOVERY-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 | — | Every PR must document: @@ -5256,23 +5269,162 @@ configuration` before runtime staging, verification, smoke, comparison, or diagn - Follow-up: push the exact implementation and ledger head, run all six target-native cells, and require both Windows clean builds to emit the same accepted summary before changing the producer. +### E-M3-WINDOWS-LINK-COMMAND-PATH-CI-RED-001 — Native fixed tracking path is absent + +- Date: 2026-07-14 +- Commit SHA / PR: exact head `dafca2060f2daf4ba27b54a0eb18bb9700136f5b`, containing exact + tracking implementation commit `4a66435b71b158624645c614543d139a9ea45d51`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Run and jobs: [run 29361673339](https://github.com/stablyai/orca/actions/runs/29361673339), + conclusion `failure`; macOS arm64 `87183054190` success, Linux arm64 `87183054293` success, + Windows arm64 `87183054295` expected diagnostic failure, macOS x64 `87183054306` success, Linux + x64 `87183054310` success, and Windows x64 `87183054318` expected diagnostic failure +- Runners: GitHub-hosted Windows x64 used `windows-2022` / `win22` `20260706.237.1`, native X64; + Windows arm64 used `windows-11-arm64` / `win11-arm64` `20260706.102.1`, native ARM64. Both used + runner `2.335.1`, Node v24.18.0, MSVC 14.44.35207, and exact source head `dafca2060`. The four + POSIX controls used their previously recorded target-native labels. +- Remote and transport: none; target-native artifact assembly, strict pre-staging diagnostic, and + unpublished Actions artifacts only +- Exact evidence commands: + + ```sh + gh run view 29361673339 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh api repos/stablyai/orca/actions/jobs/87183054318/logs + gh api repos/stablyai/orca/actions/jobs/87183054295/logs + gh api 'repos/stablyai/orca/actions/runs/29361673339/artifacts?per_page=100' + ``` + +- Result: FAIL as the intended diagnostic boundary. Each Windows job successfully compiles + `conpty_console_list.node` for its native architecture, then the bounded reader receives `ENOENT` + for the locally assumed fixed `build/Release/obj/conpty_console_list/conpty_console_list.tlog/` + `link.command.1.tlog` location. Both jobs fail before runtime staging, verification, smoke, + comparison, or upload. This proves only that the fixed path is wrong; it does not show whether a + target-matching tracking file exists elsewhere. +- Uploaded controls: exactly four unpublished seven-day POSIX artifacts and no Windows artifacts: + Linux x64 `8322467354`, 29,282,467 bytes, + `sha256:75a29015224b0dd96e479f94d362be7023af0263373192332488ca4cc30647ae`; Linux arm64 + `8322477826`, 28,215,101 bytes, + `sha256:07e9be64b1ab7fd2a9ebf9a30672bd275c670d5f0c95bd624b933d3bc870515e`; macOS x64 + `8322529916`, 26,372,643 bytes, + `sha256:a0c3fdd314b19b0f371b5ea88eb862dfe335a785506b8011f8fde915f1323b1c`; and macOS arm64 + `8322474835`, 24,749,773 bytes, + `sha256:11ddd1291e7b104edae6e54d18ab44a20a90aa28a29ed3d2bd92e2f3202d215c`. +- Duration and resource metrics: jobs ran 3m25s macOS arm64, 3m27s Linux arm64, 6m40s Windows + arm64, 5m37s macOS x64, 3m06s Linux x64, and 3m00s Windows x64. Both Windows failures occur + after one native compile but before runtime smoke; no new Windows RSS, channel/file count, + cancellation, or fallback metric exists. +- Artifact/log/trace link: run/jobs and the four unpublished POSIX artifacts above; no Windows + runtime or rejected binary was staged or uploaded +- Oracle proved: both native Windows architectures share the same absent fixed tracking path while + the target native build itself completes; all four POSIX controls remain reproducible and upload; + rejected Windows output remains unavailable to consumers. +- Does not prove: the actual tracking-file path, candidate cardinality, command record shape, + allowlisted switch summary, incremental-link state, thunk ownership, a safe producer correction, + Windows runtime equality, oldest baselines, native trust, SSH, publication, transfer, fallback, + UI, or any enabled tuple. +- Checklist items satisfied: native negative path-shape evidence only; no tuple or production + checkbox. +- Follow-up: replace the false fixed-path assumption with bounded build-tree discovery that requires + exactly one target-matching `link.command.1.tlog`, then rerun all six native cells without + weakening parsing, comparison, or no-upload controls. + +### E-M3-WINDOWS-LINK-COMMAND-DISCOVERY-LOCAL-001 — Bounded target-matching discovery + +- Date: 2026-07-14 +- Commit SHA / PR: implementation and ledger are in the current artifact-only checkpoint; record + the exact implementation SHA in the immediate documentation evidence head after commit; stacked + draft PR [#8741](https://github.com/stablyai/orca/pull/8741), target-native execution pending +- Runner: macOS 26.2 build 25C56, native Apple M4 arm64; Node v26.0.0 and pnpm 10.24.0. This runner + cannot generate the native MSBuild tree, so Windows x64/arm64 jobs remain authoritative. +- Remote and transport: none; bounded build-tree discovery, target selection, and builder-ordering + contracts only +- Red evidence: E-M3-WINDOWS-LINK-COMMAND-PATH-CI-RED-001 on both native Windows architectures; + no synthetic local failure is substituted for that real path-shape evidence +- Exact green commands: + + ```sh + node --check config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs + node --check config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs + node --check config/scripts/ssh-relay-node-pty-build.mjs + /usr/bin/time -lp pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.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 \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-build.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs + pnpm run typecheck + pnpm exec oxlint \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.mjs + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.mjs \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md + git diff --check + ``` + +- Green result: PASS. The purpose suites pass 13/13 tests and all 15 artifact suites pass 58/58. + Syntax checks, typecheck, focused oxlint, the 355-entry max-lines ratchet, full repository + lint/reliability/localization, formatting, and diff checks exit zero. Full lint emits only + pre-existing warnings outside this artifact package. +- Duration and resource metrics: focused purpose gate 1.03 s wall / 227 ms Vitest with + 131,825,664-byte maximum RSS and 95,999,352-byte peak footprint; complete artifact gate 2.96 s + wall / 1.36 s Vitest; typecheck 3.08 s, focused lint 0.66 s, max-lines 1.47 s, and full lint + 13.87 s. The final full artifact run was parallel with static gates, so its process RSS was not + isolated. No SSH channels/files, cancellation, fallback, or runtime latency is exercised. +- Artifact/log/trace link: current exact source/tests and local command output; no runtime artifact + was created, staged, published, or uploaded +- Oracle proved: discovery is deterministic and bounded to 10,000 build-tree entries, depth eight, + and 32 exact-name candidates; symbolic links fail closed; each candidate read remains bounded; + target selection requires exactly one token-bounded `conpty_console_list.node` output rather than + a lookalike or path assumption. Missing/duplicate targets, excessive depth/cardinality, oversized + bytes, malformed contents, and all existing strict-parser failures settle before staging. POSIX + performs no discovery. +- Does not prove: that either native Windows tree contains a candidate, candidate cardinality on the + runners, the actual record shape, allowlisted switch summary, incremental-link state, thunk + ownership, a safe producer correction, arm64 equality, oldest baselines, native trust, SSH, + publication, transfer, fallback, UI, or any enabled tuple. +- Checklist items satisfied: local bounded tracking discovery only; no tuple or production checkbox. +- Follow-up: push the exact implementation and ledger head, rerun all six target-native cells, and + require both Windows clean builds to select one target record and emit the same accepted summary + before changing the producer. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit owner and promotion condition. -| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | -| ------------------------------------------ | ------------------------------------------------------------------------ | ---------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | -| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | -| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | -| Native clean-rebuild identity unproved | Five native cells pass; bounded linker-command gate awaits native output | Toolchain drift may change native content IDs | Codex implementation owner | Prove the actual linker-command state, apply only an evidenced producer correction, then make all six clean builds match without weakening the oracle | Open | -| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | -| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | -| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | -| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | -| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | -| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | +| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | +| ------------------------------------------ | -------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------ | +| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | +| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | +| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | +| Native clean-rebuild identity unproved | Four POSIX controls pass; bounded target-record discovery is locally green | Toolchain drift may change native content IDs | Codex implementation owner | Prove the actual target linker command natively, then apply only an evidenced producer correction and make all six clean builds match | Open | +| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | +| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | +| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | +| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | +| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | +| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | ## Final Definition of Done @@ -5313,11 +5465,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push exact implementation commit `4a66435b7` plus its evidence-ledger head, then rerun both Windows -architectures and all four POSIX controls at the exact head. Require both Windows clean builds to -report the same allowlisted switch summary. Only if that target-native evidence classifies the -2,879 function thunks may one copied-artifact producer correction be considered; retain strict -comparison and rejected-output no-upload. +Commit and push the bounded build-tree discovery that requires exactly one `link.command.1.tlog` +whose bounded decoded record names `conpty_console_list.node`, then rerun both Windows architectures +and all four POSIX controls at the exact head. Require both Windows clean builds to report the same +allowlisted switch summary. Only if that target-native evidence classifies the 2,879 function +thunks may one copied-artifact producer correction be considered; retain strict parsing/comparison +and rejected-output no-upload. 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 publication, desktop resolver, SSH transfer/install, per-target From f9a49d53123b27b19713eadef927de1102ec08de Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 15:39:37 -0400 Subject: [PATCH 047/267] docs(ssh): record native tracking discovery gate Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) 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 dc58549f152..91c4b61cb72 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29358223742 isolates 2,879 Windows arm64 linker-emitted 16-byte function thunks whose `adrp`/`add`/`br` control flow is byte-identical and whose unreachable fourth `udf` instruction differs, plus 68 derived `/Brepro` identity bytes; exact-head run 29359948742 disproves the empty `LinkIncremental` property oracle; exact-head run 29361673339 then proves the locally assumed `link.command.1.tlog` path is absent after successful native compilation on both Windows architectures, so each fails closed before staging/upload while all four POSIX controls remain reproducible and upload; the bounded, target-matching tracking-file discovery correction is locally green under E-M3-WINDOWS-LINK-COMMAND-DISCOVERY-LOCAL-001 and requires a new six-cell exact-head run; no evidence authorizes `/INCREMENTAL:NO` or any producer change; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29358223742 isolates 2,879 Windows arm64 linker-emitted 16-byte function thunks whose `adrp`/`add`/`br` control flow is byte-identical and whose unreachable fourth `udf` instruction differs, plus 68 derived `/Brepro` identity bytes; exact-head run 29359948742 disproves the empty `LinkIncremental` property oracle; exact-head run 29361673339 then proves the locally assumed `link.command.1.tlog` path is absent after successful native compilation on both Windows architectures, so each fails closed before staging/upload while all four POSIX controls remain reproducible and upload; exact implementation commit `6198f0ddd` adds bounded, target-matching tracking-file discovery and is locally green under E-M3-WINDOWS-LINK-COMMAND-DISCOVERY-LOCAL-001; the next six-cell exact-head run remains required, and no evidence authorizes `/INCREMENTAL:NO` or any producer change; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -1326,7 +1326,7 @@ Update status and evidence as work begins. Do not combine these into one large b | ------------------------- | ------------------------------------------------------------------------------------------ | --------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------- | | 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 target-record discovery pending | E-M3-WINDOWS-LINK-COMMAND-DISCOVERY-LOCAL-001 | +| 2. Runtime builds | Per-tuple assembly, native smoke, SBOM/provenance/signing | None | Draft PR #8741; native target-record discovery pending | `6198f0ddd`; E-M3-WINDOWS-LINK-COMMAND-DISCOVERY-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 | — | @@ -5332,9 +5332,9 @@ configuration` before runtime staging, verification, smoke, comparison, or diagn ### E-M3-WINDOWS-LINK-COMMAND-DISCOVERY-LOCAL-001 — Bounded target-matching discovery - Date: 2026-07-14 -- Commit SHA / PR: implementation and ledger are in the current artifact-only checkpoint; record - the exact implementation SHA in the immediate documentation evidence head after commit; stacked - draft PR [#8741](https://github.com/stablyai/orca/pull/8741), target-native execution pending +- Commit SHA / PR: exact implementation commit + `6198f0ddd5b31704ab72902e9a21e78412da870d`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741), target-native execution pending - Runner: macOS 26.2 build 25C56, native Apple M4 arm64; Node v26.0.0 and pnpm 10.24.0. This runner cannot generate the native MSBuild tree, so Windows x64/arm64 jobs remain authoritative. - Remote and transport: none; bounded build-tree discovery, target selection, and builder-ordering @@ -5465,12 +5465,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the bounded build-tree discovery that requires exactly one `link.command.1.tlog` -whose bounded decoded record names `conpty_console_list.node`, then rerun both Windows architectures -and all four POSIX controls at the exact head. Require both Windows clean builds to report the same -allowlisted switch summary. Only if that target-native evidence classifies the 2,879 function -thunks may one copied-artifact producer correction be considered; retain strict parsing/comparison -and rejected-output no-upload. +Push exact implementation commit `6198f0ddd` plus its evidence-ledger head, then rerun both Windows +architectures and all four POSIX controls at the exact head. Require both Windows clean builds to +select one target record and report the same allowlisted switch summary. Only if that target-native +evidence classifies the 2,879 function thunks may one copied-artifact producer correction be +considered; retain strict parsing/comparison and rejected-output no-upload. 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 publication, desktop resolver, SSH transfer/install, per-target From 6f28a8cbf53d1cfe889ce259476eb4aaaead2d87 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 15:50:35 -0400 Subject: [PATCH 048/267] build(ssh): scope linker discovery to release output Co-authored-by: Orca --- ...lay-node-pty-windows-build-determinism.mjs | 3 +- ...ode-pty-windows-build-determinism.test.mjs | 26 ++- ...github-release-implementation-checklist.md | 205 +++++++++++++++--- 3 files changed, 196 insertions(+), 38 deletions(-) diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs index 1bb093f59f3..ff48d46764e 100644 --- a/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs @@ -151,7 +151,8 @@ async function readBoundedLinkCommandTracking(path) { } async function discoverLinkCommandTracking(nodePtyDirectory) { - const queue = [{ depth: 0, path: join(nodePtyDirectory, 'build') }] + // Why: generated dependency projects live elsewhere; only Release contains target link outputs. + const queue = [{ depth: 0, path: join(nodePtyDirectory, 'build', 'Release') }] const candidates = [] let entries = 0 for (let index = 0; index < queue.length; index += 1) { diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs index 6e9cdc48b12..34505c908db 100644 --- a/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs @@ -154,13 +154,16 @@ describe('SSH relay Windows node-pty build determinism', () => { const trackingPath = join( directory, 'build', + 'Release', 'unexpected', 'target.tlog', 'link.command.1.tlog' ) - const otherPath = join(directory, 'build', 'other', 'link.command.1.tlog') - await mkdir(join(directory, 'build', 'unexpected', 'target.tlog'), { recursive: true }) - await mkdir(join(directory, 'build', 'other'), { recursive: true }) + const otherPath = join(directory, 'build', 'Release', 'other', 'link.command.1.tlog') + await mkdir(join(directory, 'build', 'Release', 'unexpected', 'target.tlog'), { + recursive: true + }) + await mkdir(join(directory, 'build', 'Release', 'other'), { recursive: true }) await writeFile( trackingPath, '^one\n/OUT:conpty_console_list.node /INCREMENTAL:NO /Brepro /GUARD:CF /experimental:deterministic' @@ -190,7 +193,7 @@ describe('SSH relay Windows node-pty build determinism', () => { it('rejects missing or duplicate target linker tracking candidates', async () => { const directory = await mkdtemp(join(tmpdir(), 'orca-node-pty-link-tracking-')) temporaryDirectories.push(directory) - await mkdir(join(directory, 'build', 'first'), { recursive: true }) + await mkdir(join(directory, 'build', 'Release', 'first'), { recursive: true }) await expect( inspectWindowsNodePtyLinkCommandTracking({ nodePtyDirectory: directory, @@ -199,9 +202,9 @@ describe('SSH relay Windows node-pty build determinism', () => { ).rejects.toThrow('exactly one target command file') const command = '^one\n/OUT:conpty_console_list.node /Brepro /GUARD:CF /experimental:deterministic' - await writeFile(join(directory, 'build', 'first', 'link.command.1.tlog'), command) - await mkdir(join(directory, 'build', 'second'), { recursive: true }) - await writeFile(join(directory, 'build', 'second', 'link.command.1.tlog'), command) + await writeFile(join(directory, 'build', 'Release', 'first', 'link.command.1.tlog'), command) + await mkdir(join(directory, 'build', 'Release', 'second'), { recursive: true }) + await writeFile(join(directory, 'build', 'Release', 'second', 'link.command.1.tlog'), command) await expect( inspectWindowsNodePtyLinkCommandTracking({ nodePtyDirectory: directory, @@ -214,7 +217,12 @@ describe('SSH relay Windows node-pty build determinism', () => { const deepDirectory = await mkdtemp(join(tmpdir(), 'orca-node-pty-link-depth-')) temporaryDirectories.push(deepDirectory) await mkdir( - join(deepDirectory, 'build', ...Array.from({ length: 10 }, (_, index) => `${index}`)), + join( + deepDirectory, + 'build', + 'Release', + ...Array.from({ length: 10 }, (_, index) => `${index}`) + ), { recursive: true } @@ -230,7 +238,7 @@ describe('SSH relay Windows node-pty build determinism', () => { temporaryDirectories.push(wideDirectory) await Promise.all( Array.from({ length: 33 }, async (_, index) => { - const candidateDirectory = join(wideDirectory, 'build', `${index}`) + const candidateDirectory = join(wideDirectory, 'build', 'Release', `${index}`) await mkdir(candidateDirectory, { recursive: true }) await writeFile(join(candidateDirectory, 'link.command.1.tlog'), '^candidate') }) 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 91c4b61cb72..3ee01a6e21b 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29358223742 isolates 2,879 Windows arm64 linker-emitted 16-byte function thunks whose `adrp`/`add`/`br` control flow is byte-identical and whose unreachable fourth `udf` instruction differs, plus 68 derived `/Brepro` identity bytes; exact-head run 29359948742 disproves the empty `LinkIncremental` property oracle; exact-head run 29361673339 then proves the locally assumed `link.command.1.tlog` path is absent after successful native compilation on both Windows architectures, so each fails closed before staging/upload while all four POSIX controls remain reproducible and upload; exact implementation commit `6198f0ddd` adds bounded, target-matching tracking-file discovery and is locally green under E-M3-WINDOWS-LINK-COMMAND-DISCOVERY-LOCAL-001; the next six-cell exact-head run remains required, and no evidence authorizes `/INCREMENTAL:NO` or any producer change; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29358223742 isolates 2,879 Windows arm64 linker-emitted 16-byte function thunks whose `adrp`/`add`/`br` control flow is byte-identical and whose unreachable fourth `udf` instruction differs, plus 68 derived `/Brepro` identity bytes; exact-head run 29359948742 disproves the empty `LinkIncremental` property oracle; exact-head run 29361673339 proves the locally assumed fixed tracking path is absent; exact-head run 29362672415 then proves both native Windows builds fail closed because bounded discovery from the entire generated `build` tree reaches unrelated dependency depth, while all four POSIX controls remain reproducible and upload; the narrow `build/Release` output-tree correction is locally green under E-M3-WINDOWS-LINK-COMMAND-RELEASE-ROOT-LOCAL-001 and requires a new six-cell exact-head run; no evidence authorizes changing a bound, producer, comparator, or default path; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -719,6 +719,19 @@ rejects symbolic links and more than 32 tracking candidates, reads each candidat existing 256 KiB cap, and requires exactly one exact target-output match before strict parsing. The active gate is now a new exact-head native run; no producer change is authorized locally. +Exact-head run 29362672415 proves both native Windows builds reach the discovery gate after target +compilation but reject because scanning from the entire generated `build` tree encounters unrelated +dependency directories beyond depth eight. All four POSIX controls compare and upload. The active +correction may narrow only the traversal root to `build/Release`, the output tree where both logs +show MSBuild emitted `conpty_console_list.node`; every existing depth, entry, candidate, byte, +encoding, target-cardinality, parser, comparator, and no-upload bound remains unchanged +(E-M3-WINDOWS-LINK-COMMAND-DISCOVERY-CI-RED-001). + +The correction now scopes the same bounded discovery to `build/Release` only and is locally green +under E-M3-WINDOWS-LINK-COMMAND-RELEASE-ROOT-LOCAL-001. No budget, parser, producer, comparison, +staging, upload, legacy, or production behavior changed. The active gate is another exact-head +six-cell native run. + Each runtime must contain only the executable closure required by the relay. - [ ] Replace or extend `config/scripts/build-relay.mjs` without weakening its existing relay and @@ -1322,16 +1335,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 target-record discovery pending | `6198f0ddd`; E-M3-WINDOWS-LINK-COMMAND-DISCOVERY-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 Release-tree discovery pending | E-M3-WINDOWS-LINK-COMMAND-RELEASE-ROOT-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 | — | Every PR must document: @@ -5408,23 +5421,158 @@ configuration` before runtime staging, verification, smoke, comparison, or diagn require both Windows clean builds to select one target record and emit the same accepted summary before changing the producer. +### E-M3-WINDOWS-LINK-COMMAND-DISCOVERY-CI-RED-001 — Whole build tree exceeds depth bound + +- Date: 2026-07-14 +- Commit SHA / PR: exact head `f9a49d53123b27b19713eadef927de1102ec08de`, containing exact + discovery implementation commit `6198f0ddd5b31704ab72902e9a21e78412da870d`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Run and jobs: [run 29362672415](https://github.com/stablyai/orca/actions/runs/29362672415), + conclusion `failure`; Windows x64 `87186432938` expected diagnostic failure, Windows arm64 + `87186432948` expected diagnostic failure, macOS arm64 `87186432965` success, Linux arm64 + `87186432979` success, Linux x64 `87186432980` success, and macOS x64 `87186433013` success +- Runners: GitHub-hosted Windows x64 and arm64 used the same native labels, images, runner, Node, + and MSVC versions recorded by E-M3-WINDOWS-LINK-COMMAND-PATH-CI-RED-001; exact source head was + `f9a49d531`. The four POSIX controls used their previously recorded target-native labels. +- Remote and transport: none; target-native artifact assembly, bounded build-tree discovery, and + unpublished Actions artifacts only +- Exact evidence commands: + + ```sh + gh run view 29362672415 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh api repos/stablyai/orca/actions/jobs/87186432938/logs + gh api repos/stablyai/orca/actions/jobs/87186432948/logs + gh api 'repos/stablyai/orca/actions/runs/29362672415/artifacts?per_page=100' + ``` + +- Result: FAIL as the intended discovery boundary. Each Windows job successfully compiles the + target and enters bounded discovery, then rejects with `MSBuild linker tracking discovery exceeds +the bounded depth` before opening or parsing a candidate. Starting at the complete generated + `build` tree includes unrelated dependency/project subtrees; the evidence does not justify + raising the eight-level limit. Both logs show the target binary is emitted under `build/Release`, + which is the narrow output-tree root for the next correction. +- Uploaded controls: exactly four unpublished seven-day POSIX artifacts and no Windows artifacts: + Linux x64 `8322871131`, 29,278,807 bytes, + `sha256:b11ceb6bd42d28d379d32b3627a242f60cf62f78e9a7267589220ce7737b9c63`; Linux arm64 + `8322874205`, 28,209,038 bytes, + `sha256:f2195e1f4c03d1fb6507e32b5d5328a65f351c2c22b56a8144aa1c9449c3f733`; macOS x64 + `8322934998`, 26,370,804 bytes, + `sha256:6fea712fd57b06cc93a7e8f0f3ebda4526307258be66ba901cd3fc190f7a03ed`; and macOS arm64 + `8322879006`, 24,728,020 bytes, + `sha256:2407308e60002c5e735cd301f1f30e9754d5c383a18f36009d14f08085f1f577`. +- Duration and resource metrics: jobs ran 2m58s Windows x64, 6m25s Windows arm64, 3m45s macOS + arm64, 3m25s Linux arm64, 3m22s Linux x64, and 5m56s macOS x64. Windows fails after one native + compile but before runtime smoke; no new Windows RSS, channel/file count, cancellation, or + fallback metric exists. +- Artifact/log/trace link: run/jobs and four unpublished POSIX artifacts above; no Windows runtime + or rejected binary was staged or uploaded +- Oracle proved: both native Windows architectures reach and enforce the same traversal-depth + bound; whole-build-tree discovery is too broad; the target output tree is `build/Release`; all + four POSIX controls remain reproducible and upload. +- Does not prove: whether `build/Release` contains tracking candidates, candidate/target cardinality, + record shape, allowlisted switch summary, incremental-link state, thunk ownership, a safe producer + correction, Windows equality, oldest baselines, native trust, SSH, publication, transfer, + fallback, UI, or any enabled tuple. +- Checklist items satisfied: native negative discovery-root evidence only; no tuple or production + checkbox. +- Follow-up: narrow the discovery root to `build/Release` without raising or removing any bound, + then rerun all six native cells and require exactly one target record on both Windows runners. + +### E-M3-WINDOWS-LINK-COMMAND-RELEASE-ROOT-LOCAL-001 — Release output-tree discovery scope + +- Date: 2026-07-14 +- Commit SHA / PR: implementation and ledger are in the current artifact-only checkpoint; record + the exact implementation SHA in the immediate documentation evidence head after commit; stacked + draft PR [#8741](https://github.com/stablyai/orca/pull/8741), target-native execution pending +- Runner: macOS 26.2 build 25C56, native Apple M4 arm64; Node v26.0.0 and pnpm 10.24.0. This runner + cannot generate the native MSBuild tree, so Windows x64/arm64 jobs remain authoritative. +- Remote and transport: none; bounded discovery-root and builder-ordering contracts only +- Red evidence: E-M3-WINDOWS-LINK-COMMAND-DISCOVERY-CI-RED-001 on both native Windows + architectures; no synthetic local failure substitutes for that real traversal-shape evidence +- Exact green commands: + + ```sh + node --check config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs + node --check config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs + node --check config/scripts/ssh-relay-node-pty-build.mjs + /usr/bin/time -lp pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.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 \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-build.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs + pnpm run typecheck + pnpm exec oxlint \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.mjs + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.mjs \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md + git diff --check + ``` + +- Green result: PASS. The purpose suites pass 13/13 tests and all 15 artifact suites pass 58/58. + Syntax checks, typecheck, focused oxlint, the 355-entry max-lines ratchet, full repository + lint/reliability/localization, formatting, and diff checks exit zero. Full lint emits only + pre-existing warnings outside this package. +- Duration and resource metrics: focused purpose gate 1.26 s wall / 263 ms Vitest with + 131,710,976-byte maximum RSS and 95,868,256-byte peak footprint; complete artifact gate 4.01 s + wall / 1.92 s Vitest; typecheck 4.21 s, focused lint 0.90 s, max-lines 2.37 s, and full lint + 13.78 s. The final artifact run was parallel with static gates, so its process RSS was not + isolated. No SSH channels/files, cancellation, fallback, or runtime latency is exercised. +- Artifact/log/trace link: current exact source/tests and local command output; no runtime artifact + was created, staged, published, or uploaded +- Oracle proved: target discovery begins at `build/Release`, matching the exact native output tree + evidenced by both Windows logs; tests place valid, irrelevant, duplicate, oversized, deep, and + over-cardinality fixtures below that root. The existing entry/depth/candidate/byte, symbolic-link, + encoding, target-cardinality, strict-parser, and POSIX no-op contracts remain unchanged. +- Does not prove: that either native Release tree contains a candidate, candidate/target + cardinality, record shape, allowlisted switch summary, incremental-link state, thunk ownership, a + safe producer correction, arm64 equality, oldest baselines, native trust, SSH, publication, + transfer, fallback, UI, or any enabled tuple. +- Checklist items satisfied: local Release-tree discovery scope only; no tuple or production + checkbox. +- Follow-up: push the exact implementation and ledger head, rerun all six target-native cells, and + require both Windows clean builds to select one target record and emit the same accepted summary + before changing the producer. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit owner and promotion condition. -| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | -| ------------------------------------------ | -------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------ | -| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | -| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | -| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | -| Native clean-rebuild identity unproved | Four POSIX controls pass; bounded target-record discovery is locally green | Toolchain drift may change native content IDs | Codex implementation owner | Prove the actual target linker command natively, then apply only an evidenced producer correction and make all six clean builds match | Open | -| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | -| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | -| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | -| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | -| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | -| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | +| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | +| ------------------------------------------ | ------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------ | +| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | +| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | +| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | +| Native clean-rebuild identity unproved | Four POSIX controls pass; bounded Release-tree discovery is locally green | Toolchain drift may change native content IDs | Codex implementation owner | Prove the actual target linker command natively, then make all six clean builds match | Open | +| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | +| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | +| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | +| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | +| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | +| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | ## Final Definition of Done @@ -5465,11 +5613,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push exact implementation commit `6198f0ddd` plus its evidence-ledger head, then rerun both Windows -architectures and all four POSIX controls at the exact head. Require both Windows clean builds to -select one target record and report the same allowlisted switch summary. Only if that target-native -evidence classifies the 2,879 function thunks may one copied-artifact producer correction be -considered; retain strict parsing/comparison and rejected-output no-upload. +Commit and push the bounded `build/Release` output-tree discovery correction without raising any +entry/depth/candidate/byte bound, then rerun both Windows architectures and all four POSIX controls. +Require both Windows clean builds to select one target record and report the same allowlisted +switch summary. Only if that target-native evidence classifies the 2,879 function thunks may one +copied-artifact producer correction be considered; retain strict parsing/comparison and +rejected-output no-upload. 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 publication, desktop resolver, SSH transfer/install, per-target From 2db46d91b17789ce70eed5e5e496dec2ff56442a Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 15:51:18 -0400 Subject: [PATCH 049/267] docs(ssh): record release-tree discovery gate Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) 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 3ee01a6e21b..df6c74d0a19 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29358223742 isolates 2,879 Windows arm64 linker-emitted 16-byte function thunks whose `adrp`/`add`/`br` control flow is byte-identical and whose unreachable fourth `udf` instruction differs, plus 68 derived `/Brepro` identity bytes; exact-head run 29359948742 disproves the empty `LinkIncremental` property oracle; exact-head run 29361673339 proves the locally assumed fixed tracking path is absent; exact-head run 29362672415 then proves both native Windows builds fail closed because bounded discovery from the entire generated `build` tree reaches unrelated dependency depth, while all four POSIX controls remain reproducible and upload; the narrow `build/Release` output-tree correction is locally green under E-M3-WINDOWS-LINK-COMMAND-RELEASE-ROOT-LOCAL-001 and requires a new six-cell exact-head run; no evidence authorizes changing a bound, producer, comparator, or default path; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29358223742 isolates 2,879 Windows arm64 linker-emitted 16-byte function thunks whose `adrp`/`add`/`br` control flow is byte-identical and whose unreachable fourth `udf` instruction differs, plus 68 derived `/Brepro` identity bytes; exact-head run 29359948742 disproves the empty `LinkIncremental` property oracle; exact-head run 29361673339 proves the locally assumed fixed tracking path is absent; exact-head run 29362672415 then proves both native Windows builds fail closed because bounded discovery from the entire generated `build` tree reaches unrelated dependency depth, while all four POSIX controls remain reproducible and upload; exact implementation commit `6f28a8cbf` narrows discovery to the `build/Release` output tree and is locally green under E-M3-WINDOWS-LINK-COMMAND-RELEASE-ROOT-LOCAL-001; the next six-cell exact-head run remains required, and no evidence authorizes changing a bound, producer, comparator, or default path; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -1339,7 +1339,7 @@ Update status and evidence as work begins. Do not combine these into one large b | ------------------------- | ------------------------------------------------------------------------------------------ | --------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------- | | 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 Release-tree discovery pending | E-M3-WINDOWS-LINK-COMMAND-RELEASE-ROOT-LOCAL-001 | +| 2. Runtime builds | Per-tuple assembly, native smoke, SBOM/provenance/signing | None | Draft PR #8741; native Release-tree discovery pending | `6f28a8cbf`; E-M3-WINDOWS-LINK-COMMAND-RELEASE-ROOT-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 | — | @@ -5482,9 +5482,9 @@ the bounded depth` before opening or parsing a candidate. Starting at the comple ### E-M3-WINDOWS-LINK-COMMAND-RELEASE-ROOT-LOCAL-001 — Release output-tree discovery scope - Date: 2026-07-14 -- Commit SHA / PR: implementation and ledger are in the current artifact-only checkpoint; record - the exact implementation SHA in the immediate documentation evidence head after commit; stacked - draft PR [#8741](https://github.com/stablyai/orca/pull/8741), target-native execution pending +- Commit SHA / PR: exact implementation commit + `6f28a8cbf53d1cfe889ce259476eb4aaaead2d87`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741), target-native execution pending - Runner: macOS 26.2 build 25C56, native Apple M4 arm64; Node v26.0.0 and pnpm 10.24.0. This runner cannot generate the native MSBuild tree, so Windows x64/arm64 jobs remain authoritative. - Remote and transport: none; bounded discovery-root and builder-ordering contracts only @@ -5613,12 +5613,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the bounded `build/Release` output-tree discovery correction without raising any -entry/depth/candidate/byte bound, then rerun both Windows architectures and all four POSIX controls. -Require both Windows clean builds to select one target record and report the same allowlisted -switch summary. Only if that target-native evidence classifies the 2,879 function thunks may one -copied-artifact producer correction be considered; retain strict parsing/comparison and -rejected-output no-upload. +Push exact implementation commit `6f28a8cbf` plus its evidence-ledger head, then rerun both Windows +architectures and all four POSIX controls. Require both Windows clean builds to select one target +record and report the same allowlisted switch summary. Only if that target-native evidence +classifies the 2,879 function thunks may one copied-artifact producer correction be considered; +retain strict parsing/comparison and rejected-output no-upload. 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 publication, desktop resolver, SSH transfer/install, per-target From e7d9a1c8d3222ff5923679ad69b76b57d963c4f3 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 16:06:27 -0400 Subject: [PATCH 050/267] build(ssh): inspect Windows incremental database Co-authored-by: Orca --- ...lay-node-pty-windows-build-determinism.mjs | 21 +++++++++- ...ode-pty-windows-build-determinism.test.mjs | 39 +++++++++++++------ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs index ff48d46764e..bb2866a3734 100644 --- a/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs @@ -1,4 +1,4 @@ -import { open, readFile, readdir, writeFile } from 'node:fs/promises' +import { open, readFile, readdir, stat, writeFile } from 'node:fs/promises' import { join } from 'node:path' const COMPILER_OPTIONS = ` 'VCCLCompilerTool': { @@ -47,6 +47,7 @@ const MAX_LINK_COMMAND_TRACKING_CANDIDATES = 32 const MAX_LINK_COMMAND_TRACKING_DEPTH = 8 const MAX_LINK_COMMAND_TRACKING_ENTRIES = 10_000 const TARGET_LINK_OUTPUT_PATTERN = /(?:^|[\\/:"'\s])conpty_console_list\.node(?=$|[\\/:"'\s])/i +const TARGET_INCREMENTAL_DATABASE = 'conpty_console_list.ilk' function decodeXmlText(value) { return value @@ -154,6 +155,7 @@ async function discoverLinkCommandTracking(nodePtyDirectory) { // Why: generated dependency projects live elsewhere; only Release contains target link outputs. const queue = [{ depth: 0, path: join(nodePtyDirectory, 'build', 'Release') }] const candidates = [] + const incrementalDatabases = [] let entries = 0 for (let index = 0; index < queue.length; index += 1) { const current = queue[index] @@ -178,10 +180,15 @@ async function discoverLinkCommandTracking(nodePtyDirectory) { if (candidates.length > MAX_LINK_COMMAND_TRACKING_CANDIDATES) { throw new Error('MSBuild linker tracking discovery has too many candidates') } + } else if (child.isFile() && child.name.toLowerCase() === TARGET_INCREMENTAL_DATABASE) { + incrementalDatabases.push(childPath) + if (incrementalDatabases.length > 1) { + throw new Error('MSBuild incremental database discovery has duplicate target files') + } } } } - return { candidates, entries } + return { candidates, entries, incrementalDatabases } } function decodeLinkCommandTracking(bytes) { @@ -270,9 +277,19 @@ export async function inspectWindowsNodePtyLinkCommandTracking({ nodePtyDirector `MSBuild linker tracking discovery requires exactly one target command file (candidates=${discovery.candidates.length}, matches=${matching.length}, entries=${discovery.entries})` ) } + // Why: /DEBUG can imply incremental linking without spelling /INCREMENTAL in the command record. + let incrementalDatabase = { state: 'absent' } + if (discovery.incrementalDatabases.length === 1) { + const bytes = (await stat(discovery.incrementalDatabases[0])).size + if (!Number.isSafeInteger(bytes) || bytes < 0) { + throw new Error('MSBuild incremental database has an invalid size') + } + incrementalDatabase = { bytes, state: 'present' } + } return { ...parseWindowsNodePtyLinkCommandTracking(matching[0]), candidateFiles: discovery.candidates.length, + incrementalDatabase, searchedEntries: discovery.entries } } diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs index 34505c908db..2e48b2f1a38 100644 --- a/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs @@ -151,30 +151,34 @@ describe('SSH relay Windows node-pty build determinism', () => { it('bounds linker tracking bytes while reading the generated file', async () => { const directory = await mkdtemp(join(tmpdir(), 'orca-node-pty-link-tracking-')) temporaryDirectories.push(directory) - const trackingPath = join( - directory, - 'build', - 'Release', - 'unexpected', - 'target.tlog', - 'link.command.1.tlog' - ) - const otherPath = join(directory, 'build', 'Release', 'other', 'link.command.1.tlog') - await mkdir(join(directory, 'build', 'Release', 'unexpected', 'target.tlog'), { + const releaseDirectory = join(directory, 'build', 'Release') + const trackingPath = join(releaseDirectory, 'unexpected', 'target.tlog', 'link.command.1.tlog') + const otherPath = join(releaseDirectory, 'other', 'link.command.1.tlog') + const incrementalPath = join(releaseDirectory, 'unexpected', 'conpty_console_list.ilk') + await mkdir(join(releaseDirectory, 'unexpected', 'target.tlog'), { recursive: true }) - await mkdir(join(directory, 'build', 'Release', 'other'), { recursive: true }) + await mkdir(join(releaseDirectory, 'other'), { recursive: true }) await writeFile( trackingPath, '^one\n/OUT:conpty_console_list.node /INCREMENTAL:NO /Brepro /GUARD:CF /experimental:deterministic' ) await writeFile(otherPath, '^other\n/OUT:not_conpty_console_list.node /INCREMENTAL:NO') + await writeFile(incrementalPath, Buffer.alloc(4096)) await expect( inspectWindowsNodePtyLinkCommandTracking({ nodePtyDirectory: directory, tuple: 'win32-x64' }) - ).resolves.toMatchObject({ candidateFiles: 2, incremental: 'disabled' }) + ).resolves.toMatchObject({ + candidateFiles: 2, + incremental: 'disabled', + incrementalDatabase: { bytes: 4096, state: 'present' } + }) + await rm(incrementalPath) + await expect( + inspectWindowsNodePtyLinkCommandTracking({ nodePtyDirectory: directory, tuple: 'win32-x64' }) + ).resolves.toMatchObject({ incrementalDatabase: { state: 'absent' } }) await expect( inspectWindowsNodePtyLinkCommandTracking({ nodePtyDirectory: '/not-used-on-posix', @@ -204,6 +208,17 @@ describe('SSH relay Windows node-pty build determinism', () => { '^one\n/OUT:conpty_console_list.node /Brepro /GUARD:CF /experimental:deterministic' await writeFile(join(directory, 'build', 'Release', 'first', 'link.command.1.tlog'), command) await mkdir(join(directory, 'build', 'Release', 'second'), { recursive: true }) + const firstIlk = join(directory, 'build', 'Release', 'first', 'conpty_console_list.ilk') + const secondIlk = join(directory, 'build', 'Release', 'second', 'conpty_console_list.ilk') + await writeFile(firstIlk, '') + await writeFile(secondIlk, '') + await expect( + inspectWindowsNodePtyLinkCommandTracking({ + nodePtyDirectory: directory, + tuple: 'win32-arm64' + }) + ).rejects.toThrow('duplicate target files') + await Promise.all([rm(firstIlk), rm(secondIlk)]) await writeFile(join(directory, 'build', 'Release', 'second', 'link.command.1.tlog'), command) await expect( inspectWindowsNodePtyLinkCommandTracking({ From d1eca8a55dec3e96f9ecf1600e8e6e169cdfd3fe Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 16:08:12 -0400 Subject: [PATCH 051/267] docs(ssh): record incremental database gate Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 202 ++++++++++++++++-- 1 file changed, 183 insertions(+), 19 deletions(-) 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 df6c74d0a19..6dd5979f498 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run 29358223742 isolates 2,879 Windows arm64 linker-emitted 16-byte function thunks whose `adrp`/`add`/`br` control flow is byte-identical and whose unreachable fourth `udf` instruction differs, plus 68 derived `/Brepro` identity bytes; exact-head run 29359948742 disproves the empty `LinkIncremental` property oracle; exact-head run 29361673339 proves the locally assumed fixed tracking path is absent; exact-head run 29362672415 then proves both native Windows builds fail closed because bounded discovery from the entire generated `build` tree reaches unrelated dependency depth, while all four POSIX controls remain reproducible and upload; exact implementation commit `6f28a8cbf` narrows discovery to the `build/Release` output tree and is locally green under E-M3-WINDOWS-LINK-COMMAND-RELEASE-ROOT-LOCAL-001; the next six-cell exact-head run remains required, and no evidence authorizes changing a bound, producer, comparator, or default path; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run [29363423068](https://github.com/stablyai/orca/actions/runs/29363423068) at commit `2db46d91b` proves bounded `build/Release` discovery on both native Windows architectures: both clean builds select three tracking candidates, search 82 entries, select one target record, and report the same `/brepro`, `/debug`, `/experimental:deterministic`, `/guard:cf` summary with no explicit `/incremental` switch; Linux x64/arm64, macOS x64/arm64, and Windows x64 compare exactly and upload, while Windows arm64 retains the 2,879 linker-thunk mismatch and fails before upload under E-M3-WINDOWS-LINK-COMMAND-RELEASE-ROOT-CI-RED-001; exact implementation commit `e7d9a1c8d` adds only a bounded target `.ilk` presence/size diagnostic and is locally green under E-M3-WINDOWS-INCREMENTAL-DATABASE-LOCAL-001; the next exact-head six-cell run is required before any producer change; no evidence authorizes changing a comparator, production/default path, or tuple state; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -732,6 +732,16 @@ under E-M3-WINDOWS-LINK-COMMAND-RELEASE-ROOT-LOCAL-001. No budget, parser, produ staging, upload, legacy, or production behavior changed. The active gate is another exact-head six-cell native run. +Exact-head run 29363423068 proves the scoped discovery on both native Windows runners. Both clean +builds on both architectures select three tracking candidates under 82 Release-tree entries and +report the same allowlisted command summary: `/brepro`, `/debug`, `/experimental:deterministic`, +and `/guard:cf`, with no explicit `/incremental` switch. Windows x64 and all four POSIX controls +compare and upload; Windows arm64 still fails strict comparison at the same 2,879 linker thunks and +uploads nothing (E-M3-WINDOWS-LINK-COMMAND-RELEASE-ROOT-CI-RED-001). Because `/DEBUG` may imply +incremental linking without an explicit command switch, the next bounded diagnostic records only +the exact target `.ilk` file's presence and size from the already bounded clean Release tree before +any copied-artifact producer change. + Each runtime must contain only the executable closure required by the relay. - [ ] Replace or extend `config/scripts/build-relay.mjs` without weakening its existing relay and @@ -1339,7 +1349,7 @@ Update status and evidence as work begins. Do not combine these into one large b | ------------------------- | ------------------------------------------------------------------------------------------ | --------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------- | | 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 Release-tree discovery pending | `6f28a8cbf`; E-M3-WINDOWS-LINK-COMMAND-RELEASE-ROOT-LOCAL-001 | +| 2. Runtime builds | Per-tuple assembly, native smoke, SBOM/provenance/signing | None | Draft PR #8741; native target `.ilk` evidence pending | `e7d9a1c8d`; E-M3-WINDOWS-INCREMENTAL-DATABASE-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 | — | @@ -5556,23 +5566,177 @@ the bounded depth` before opening or parsing a candidate. Starting at the comple require both Windows clean builds to select one target record and emit the same accepted summary before changing the producer. +### E-M3-WINDOWS-LINK-COMMAND-RELEASE-ROOT-CI-RED-001 — Native command classified; arm64 drift retained + +- Date: 2026-07-14 +- Commit SHA / PR: exact head `2db46d91b17789ce70eed5e5e496dec2ff56442a`, containing exact + Release-root implementation commit `6f28a8cbf53d1cfe889ce259476eb4aaaead2d87`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Run and jobs: [run 29363423068](https://github.com/stablyai/orca/actions/runs/29363423068), + conclusion `failure`; Windows x64 `87188953411` success, Windows arm64 `87188953416` expected + strict-comparison failure, Linux arm64 `87188953471` success, Linux x64 `87188953495` success, + macOS x64 `87188953507` success, and macOS arm64 `87188953532` success +- Runners: Windows x64 used `windows-2022` / `win22` `20260706.237.1`, native X64, Windows Server + 2022 build 20348; Windows arm64 used `windows-11-arm` / `win11-arm64` `20260706.102.1`, native + ARM64, Windows build 26200. Both used Node v24.18.0 and MSVC 14.44.35207 / compiler 19.44.35228. + Linux x64 used `ubuntu-24.04` / `ubuntu24` `20260705.232.1`; Linux arm64 used + `ubuntu-24.04-arm` / `ubuntu24-arm64` `20260706.52.2`; macOS x64 used `macos-15-intel` / + `macos15` `20260629.0276.1`; macOS arm64 used `macos-15` / `macos15` `20260706.0213.1`. + Every runner checked out the exact head above. +- Remote and transport: none; target-native artifact assembly, strict comparison, and unpublished + seven-day Actions artifacts only +- Exact evidence commands: + + ```sh + gh run view 29363423068 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh api repos/stablyai/orca/actions/jobs/87188953411/logs + gh api repos/stablyai/orca/actions/jobs/87188953416/logs + gh api 'repos/stablyai/orca/actions/runs/29363423068/artifacts?per_page=100' + ``` + +- Result: FAIL as the intended strict reproducibility boundary. Each clean build on both Windows + architectures discovers three `link.command.1.tlog` candidates under 82 `build/Release` entries, + selects exactly one record naming `conpty_console_list.node`, and accepts fatal UTF-16LE input. + The two x64 records are 2,114 bytes and the two arm64 records are 2,092 bytes. All four report + `incremental: unspecified` and the identical allowlisted switches `/brepro`, `/debug`, + `/experimental:deterministic`, and `/guard:cf`; no explicit `/incremental` or `/opt` switch is + present. Windows x64 and all four POSIX jobs compare exactly. Windows arm64 builds, verifies, and + executes both candidates, then strict comparison rejects only + `runtime/node_modules/node-pty/build/Release/conpty_console_list.node` and skips upload. +- Arm64 diagnostic: the two 956,928-byte modules differ at 5,826 bytes across 2,886 ranges: 5,758 + bytes in 2,879 two-byte `.text` ranges spaced 16 bytes apart, plus 68 derived + COFF/debug/CodeView bytes. Their content IDs are + `sha256:4c4cc3b9ee0bac3a391a49ebfce230fef1d94984ef0f66eeba59fd2833b2a0d2` and + `sha256:26afc17c33337f6f1b8e767504a58207f4fc68cb26e807aca68a9d5e8040aba5`. +- Uploaded controls: exactly five unpublished artifacts and no Windows arm64 artifact: Windows x64 + `8323220212`, 37,075,312 bytes, + `sha256:10a07ff4863e4a0b4aef3325540660952f03f88cc7b2c2a4013f3081b5c9310c`; Linux x64 + `8323166437`, 29,285,377 bytes, + `sha256:21ee42d19e6a9f15e7afda18f9e27ff5ae280a19da61ef4042ca5b0a3ce608fe`; Linux arm64 + `8323165493`, 28,220,691 bytes, + `sha256:19cb887221810cd9e9df160d6ba6a185fd6282adee90d2dde6c66ad9a83e4fe1`; macOS x64 + `8323238492`, 26,405,918 bytes, + `sha256:af41bae838a26b1e127bc1bd20c3d1f2a6d0d5de1f6aeb9372d2b19561a2351a`; and macOS arm64 + `8323152771`, 24,729,554 bytes, + `sha256:aff001434f0d00db32d92a13ffbb40ed4f0cc0199cf29925109fc7f7bbf84977`. +- Duration and resource metrics: jobs ran 5m33s Windows x64, 8m15s Windows arm64, 3m25s Linux + arm64, 3m30s Linux x64, 6m18s macOS x64, and 2m55s macOS arm64. Windows arm64 clean builds took + 135,371.797 ms and 118,713.734 ms; smoke took 5,767.417 ms at 53,186,560-byte RSS and + 5,443.331 ms at 51,802,112-byte RSS. SSH channels/files, cancellation, and fallback delay remain + outside this artifact-only run. +- Artifact/log/trace link: run/jobs and the five unpublished artifacts above; rejected arm64 bytes + existed only in the bounded job workspace and logs and were not uploaded +- Oracle proved: the native Release-tree shape, candidate and target cardinality, record encoding, + and allowlisted linker summary on both Windows architectures; no explicit incremental-link switch; + five exact clean-build controls; unchanged arm64 thunk drift; strict rejection and no-upload. +- Does not prove: effective incremental linking when `/DEBUG` supplies a default, the target `.ilk` + state, a safe producer correction, arm64 equality, oldest baselines, native trust, SSH, + publication, transfer, fallback, UI, or any enabled tuple. +- Checklist items satisfied: native Release-root/command diagnostic only; no tuple or production + checkbox. +- Follow-up: inspect only the exact target `.ilk` file's presence and size within the already + bounded clean Release tree on both architectures. If it is present, require target-native evidence + before considering copied-artifact `/INCREMENTAL:NO`; if absent, investigate the next bounded + linker feature. Preserve `/guard:cf`, strict comparison, rejected-output no-upload, and all + repository-wide/default behavior. + +### E-M3-WINDOWS-INCREMENTAL-DATABASE-LOCAL-001 — Bounded target `.ilk` diagnostic + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `e7d9a1c8d3222ff5923679ad69b76b57d963c4f3`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741), target-native execution pending +- Runner: macOS 26.2 build 25C56, native Apple M4 arm64; Node v26.0.0 and pnpm 10.24.0. This runner + cannot produce a native MSVC `.ilk`, so Windows x64/arm64 jobs remain authoritative. +- Remote and transport: none; bounded local discovery/result-shape contracts only +- Red evidence: E-M3-WINDOWS-LINK-COMMAND-RELEASE-ROOT-CI-RED-001 proves both native commands carry + `/DEBUG` but no explicit `/INCREMENTAL`; no synthetic local result substitutes for the pending + target-native file-state evidence +- Exact green commands: + + ```sh + node --check config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs + node --check config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs + /usr/bin/time -lp pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + /usr/bin/time -lp 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 \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-build.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs + pnpm run typecheck + pnpm exec oxlint \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.mjs + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.mjs \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md + git diff --check + ``` + +- Result: PASS. The purpose suites pass 13/13 tests and all 15 artifact suites pass 58/58. Syntax, + typecheck, focused oxlint, the 355-entry max-lines ratchet, full repository + lint/reliability/localization, formatting, diff, and staged pre-commit hooks exit zero. Full lint + emits only pre-existing warnings outside this artifact package. +- Duration and resource metrics: purpose gate 0.94 s wall / 204 ms Vitest with 131,940,352-byte + maximum RSS and 96,081,224-byte peak footprint; complete artifact gate 3.50 s wall / 1.59 s + Vitest with 132,235,264-byte maximum RSS and 96,392,544-byte peak footprint. The parallel + typecheck/focused-lint/max-lines group completed in 3.8 s; full lint completed in 17.3 s and + formatting/diff checks in 0.8 s. No runtime archive, SSH channel/file, cancellation, fallback, or + launch-latency metric is exercised by this diagnostic-only package. +- Artifact/log/trace link: exact implementation commit and local command output; no runtime artifact + was created, staged, published, or uploaded +- Oracle proved: the already bounded and deterministic `build/Release` traversal detects at most one + exact case-insensitive `conpty_console_list.ilk`; duplicate target databases fail closed; a present + file reports only a safe numeric byte size, absence reports no guessed link state, symbolic links + retain the existing rejection, and POSIX remains a no-op. The diagnostic result is appended to + the existing allowlisted MSBuild summary before staging. +- Does not prove: whether either native Windows build emits the target `.ilk`, that `.ilk` presence + alone owns the arm64 thunk drift, that `/INCREMENTAL:NO` is a safe correction, arm64 equality, + oldest baselines, native trust, SSH, publication, transfer, fallback, UI, or any enabled tuple. +- Checklist items satisfied: local bounded target incremental-database diagnostic only; no tuple or + production checkbox. +- Follow-up: push the exact implementation plus ledger head, rerun all six native cells, and require + both clean builds on both Windows architectures to report the target `.ilk` state before any + producer change. Preserve `/guard:cf`, strict comparison, no rejected upload, and every + default/legacy boundary. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit owner and promotion condition. -| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | -| ------------------------------------------ | ------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------ | -| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | -| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | -| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | -| Native clean-rebuild identity unproved | Four POSIX controls pass; bounded Release-tree discovery is locally green | Toolchain drift may change native content IDs | Codex implementation owner | Prove the actual target linker command natively, then make all six clean builds match | Open | -| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | -| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | -| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | -| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | -| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | -| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | +| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | +| ------------------------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------ | +| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | +| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | +| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | +| Native clean-rebuild identity unproved | Five controls pass; arm64 target command has no explicit `/INCREMENTAL` | Toolchain drift may change native content IDs | Codex implementation owner | Classify target `.ilk`, then make all six clean builds match | Open | +| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | +| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | +| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | +| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | +| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | +| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | ## Final Definition of Done @@ -5613,11 +5777,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push exact implementation commit `6f28a8cbf` plus its evidence-ledger head, then rerun both Windows -architectures and all four POSIX controls. Require both Windows clean builds to select one target -record and report the same allowlisted switch summary. Only if that target-native evidence -classifies the 2,879 function thunks may one copied-artifact producer correction be considered; -retain strict parsing/comparison and rejected-output no-upload. +Add a purpose-tested bounded diagnostic for the exact target `conpty_console_list.ilk` file under +the already bounded clean `build/Release` traversal, recording only presence and size. Rerun both +Windows architectures and all four POSIX controls. Only if target-native evidence proves implicit +incremental linking may a copied-artifact `/INCREMENTAL:NO` producer correction be considered; +retain `/guard:cf`, strict parsing/comparison, and rejected-output no-upload. 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 publication, desktop resolver, SSH transfer/install, per-target From 6dfdfa0bd0d812c20981ffb728ddd18273097e6a Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 16:25:48 -0400 Subject: [PATCH 052/267] build(ssh): disable incremental relay linking Co-authored-by: Orca --- ...lay-node-pty-windows-build-determinism.mjs | 30 +++++++++++-------- ...ode-pty-windows-build-determinism.test.mjs | 22 +++++++++----- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs index bb2866a3734..5dd52e29056 100644 --- a/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs @@ -36,18 +36,19 @@ const REPRODUCIBLE_LINKER_OPTIONS = ` 'VCLinkerTool': { 'AdditionalOptions': [ '/DYNAMICBASE', '/guard:cf', + '/INCREMENTAL:NO', '/Brepro', '/experimental:deterministic' ] }` -const REQUIRED_GENERATED_OPTIONS = ['/Brepro', '/experimental:deterministic'] +const REQUIRED_COMPILER_OPTIONS = ['/Brepro', '/experimental:deterministic'] +const REQUIRED_LINKER_OPTIONS = [...REQUIRED_COMPILER_OPTIONS, '/INCREMENTAL:NO'] const MAX_LINK_COMMAND_TRACKING_BYTES = 256 * 1024 const MAX_LINK_COMMAND_TRACKING_CANDIDATES = 32 const MAX_LINK_COMMAND_TRACKING_DEPTH = 8 const MAX_LINK_COMMAND_TRACKING_ENTRIES = 10_000 const TARGET_LINK_OUTPUT_PATTERN = /(?:^|[\\/:"'\s])conpty_console_list\.node(?=$|[\\/:"'\s])/i -const TARGET_INCREMENTAL_DATABASE = 'conpty_console_list.ilk' function decodeXmlText(value) { return value @@ -72,7 +73,7 @@ function releaseDefinitionGroup(source, configuration) { return groups[0][2] } -function requiredToolOptions(group, tool) { +function requiredToolOptions(group, tool, requiredOptions) { const tools = [...group.matchAll(new RegExp(`<${tool}\\b[^>]*>([\\s\\S]*?)`, 'g'))] const options = tools.flatMap((match) => [ ...match[1].matchAll(/]*>([\s\S]*?)<\/AdditionalOptions>/g) @@ -84,13 +85,13 @@ function requiredToolOptions(group, tool) { if (!tokens.includes('%(AdditionalOptions)')) { throw new Error(`generated MSBuild settings do not inherit ${tool} options`) } - for (const required of REQUIRED_GENERATED_OPTIONS) { + for (const required of requiredOptions) { const count = tokens.filter((token) => token.toLowerCase() === required.toLowerCase()).length if (count !== 1) { throw new Error(`generated MSBuild settings require exactly one ${tool} ${required}`) } } - return [...REQUIRED_GENERATED_OPTIONS] + return [...requiredOptions] } export async function applyWindowsNodePtyBuildDeterminism({ nodePtyDirectory, tuple }) { @@ -103,11 +104,13 @@ export async function applyWindowsNodePtyBuildDeterminism({ nodePtyDirectory, tu source.split(COMPILER_OPTIONS).length !== 2 || source.split(LINKER_OPTIONS).length !== 2 || source.includes("'/Brepro'") || + source.includes("'/INCREMENTAL:NO'") || source.includes("'/experimental:deterministic'") ) { throw new Error('node-pty Windows build settings do not match the reviewed source') } - // Why: ARM64 compiler output changes even with a reproducible linker unless both stages opt in. + // Why: ARM64 needs both reproducible stages, while /DEBUG-implied incremental thunks still drift. + // Keep this producer correction inside the exclusive copied artifact source. const reproducibleSource = source .replace(COMPILER_OPTIONS, REPRODUCIBLE_COMPILER_OPTIONS) .replace(LINKER_OPTIONS, REPRODUCIBLE_LINKER_OPTIONS) @@ -126,8 +129,8 @@ export async function assertWindowsNodePtyGeneratedBuildSettings({ nodePtyDirect // Why: the source rewrite is insufficient proof unless gyp propagates both stages into MSBuild. return { configuration, - compilerOptions: requiredToolOptions(group, 'ClCompile'), - linkerOptions: requiredToolOptions(group, 'Link'), + compilerOptions: requiredToolOptions(group, 'ClCompile', REQUIRED_COMPILER_OPTIONS), + linkerOptions: requiredToolOptions(group, 'Link', REQUIRED_LINKER_OPTIONS), project } } @@ -180,7 +183,7 @@ async function discoverLinkCommandTracking(nodePtyDirectory) { if (candidates.length > MAX_LINK_COMMAND_TRACKING_CANDIDATES) { throw new Error('MSBuild linker tracking discovery has too many candidates') } - } else if (child.isFile() && child.name.toLowerCase() === TARGET_INCREMENTAL_DATABASE) { + } else if (child.isFile() && child.name.toLowerCase() === 'conpty_console_list.ilk') { incrementalDatabases.push(childPath) if (incrementalDatabases.length > 1) { throw new Error('MSBuild incremental database discovery has duplicate target files') @@ -281,13 +284,14 @@ export async function inspectWindowsNodePtyLinkCommandTracking({ nodePtyDirector let incrementalDatabase = { state: 'absent' } if (discovery.incrementalDatabases.length === 1) { const bytes = (await stat(discovery.incrementalDatabases[0])).size - if (!Number.isSafeInteger(bytes) || bytes < 0) { - throw new Error('MSBuild incremental database has an invalid size') - } incrementalDatabase = { bytes, state: 'present' } } + const linkCommand = parseWindowsNodePtyLinkCommandTracking(matching[0]) + if (linkCommand.incremental !== 'disabled' || incrementalDatabase.state !== 'absent') { + throw new Error('MSBuild incremental linking must be disabled without a target database') + } return { - ...parseWindowsNodePtyLinkCommandTracking(matching[0]), + ...linkCommand, candidateFiles: discovery.candidates.length, incrementalDatabase, searchedEntries: discovery.entries diff --git a/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs index 2e48b2f1a38..a6c97ac5d8c 100644 --- a/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs +++ b/config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs @@ -25,7 +25,7 @@ async function copiedSource() { async function generatedProject({ compilerOptions = '/Brepro /experimental:deterministic %(AdditionalOptions)', - linkerOptions = '/Brepro /experimental:deterministic %(AdditionalOptions)', + linkerOptions = '/Brepro /experimental:deterministic /INCREMENTAL:NO %(AdditionalOptions)', platform = 'ARM64' } = {}) { const directory = await mkdtemp(join(tmpdir(), 'orca-node-pty-windows-msbuild-')) @@ -54,6 +54,7 @@ describe('SSH relay Windows node-pty build determinism', () => { expect(compilerOptions).toContain("'/experimental:deterministic'") expect(linkerOptions).toContain("'/Brepro'") expect(linkerOptions).toContain("'/experimental:deterministic'") + expect(linkerOptions).toContain("'/INCREMENTAL:NO'") expect(await readFile(resolve('node_modules/node-pty/binding.gyp'), 'utf8')).toBe( repositorySource ) @@ -83,7 +84,7 @@ describe('SSH relay Windows node-pty build determinism', () => { ).resolves.toEqual({ configuration: 'Release|ARM64', compilerOptions: ['/Brepro', '/experimental:deterministic'], - linkerOptions: ['/Brepro', '/experimental:deterministic'], + linkerOptions: ['/Brepro', '/experimental:deterministic', '/INCREMENTAL:NO'], project: 'conpty_console_list.vcxproj' }) }) @@ -170,21 +171,28 @@ describe('SSH relay Windows node-pty build determinism', () => { nodePtyDirectory: directory, tuple: 'win32-x64' }) + ).rejects.toThrow('must be disabled without a target database') + await rm(incrementalPath) + await expect( + inspectWindowsNodePtyLinkCommandTracking({ nodePtyDirectory: directory, tuple: 'win32-x64' }) ).resolves.toMatchObject({ candidateFiles: 2, incremental: 'disabled', - incrementalDatabase: { bytes: 4096, state: 'present' } + incrementalDatabase: { state: 'absent' } }) - await rm(incrementalPath) - await expect( - inspectWindowsNodePtyLinkCommandTracking({ nodePtyDirectory: directory, tuple: 'win32-x64' }) - ).resolves.toMatchObject({ incrementalDatabase: { state: 'absent' } }) await expect( inspectWindowsNodePtyLinkCommandTracking({ nodePtyDirectory: '/not-used-on-posix', tuple: 'linux-arm64-glibc' }) ).resolves.toBeUndefined() + await writeFile( + trackingPath, + '^one\n/OUT:conpty_console_list.node /Brepro /GUARD:CF /experimental:deterministic' + ) + await expect( + inspectWindowsNodePtyLinkCommandTracking({ nodePtyDirectory: directory, tuple: 'win32-x64' }) + ).rejects.toThrow('must be disabled without a target database') await writeFile(trackingPath, Buffer.alloc(300_000)) await expect( inspectWindowsNodePtyLinkCommandTracking({ From 3ec1a48afde95618fb7e3f6be71303410e6701dd Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 16:27:55 -0400 Subject: [PATCH 053/267] docs(ssh): record incremental link evidence Co-authored-by: Orca --- ...github-release-implementation-checklist.md | 197 ++++++++++++++++-- 1 file changed, 180 insertions(+), 17 deletions(-) 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 6dd5979f498..e736565e8ac 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 @@ -2,7 +2,7 @@ Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run [29363423068](https://github.com/stablyai/orca/actions/runs/29363423068) at commit `2db46d91b` proves bounded `build/Release` discovery on both native Windows architectures: both clean builds select three tracking candidates, search 82 entries, select one target record, and report the same `/brepro`, `/debug`, `/experimental:deterministic`, `/guard:cf` summary with no explicit `/incremental` switch; Linux x64/arm64, macOS x64/arm64, and Windows x64 compare exactly and upload, while Windows arm64 retains the 2,879 linker-thunk mismatch and fails before upload under E-M3-WINDOWS-LINK-COMMAND-RELEASE-ROOT-CI-RED-001; exact implementation commit `e7d9a1c8d` adds only a bounded target `.ilk` presence/size diagnostic and is locally green under E-M3-WINDOWS-INCREMENTAL-DATABASE-LOCAL-001; the next exact-head six-cell run is required before any producer change; no evidence authorizes changing a comparator, production/default path, or tuple state; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 target-native runtime assembly — exact-head run [29364581781](https://github.com/stablyai/orca/actions/runs/29364581781) at ledger head `d1eca8a55` proves both clean Windows x64 builds create the same 4,238,838-byte target `.ilk` and both clean Windows arm64 builds create the same 4,980,517-byte target `.ilk`; Microsoft documents that `/DEBUG` implies `/INCREMENTAL` and that incremental linking creates this database; Windows x64 and all four POSIX controls compare and upload, while arm64 retains the exact 2,879-thunk drift and no rejected upload under E-M3-WINDOWS-INCREMENTAL-DATABASE-CI-RED-001; exact implementation commit `6dfdfa0bd` adds `/INCREMENTAL:NO` only to the copied node-pty Windows linker options and is locally green under E-M3-WINDOWS-INCREMENTAL-DISABLE-LOCAL-001; the next exact-head six-cell run must prove generated/actual option propagation, target `.ilk` absence, complete strict equality, smoke, and upload before this correction is accepted; `/guard:cf`, the repository-wide node-pty patch, production/default behavior, and every tuple state remain unchanged; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -742,6 +742,15 @@ incremental linking without an explicit command switch, the next bounded diagnos the exact target `.ilk` file's presence and size from the already bounded clean Release tree before any copied-artifact producer change. +Exact-head run 29364581781 proves both Windows architectures create a stable target `.ilk` in each +clean build: 4,238,838 bytes on x64 and 4,980,517 bytes on arm64. Microsoft documents that +`/DEBUG` implies `/INCREMENTAL`, incremental linking creates the `.ilk`, and `/INCREMENTAL:NO` is +the explicit override. Five controls compare and upload; arm64 retains the exact thunk drift and +fails before upload (E-M3-WINDOWS-INCREMENTAL-DATABASE-CI-RED-001). This authorizes one narrow +producer correction: add `/INCREMENTAL:NO` only to the copied node-pty Windows linker options, +retain `/guard:cf`, and fail closed unless the generated project and actual command each contain +exactly one disable switch and the clean output tree contains no target `.ilk`. + Each runtime must contain only the executable closure required by the relay. - [ ] Replace or extend `config/scripts/build-relay.mjs` without weakening its existing relay and @@ -1345,16 +1354,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 target `.ilk` evidence pending | `e7d9a1c8d`; E-M3-WINDOWS-INCREMENTAL-DATABASE-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 `/INCREMENTAL:NO` proof pending | `6dfdfa0bd`; E-M3-WINDOWS-INCREMENTAL-DISABLE-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 | — | Every PR must document: @@ -5720,6 +5729,161 @@ the bounded depth` before opening or parsing a candidate. Starting at the comple producer change. Preserve `/guard:cf`, strict comparison, no rejected upload, and every default/legacy boundary. +### E-M3-WINDOWS-INCREMENTAL-DATABASE-CI-RED-001 — Native `.ilk` proves implicit incremental link + +- Date: 2026-07-14 +- Commit SHA / PR: exact ledger head `d1eca8a55dec3e96f9ecf1600e8e6e169cdfd3fe`, containing exact + diagnostic implementation commit `e7d9a1c8d3222ff5923679ad69b76b57d963c4f3`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Run and jobs: [run 29364581781](https://github.com/stablyai/orca/actions/runs/29364581781), + conclusion `failure`; Windows x64 `87192850326` success, Windows arm64 `87192850417` expected + strict-comparison failure, Linux arm64 `87192850309` success, Linux x64 `87192850444` success, + macOS x64 `87192850303` success, and macOS arm64 `87192850364` success +- Runners: Windows x64 used `windows-2022` / `win22` `20260706.237.1`, native X64; Windows arm64 + used `windows-11-arm` / `win11-arm64` `20260706.102.1`, native ARM64. Linux x64 used + `ubuntu-24.04` / `ubuntu24` `20260705.232.1`; Linux arm64 used `ubuntu-24.04-arm` / + `ubuntu24-arm64` `20260706.52.2`; macOS x64 used `macos-15-intel` / `macos15` + `20260629.0276.1`; macOS arm64 used `macos-15` / `macos15` `20260706.0213.1`. Every runner + checked out the exact head above; both Windows jobs used Node v24.18.0 and MSVC 14.44.35207 / + compiler 19.44.35228. +- Remote and transport: none; target-native artifact assembly, strict comparison, and unpublished + seven-day Actions artifacts only +- Exact evidence commands: + + ```sh + gh run view 29364581781 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh api repos/stablyai/orca/actions/jobs/87192850326/logs + gh api repos/stablyai/orca/actions/jobs/87192850417/logs + gh api 'repos/stablyai/orca/actions/runs/29364581781/artifacts?per_page=100' + curl -fsSL --proto '=https' --tlsv1.2 \ + 'https://learn.microsoft.com/en-us/cpp/build/reference/incremental-link-incrementally?view=msvc-170' \ + | rg -i -C 2 'DEBUG|incremental.*default|\.ilk|INCREMENTAL:NO' + ``` + +- Result: FAIL as the intended strict evidence boundary. Both clean x64 builds report the same + 4,238,838-byte `conpty_console_list.ilk`; both clean arm64 builds report the same 4,980,517-byte + target database. All four still report `/debug` with `incremental: unspecified` and no explicit + `/incremental` or `/opt` switch in the command record. Microsoft documents that `/DEBUG` implies + `/INCREMENTAL`, incremental links create/update an `.ilk`, and `/INCREMENTAL:NO` overrides the + default. Windows x64 and all four POSIX controls compare exactly. Windows arm64 builds, verifies, + and executes both candidates, then rejects only `conpty_console_list.node` and skips upload. +- Arm64 diagnostic: the two 956,928-byte modules again differ at 5,826 bytes across 2,886 ranges: + 5,758 bytes in 2,879 two-byte `.text` ranges spaced 16 bytes apart, plus the same 68 derived + COFF/debug/CodeView bytes. Their content IDs are + `sha256:b38d3343e96f339eb7197a4fb379b5f636d5f6752d42434b508bb7c67803f57c` and + `sha256:9d937233f63b142f03e504af2ffeeeb05b2de2b979047a369a79dd1d793b1786`. +- Uploaded controls: exactly five unpublished artifacts and no Windows arm64 artifact: Windows x64 + `8323660213`, 37,075,309 bytes, + `sha256:788c43c94db25e995e6d36ab437d841e01a24846f3b8ec519f62fd7bebeb4fbd`; Linux x64 + `8323618920`, 29,280,401 bytes, + `sha256:0b19a0b1dd78793ae0d55f7077eb871a7a8f98d3f7e4c3b8fd5f457f8f008fd6`; Linux arm64 + `8323624757`, 28,207,487 bytes, + `sha256:cf4bb8255b260fc9109b9180b05d561a3b3cb0c3eeb791d111aff649368a5c86`; macOS x64 + `8323808296`, 26,420,419 bytes, + `sha256:4117ae641972cad3936b9733b8461148ab2e4e3280cb3d27d31b479d966e412a`; and macOS arm64 + `8323599375`, 24,749,802 bytes, + `sha256:b29cba1d66f7fedfa89bd4994d0186b5f2e0f89a17b95d709727808e0ccf3590`. +- Duration and resource metrics: jobs ran 4m56s Windows x64, 10m25s Windows arm64, 3m32s Linux + arm64, 3m22s Linux x64, 10m47s macOS x64, and 2m38s macOS arm64. Windows arm64 clean builds took + 156,490.445 ms and 135,338.371 ms; smoke took 5,949.372 ms at 53,084,160-byte RSS and + 5,425.281 ms at 52,871,168-byte RSS. SSH channels/files, cancellation, and fallback delay remain + outside this artifact-only run. +- Artifact/log/trace link: run/jobs, Microsoft linker reference above, and the five unpublished + artifacts; rejected arm64 bytes existed only in the bounded job workspace and logs and were not + uploaded +- Oracle proved: a fresh target incremental-link database on both clean builds and both native + architectures; the documented `/DEBUG`-implied incremental state; stable target database sizes; + five exact controls; unchanged arm64 thunk drift; strict rejection and no rejected upload. +- Does not prove: that `/INCREMENTAL:NO` removes the thunk drift, that all six outputs compare, + oldest baselines, native trust, SSH, publication, transfer, fallback, UI, or any enabled tuple. +- Checklist items satisfied: native implicit-incremental-link classification only; no tuple or + production checkbox. +- Follow-up: add exactly one `/INCREMENTAL:NO` to only the copied node-pty Windows linker options. + Fail closed unless the generated project and actual command each contain it exactly once and the + clean tree contains no target `.ilk`; rerun all six native cells with `/guard:cf`, strict + comparison, and rejected-output no-upload unchanged. + +### E-M3-WINDOWS-INCREMENTAL-DISABLE-LOCAL-001 — Copied-artifact full-link correction + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `6dfdfa0bd0d812c20981ffb728ddd18273097e6a`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741), target-native execution pending +- Runner: macOS 26.2 build 25C56, native Apple M4 arm64; Node v26.0.0 and pnpm 10.24.0. This runner + cannot execute MSVC, so Windows x64/arm64 jobs remain authoritative. +- Remote and transport: none; copied-source, generated-project, actual-command/result-shape, and + ordering contracts only +- Red evidence: E-M3-WINDOWS-INCREMENTAL-DATABASE-CI-RED-001 proves both native architectures use + `/DEBUG`-implied incremental linking, create stable target `.ilk` files, and retain the arm64 thunk + drift; no synthetic local failure substitutes for that target-native evidence +- Exact green commands: + + ```sh + node --check config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs + node --check config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs + /usr/bin/time -lp pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + /usr/bin/time -lp 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 \ + config/scripts/ssh-relay-node-pty-build.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ + config/scripts/ssh-relay-node-zip-inspection.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-build.test.mjs \ + config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ + config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ + config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs + pnpm run typecheck + pnpm exec oxlint \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.mjs + pnpm run check:max-lines-ratchet + GOMAXPROCS=2 pnpm run lint + pnpm exec oxfmt --check \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.mjs \ + config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ + config/scripts/ssh-relay-node-pty-build.mjs \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md + git diff --check + ``` + +- Result: PASS. The purpose suites pass 13/13 tests and all 15 artifact suites pass 58/58. Syntax, + typecheck, focused oxlint, the 355-entry max-lines ratchet, full repository + lint/reliability/localization, formatting, diff, and staged pre-commit hooks exit zero. Full lint + emits only pre-existing warnings outside this artifact package. +- Duration and resource metrics: purpose gate 1.00 s wall / 202 ms Vitest with 132,235,264-byte + maximum RSS and 96,408,952-byte peak footprint; complete artifact gate 2.63 s wall / 1.13 s + Vitest with 132,251,648-byte maximum RSS and 96,490,944-byte peak footprint. The parallel + typecheck/focused-lint/max-lines group completed in 3.1 s; full lint and format/diff gates settled + within 10.1 s. No runtime archive, SSH channel/file, cancellation, fallback, or launch-latency + metric is exercised by this local package. +- Artifact/log/trace link: exact implementation commit and local command output; no runtime artifact + was created, staged, published, or uploaded +- Oracle proved: only the copied Windows node-pty linker options gain exactly one + `/INCREMENTAL:NO`; compiler options remain separate and `/guard:cf` remains present; source drift + or repeat transformation fails closed; generated Release projects must inherit exactly one disable + switch; actual target commands must classify it as disabled; any target `.ilk` fails before + staging; POSIX and the installed repository source remain unchanged. Both source and test modules + remain below 300 lines without a max-lines bypass. +- Does not prove: MSVC accepts/propagates the option on either native architecture, that target + `.ilk` is absent, that the arm64 thunks become stable, that all six outputs compare and upload, + oldest baselines, native trust, SSH, publication, transfer, fallback, UI, or any enabled tuple. +- Checklist items satisfied: local copied-artifact incremental-disable contract only; no tuple or + production checkbox. +- Follow-up: push the exact implementation plus ledger head and rerun all six target-native cells. + Require both Windows clean builds to report `/incremental:no`, no target `.ilk`, complete runtime + smoke, exact output equality, and upload before accepting the correction. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -5730,7 +5894,7 @@ owner and promotion condition. | Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | | No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | | Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | -| Native clean-rebuild identity unproved | Five controls pass; arm64 target command has no explicit `/INCREMENTAL` | Toolchain drift may change native content IDs | Codex implementation owner | Classify target `.ilk`, then make all six clean builds match | Open | +| Native clean-rebuild identity unproved | Five controls pass; copied `/INCREMENTAL:NO` is locally green | Toolchain drift may change native content IDs | Codex implementation owner | Prove flag, no `.ilk`, and all-six equality on native runners | Open | | Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | | Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | | Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | @@ -5777,11 +5941,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Add a purpose-tested bounded diagnostic for the exact target `conpty_console_list.ilk` file under -the already bounded clean `build/Release` traversal, recording only presence and size. Rerun both -Windows architectures and all four POSIX controls. Only if target-native evidence proves implicit -incremental linking may a copied-artifact `/INCREMENTAL:NO` producer correction be considered; -retain `/guard:cf`, strict parsing/comparison, and rejected-output no-upload. +Push exact implementation commit `6dfdfa0bd` plus its evidence-ledger head, then rerun both Windows +architectures and all four POSIX controls. Require both Windows clean builds to report exactly one +`/INCREMENTAL:NO`, no target `.ilk`, complete smoke, exact output equality, and upload. Retain +`/guard:cf`, strict parsing/comparison, and rejected-output no-upload. 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 publication, desktop resolver, SSH transfer/install, per-target From 6751a65ab356cfdf0bdb386026e1e0fbdc3b8fd7 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 16:44:50 -0400 Subject: [PATCH 054/267] docs(ssh): add concise implementation tracker Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 162 ++++++++++++++++++ ...github-release-implementation-checklist.md | 144 +++++++++++++--- 2 files changed, 279 insertions(+), 27 deletions(-) create mode 100644 docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md 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 new file mode 100644 index 00000000000..b5945925010 --- /dev/null +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md @@ -0,0 +1,162 @@ +# SSH Relay Runtime Distribution — Implementation Checklist + +Last updated: 2026-07-14 + +This is the human-readable project tracker. The +[detailed evidence ledger](./2026-07-14-ssh-relay-github-release-implementation-checklist.md) +contains exact commands, runner identities, artifact hashes, metrics, and residual gaps. + +A checked box here means the implementation or validation has exact evidence in that ledger. +Approved design decisions are listed separately and are not presented as implemented work. + +## Current state + +- Existing SSH relay behavior remains the default for every target. +- The bundled runtime path is not connected to production and no tuple is enabled. +- The future bundled path is a per-SSH-target Beta option, off by default. +- No runtime artifact has been published to a GitHub Release. +- Latest native proof: [GitHub Actions run 29365815434](https://github.com/stablyai/orca/actions/runs/29365815434) + passed clean-build equality and runtime smoke on Linux x64/arm64, macOS x64/arm64, and Windows + x64/arm64. +- Active work package: finish the artifact/runtime closure audit, then address archive safety, + oldest-baseline execution, and native trust. + +## Locked rollout decisions + +- Legacy behavior remains the persisted and effective default. +- The Beta choice is per SSH target, off by default, and shown on target add/edit surfaces. +- Missing, old, imported, unknown, or malformed settings resolve to legacy. +- A mode change applies only on the next connection or explicit reconnect. +- Legacy remains available; removal or narrowing requires a separate review. +- Only availability/compatibility failures may fall back automatically. Integrity, security, and + corruption failures fail closed. + +## Rollout implementation remaining + +- [ ] Implement the per-target configuration field and migration tests. +- [ ] Add the off-by-default Beta-tagged option to SSH target add/edit UI. +- [ ] Implement `bundled-auto` selection for explicitly opted-in targets. +- [ ] Implement automatic legacy fallback only for classified availability/compatibility failures. +- [ ] Fail closed for signature, hash, archive, tree, native-trust, bundled-Node, and cache-corruption + failures. +- [ ] Add the explicit “disable Beta and reconnect” recovery action for fail-closed Beta errors. +- [ ] Collect privacy-safe Beta success, latency, failure-stage, and fallback-reason telemetry. +- [ ] Require a separate reviewed decision before changing any tuple to default-on. + +## Work packages + +### 0 — Existing Node/npm resolver correction + +- [x] Implement coherent remote Node/npm selection. +- [x] Add focused unit/static proof. +- [x] Prove live Linux arm64 SSH behavior. +- [x] Keep this separate from artifact distribution. +- [ ] Merge the independently reviewable fix from draft PR + [#8724](https://github.com/stablyai/orca/pull/8724). + +### 1 — Manifest, identity, and platform contracts + +- [x] Define signed immutable manifest and canonical content identity. +- [x] Define exact release tag/asset URL rules without mutable `latest` lookup. +- [x] Define conservative OS/architecture/libc selection. +- [x] Add hostile manifest/path/schema tests. +- [ ] Finish archive-safety implementation and hostile archive tests. +- [ ] Update mode-qualified remote directory parsing and GC compatibility. +- [ ] Merge the contract package from draft PR + [#8728](https://github.com/stablyai/orca/pull/8728). + +### 2 — Target-native runtime artifacts + +- [x] Pin and verify Node v24.18.0 release inputs and signatures. +- [x] Produce and smoke-test candidate runtimes with patched `node-pty` and the matching + `@parcel/watcher` native package on all six runner families. +- [x] Prove bundled Node, real PTY input/resize/exit, watcher events, and Windows resource settlement. +- [x] Prove two clean builds compare exactly on all six native GitHub runner families. +- [x] Keep `/guard:cf`; fix Windows arm64 drift with copied-artifact-only `/INCREMENTAL:NO`. +- [ ] Complete the runtime closure, license, SBOM, provenance, and prohibited-content allowlist audit. +- [ ] Prove each candidate on its oldest supported OS/libc/kernel baseline. +- [ ] Sign macOS and Windows native bytes and verify target-native trust policy. +- [ ] Record complete compiler/toolchain provenance for release promotion. +- [ ] Merge draft PR [#8741](https://github.com/stablyai/orca/pull/8741). + +### 3 — Release build, signing, and publication + +- [ ] Add target-native build jobs as release prerequisites. +- [ ] Add native signing jobs and return signed bytes before final hashing. +- [ ] Add the fail-closed aggregate and manifest-signing job. +- [ ] Embed the exact signed manifest and accepted keys into every desktop build. +- [ ] Upload the exact prebuilt runtime bytes to the matching draft release. +- [ ] Verify draft read-back hashes and execute downloaded archives. +- [ ] Test timeout, retry exhaustion, approval denial/timeout, signing failure, partial output, and + recovered-draft behavior. +- [ ] Block publication whenever any required runtime/signing/manifest gate fails. + +### 4 — Desktop resolver, verification, extraction, and cache + +- [ ] Implement offline tuple selection from the embedded signed manifest. +- [ ] Implement exact direct release URL resolution and safe redirect handling. +- [ ] Stream download with size/time bounds and archive SHA-256 verification. +- [ ] Extract into exclusive staging with full hostile-archive defenses. +- [ ] Verify the complete extracted tree before atomic cache publication. +- [ ] Implement the 2 GiB bounded cache, locking, concurrency, quarantine, and eviction rules. +- [ ] Prove verified client-cache transfer works with the client offline. +- [ ] Preserve `ORCA_RELAY_PATH` behind the official-build trust boundary. + +### 5 — Bounded SSH transfer and remote installation + +- [ ] Implement bounded/cancellable SFTP transfer with executable-mode restoration. +- [ ] Keep compatible POSIX tar streaming as an optional fast path. +- [ ] Implement POSIX no-tar transfer using only the declared shell/file primitives. +- [ ] Implement bounded binary PowerShell/.NET transfer for Windows system SSH. +- [ ] Transfer only locally verified bytes into exclusive remote staging. +- [ ] Use bundled Node to hash the complete staged tree before any native probe. +- [ ] Run native probes, PTY/watcher smoke, structured sentinel, atomic publish, and launch in order. +- [ ] Trust the initial-install proof on warm launch under the immutable-directory assumption. +- [ ] Quarantine detected mutation and recover only from freshly verified bundled bytes. +- [ ] Preserve SSH authentication, connection, and relay RPC transport behavior. + +### 6 — Modes, fallback, diagnostics, and concurrency + +- [ ] Implement internal `legacy`, `auto`, and forced diagnostic `bundled` modes. +- [ ] Abort and await all bundled work before eligible legacy fallback starts. +- [ ] Keep bundled and legacy identities, locks, partials, sentinels, and generations separate. +- [ ] Add stable stage/failure codes and privacy-safe diagnostics. +- [ ] Prove fallback races, reconnect/reattach, concurrent clients, GC, upgrade, and downgrade. +- [ ] Prove every integrity/security/corruption class fails closed in `auto`. +- [ ] Prove eligible legacy fallback begins within the documented delay budget. + +### 7 — Validation and Beta rollout + +- [ ] Name and pin representative POSIX and Windows remote snapshots for Layer B. +- [ ] For every enabled remote tuple, pass built-in SFTP and system SSH. +- [ ] For every supported client OS/architecture, pass both transfer families against representative + POSIX and Windows remotes. +- [ ] Prove remote GitHub egress is never required. +- [ ] Prove no-tar/no-Node/no-Python/no-Perl/no-hash-tool bootstrap cases. +- [ ] Run full-size, slow-link, cancellation, concurrency, and failure-injection tests. +- [ ] Measure cold/warm latency, memory, channels/files, cancellation settlement, and fallback delay + against legacy baselines. +- [ ] Pass security review, SBOM/provenance, signing, key lifecycle, CVE, and emergency-refresh gates. +- [ ] Ship the per-target Beta with every target still defaulting to legacy. +- [ ] Accumulate approved real-host Beta evidence before any default-on proposal. +- [ ] Require three qualifying RCs plus rollback proof before default-on review. + +## External blockers + +- [ ] Repository release administrator: approve the representative Layer B remote provider, + snapshots, credentials, egress policy, teardown SLA, and cost/capacity owner. +- [ ] Repository release administrator: provision the protected manifest/native-signing environment, + reviewers, test keys/certificates, and access audit. + +Safe behavior while either blocker remains: no bundled tuple is enabled or published; legacy remains +the default, and independent local/native-runner contract work may continue. + +## Definition of done + +- [ ] Every item above that applies to an enabled tuple has exact evidence in the detailed ledger. +- [ ] Every enabled tuple has target-native build, oldest-baseline, native-trust, SFTP, system-SSH, + RPC, security, and performance proof. +- [ ] Every packaged desktop embeds the exact signed manifest for the published immutable assets. +- [ ] The Beta ships off by default per target and rollback to legacy is proven in the same build. +- [ ] No default-on rollout occurs without a separate reviewed decision and completed soak evidence. +- [ ] Legacy remains available until a separately reviewed removal decision. 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 e736565e8ac..88a07cc83e2 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 @@ -1,8 +1,14 @@ # SSH Relay GitHub Release Distribution — Living Implementation Checklist +Human-readable tracker: +[SSH Relay Runtime Distribution — Implementation Checklist](./2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md) + +This file is the detailed evidence ledger. Use the tracker above for project status and remaining +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 target-native runtime assembly — exact-head run [29364581781](https://github.com/stablyai/orca/actions/runs/29364581781) at ledger head `d1eca8a55` proves both clean Windows x64 builds create the same 4,238,838-byte target `.ilk` and both clean Windows arm64 builds create the same 4,980,517-byte target `.ilk`; Microsoft documents that `/DEBUG` implies `/INCREMENTAL` and that incremental linking creates this database; Windows x64 and all four POSIX controls compare and upload, while arm64 retains the exact 2,879-thunk drift and no rejected upload under E-M3-WINDOWS-INCREMENTAL-DATABASE-CI-RED-001; exact implementation commit `6dfdfa0bd` adds `/INCREMENTAL:NO` only to the copied node-pty Windows linker options and is locally green under E-M3-WINDOWS-INCREMENTAL-DISABLE-LOCAL-001; the next exact-head six-cell run must prove generated/actual option propagation, target `.ilk` absence, complete strict equality, smoke, and upload before this correction is accepted; `/guard:cf`, the repository-wide node-pty patch, production/default behavior, and every tuple state remain unchanged; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 runtime closure — exact-head run [29365815434](https://github.com/stablyai/orca/actions/runs/29365815434) at ledger head `3ec1a48af` passes all six target-native jobs under E-M3-REPRODUCIBILITY-CI-001; both Windows clean builds propagate exactly one copied-artifact `/INCREMENTAL:NO`, retain `/guard:cf`, emit no target `.ilk`, complete bundled-Node/native/PTy/watcher smoke, compare the full runtime/archive/identity/SBOM/provenance exactly, and upload; the Windows arm64 2,879-thunk drift is closed without normalization or a repository-wide change; the active artifact-only slice is auditing and closing the remaining runtime closure/SBOM/provenance checklist claims before oldest-baseline and native-trust work; production/default behavior and every tuple state remain unchanged; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      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) @@ -169,11 +175,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: replace the empty-property oracle with a bounded parser for the actual - post-build MSBuild linker-command tracking record, then rerun all six target-native cells. Require - both Windows clean builds to report the same allowlisted linker-switch summary before considering - one copied-artifact producer correction. Preserve strict comparison, no rejected upload, the - repository-wide node-pty patch, legacy/default path, and every other release gate. +- Next required action: audit the exact staged runtime closure, licenses, SPDX, provenance, + compiler/toolchain records, and prohibited-content allowlist against the current candidate bytes. + Check only claims supported by exact purpose tests and evidence IDs, then proceed to the + oldest-baseline and native-trust gates. Preserve the artifact-only boundary, legacy/default path, + and every other release gate. ## Non-Negotiable Invariants @@ -751,6 +757,12 @@ producer correction: add `/INCREMENTAL:NO` only to the copied node-pty Windows l retain `/guard:cf`, and fail closed unless the generated project and actual command each contain exactly one disable switch and the clean output tree contains no target `.ilk`. +Exact-head run 29365815434 passes all six target-native jobs. Both Windows architectures propagate +exactly one disable switch in both clean builds, retain `/guard:cf`, report no target `.ilk`, execute +the full bundled runtime smoke, compare every runtime/archive/identity/SBOM/provenance byte exactly, +and upload. Windows arm64 now has one stable content ID and archive digest, closing the native +clean-build reproducibility gap without post-build normalization (E-M3-REPRODUCIBILITY-CI-001). + Each runtime must contain only the executable closure required by the relay. - [ ] Replace or extend `config/scripts/build-relay.mjs` without weakening its existing relay and @@ -807,8 +819,9 @@ Rules: - [ ] Real hardware or native virtualized execution is required for release promotion. QEMU or cross-compilation may add coverage but cannot fill the evidence column alone. - [ ] A tuple without a trustworthy runtime source or real runner stays disabled. -- [ ] Every archive executes the exact bundled Node, loads both native dependencies, spawns a real - PTY, performs input/resize/exit, and observes create/modify/rename/delete watcher events. +- [x] Every current candidate archive executes the exact bundled Node, loads both native + dependencies, spawns a real PTY, performs input/resize/exit, and observes + create/modify/rename/delete watcher events. (E-M3-REPRODUCIBILITY-CI-001) - [ ] Every build records compiler/toolchain/container image digests and runner architecture. ## Milestone 4 — Make Relay Artifacts Prerequisites of Desktop Builds @@ -1354,16 +1367,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 `/INCREMENTAL:NO` proof pending | `6dfdfa0bd`; E-M3-WINDOWS-INCREMENTAL-DISABLE-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; all-six reproducibility green; closure/trust pending | `3ec1a48af`; E-M3-REPRODUCIBILITY-CI-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 | — | Every PR must document: @@ -5884,6 +5897,82 @@ the bounded depth` before opening or parsing a candidate. Starting at the comple Require both Windows clean builds to report `/incremental:no`, no target `.ilk`, complete runtime smoke, exact output equality, and upload before accepting the correction. +### E-M3-REPRODUCIBILITY-CI-001 — All six target-native clean builds compare and upload + +- Date: 2026-07-14 +- Commit SHA / PR: exact ledger head `3ec1a48afde95618fb7e3f6be71303410e6701dd`, containing exact + incremental-disable implementation commit `6dfdfa0bd0d812c20981ffb728ddd18273097e6a`; stacked draft + PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Run and jobs: [run 29365815434](https://github.com/stablyai/orca/actions/runs/29365815434), + conclusion `success`; Windows arm64 `87196982746`, Windows x64 `87196982776`, Linux arm64 + `87196982798`, Linux x64 `87196982795`, macOS x64 `87196982820`, and macOS arm64 `87196982760` + all passed +- Runners: Windows x64 used `windows-2022` / `win22` `20260706.237.1`, native X64; Windows arm64 + used `windows-11-arm` / `win11-arm64` `20260706.102.1`, native ARM64. Linux x64 used + `ubuntu-24.04` / `ubuntu24` `20260705.232.1`; Linux arm64 used `ubuntu-24.04-arm` / + `ubuntu24-arm64` `20260706.52.2`; macOS x64 used `macos-15-intel` / `macos15` + `20260629.0276.1`; macOS arm64 used `macos-15` / `macos15` `20260706.0213.1`. Every runner + checked out the exact head above; both Windows jobs used Node v24.18.0 and MSVC 14.44.35207 / + compiler 19.44.35228. +- Remote and transport: none; target-native runtime assembly, exact clean-build comparison, and + unpublished seven-day Actions artifacts only +- Exact evidence commands: + + ```sh + gh run view 29365815434 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh api repos/stablyai/orca/actions/jobs/87196982776/logs + gh api repos/stablyai/orca/actions/jobs/87196982746/logs + gh api 'repos/stablyai/orca/actions/runs/29365815434/artifacts?per_page=100' + ``` + +- Result: PASS. Both clean x64 builds report one generated and actual `/INCREMENTAL:NO`, retain + `/guard:cf`, parse a 2,146-byte UTF-16LE command record, search 79 bounded Release-tree entries, + and find no target `.ilk`. Both clean arm64 builds report the same contract with a 2,124-byte + command record and no target `.ilk`. Each architecture then executes bundled Node v24.18.0, + loads the exact ABI-137 native modules, proves PTY input/resize/exit and watcher + create/update/delete, settles Windows native resources, verifies the archive, and compares the + complete runtime tree, archive, identity, SPDX, and provenance exactly before upload. No PE + mismatch diagnostic runs. +- Stable Windows identities: x64 content ID + `sha256:6f7cbeb120e67766037649f6079099346220973e6158e1429b6ebf42729f1564` and archive + `sha256:d24ca7ee8734e948c845d792ba0974a2590df7c8b60068a46f3eb1dc24af5f36`; arm64 content ID + `sha256:741765a10ddc824cd305b9a50c8efd91477517c05d9cfe1ca46342c002652186` and archive + `sha256:3e1e89234cd0d139a2a1376e7cd5e17ab47e31a6c3c2f9dcb8e73317ead5c6aa`. +- Uploaded evidence: exactly six unpublished artifacts: Windows arm64 `8324239473`, 33,083,333 + bytes, `sha256:51d590835597969b1261f81487eea433f23e1c04818c96f2f1e5d48b16688b42`; + Windows x64 `8324177555`, 37,033,210 bytes, + `sha256:3bde38ab4159bea7b420118862db3725ce845084ab515cc3e08de439aceb308a`; Linux arm64 + `8324115839`, 28,209,475 bytes, + `sha256:4f7272d896616326a5231170054a71134122c0263200361cf46431951fe01307`; Linux x64 + `8324100611`, 29,286,004 bytes, + `sha256:346afca964edbe932f868a8ff00e13adba7d3d0c489ef0bdce8ca275134dc27e`; macOS x64 + `8324167852`, 26,427,328 bytes, + `sha256:0d265ebc18798680de250955daf9038fded4a45c596cb6609f847eef328d9668`; and macOS arm64 + `8324088333`, 24,713,338 bytes, + `sha256:aed95365e3cf309f723142eb6638caad2faf46cacde60661c95cffb1703734cf`. +- Duration and resource metrics: jobs ran 8m39s Windows arm64, 6m07s Windows x64, 3m42s Linux + arm64, 3m10s Linux x64, 5m45s macOS x64, and 2m43s macOS arm64. Windows x64 clean builds took + 127,019.748 ms and 80,495.086 ms; smoke took 5,383.956 ms at 53,903,360-byte RSS and + 5,343.569 ms at 53,874,688-byte RSS. Windows arm64 clean builds took 139,406.185 ms and + 123,440.275 ms; smoke took 5,782.042 ms at 51,310,592-byte RSS and 5,463.552 ms at + 51,372,032-byte RSS. SSH channels/files, cancellation, fallback delay, and connection latency + remain outside this artifact-only run. +- Artifact/log/trace link: run/jobs and six unpublished artifacts above; no release asset was + published and no production consumer exists +- Oracle proved: target-native generated/actual full-link settings, absence of incremental-link + databases, all-six clean-build reproducibility, exact archive/identity/SBOM/provenance equality, + complete candidate runtime smoke, strict comparison, and successful evidence upload. The prior + Windows arm64 thunk drift is closed at the copied producer without byte normalization. +- Does not prove: oldest supported OS/kernel/libc execution, native code-signing trust, packaged + desktop embedding, SSH/SFTP/system-SSH transfer, remote install/launch, fallback, UI, performance, + publication, or any enabled tuple. +- Checklist items satisfied: current-candidate native clean-build reproducibility and the exact + bundled Node/native PTY/watcher archive-execution rule. No tuple or production path is enabled. +- Follow-up: audit the remaining runtime closure/SBOM/provenance claims against the exact staged + allowlist and purpose tests, then address oldest-baseline and native-trust gates. Preserve the + artifact-only boundary and all legacy/default behavior. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -5894,7 +5983,7 @@ owner and promotion condition. | Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | | No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | | Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | -| Native clean-rebuild identity unproved | Five controls pass; copied `/INCREMENTAL:NO` is locally green | Toolchain drift may change native content IDs | Codex implementation owner | Prove flag, no `.ilk`, and all-six equality on native runners | Open | +| Native clean-rebuild identity unproved | All six native cells pass exact clean-build equality | Historical blocker is closed | Codex implementation owner | Met by E-M3-REPRODUCIBILITY-CI-001 | CLOSED | | Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | | Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | | Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | @@ -5941,11 +6030,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push exact implementation commit `6dfdfa0bd` plus its evidence-ledger head, then rerun both Windows -architectures and all four POSIX controls. Require both Windows clean builds to report exactly one -`/INCREMENTAL:NO`, no target `.ilk`, complete smoke, exact output equality, and upload. Retain -`/guard:cf`, strict parsing/comparison, and rejected-output no-upload. -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 publication, desktop resolver, SSH transfer/install, per-target -Beta, fallback, tuple enablement, or default behavior may be connected by this package. +Audit the exact staged runtime closure, licenses, SPDX, provenance, compiler/toolchain records, and +prohibited-content allowlist against the current candidate bytes. Add or tighten purpose-named tests +for every unsupported claim and record exact evidence before checking the remaining Milestone 3 +items. Then proceed to oldest-baseline and native-trust proof. + +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 +publication, desktop resolver, SSH transfer/install, per-target Beta, fallback, tuple enablement, or +default behavior may be connected by this package. From ec5461affb5ad7b9dd94f0312aa80edc29c3cca7 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 16:52:32 -0400 Subject: [PATCH 055/267] build(ssh): enforce exact relay runtime closure Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 8 + config/scripts/ssh-relay-runtime-closure.mjs | 252 ++++++++++++++++++ .../ssh-relay-runtime-closure.test.mjs | 179 +++++++++++++ config/scripts/ssh-relay-runtime-tree.mjs | 55 +--- .../ssh-relay-runtime-workflow.test.mjs | 4 + 5 files changed, 451 insertions(+), 47 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-closure.mjs create mode 100644 config/scripts/ssh-relay-runtime-closure.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 6fe78588d0c..428196d010d 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -107,6 +107,8 @@ jobs: node --check config/scripts/ssh-relay-runtime-reproducibility.test.mjs node --check config/scripts/build-ssh-relay-runtime.mjs node --check config/scripts/ssh-relay-runtime-build.test.mjs + node --check config/scripts/ssh-relay-runtime-closure.mjs + node --check config/scripts/ssh-relay-runtime-closure.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 \ @@ -116,6 +118,7 @@ jobs: config/scripts/ssh-relay-node-zip-inspection.test.mjs \ config/scripts/ssh-relay-runtime-artifact.test.mjs \ config/scripts/ssh-relay-runtime-build.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 \ config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ @@ -279,6 +282,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime build script syntax check failed' } node --check config/scripts/ssh-relay-runtime-build.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime build test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-closure.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime closure script syntax check failed' } + node --check config/scripts/ssh-relay-runtime-closure.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime closure 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 ` @@ -288,6 +295,7 @@ jobs: config/scripts/ssh-relay-node-zip-inspection.test.mjs ` config/scripts/ssh-relay-runtime-artifact.test.mjs ` config/scripts/ssh-relay-runtime-build.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 ` config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs ` diff --git a/config/scripts/ssh-relay-runtime-closure.mjs b/config/scripts/ssh-relay-runtime-closure.mjs new file mode 100644 index 00000000000..b6dfa9aa5a6 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-closure.mjs @@ -0,0 +1,252 @@ +import { readFile } from 'node:fs/promises' +import { isDeepStrictEqual } from 'node:util' +import { join } from 'node:path' + +const NODE_VERSION = '24.18.0' +const NODE_PTY_VERSION = '1.1.0' +const PARCEL_WATCHER_VERSION = '2.5.6' + +const WATCHER_PACKAGES = Object.freeze({ + 'linux-x64-glibc': '@parcel/watcher-linux-x64-glibc', + 'linux-arm64-glibc': '@parcel/watcher-linux-arm64-glibc', + 'darwin-x64': '@parcel/watcher-darwin-x64', + 'darwin-arm64': '@parcel/watcher-darwin-arm64', + 'win32-x64': '@parcel/watcher-win32-x64', + 'win32-arm64': '@parcel/watcher-win32-arm64' +}) + +const COMMON_FILES = Object.freeze([ + '.version', + 'THIRD_PARTY_LICENSES.txt', + 'node_modules/@parcel/watcher/index.js', + 'node_modules/@parcel/watcher/package.json', + 'node_modules/@parcel/watcher/wrapper.js', + 'node_modules/detect-libc/lib/detect-libc.js', + 'node_modules/detect-libc/lib/elf.js', + 'node_modules/detect-libc/lib/filesystem.js', + 'node_modules/detect-libc/lib/process.js', + 'node_modules/detect-libc/package.json', + 'node_modules/is-extglob/index.js', + 'node_modules/is-extglob/package.json', + 'node_modules/is-glob/index.js', + 'node_modules/is-glob/package.json', + 'node_modules/node-pty/lib/eventEmitter2.js', + 'node_modules/node-pty/lib/index.js', + 'node_modules/node-pty/lib/terminal.js', + 'node_modules/node-pty/lib/utils.js', + 'node_modules/node-pty/package.json', + 'node_modules/picomatch/index.js', + 'node_modules/picomatch/lib/constants.js', + 'node_modules/picomatch/lib/parse.js', + 'node_modules/picomatch/lib/picomatch.js', + 'node_modules/picomatch/lib/scan.js', + 'node_modules/picomatch/lib/utils.js', + 'node_modules/picomatch/package.json', + 'relay-watcher.js', + 'relay.js', + 'runtime-metadata.json' +]) + +const POSIX_NODE_PTY_FILES = Object.freeze([ + 'node_modules/node-pty/build/Release/pty.node', + 'node_modules/node-pty/lib/unixTerminal.js' +]) + +const WINDOWS_NODE_PTY_FILES = Object.freeze([ + 'node_modules/node-pty/build/Release/conpty.node', + 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe', + 'node_modules/node-pty/build/Release/conpty/conpty.dll', + 'node_modules/node-pty/build/Release/conpty_console_list.node', + 'node_modules/node-pty/lib/conpty_console_list_agent.js', + 'node_modules/node-pty/lib/shared/conout.js', + 'node_modules/node-pty/lib/windowsConoutConnection.js', + 'node_modules/node-pty/lib/windowsPtyAgent.js', + 'node_modules/node-pty/lib/windowsTerminal.js', + 'node_modules/node-pty/lib/worker/conoutSocketWorker.js' +]) + +export function sshRelayRuntimeWatcherPackage(tuple) { + const packageName = WATCHER_PACKAGES[tuple] + if (!packageName) { + throw new Error(`No bundled Parcel watcher package is defined for ${tuple}`) + } + return packageName +} + +export function sshRelayRuntimeFileRole(path) { + if (path === 'bin/node' || path === 'bin/node.exe') { + return 'node' + } + if (path === 'relay.js') { + return 'relay' + } + if (path === 'relay-watcher.js') { + return 'relay-watcher' + } + if (/\/(?:pty|conpty|conpty_console_list)\.node$/.test(path)) { + return 'node-pty-native' + } + if (path.endsWith('/watcher.node')) { + return 'parcel-watcher-native' + } + if ( + path.endsWith('/spawn-helper') || + path.endsWith('/conpty/conpty.dll') || + path.endsWith('/conpty/OpenConsole.exe') + ) { + return 'native-runtime' + } + if (path === 'THIRD_PARTY_LICENSES.txt') { + return 'license' + } + return 'runtime-javascript' +} + +function expectedFiles(tuple) { + const watcherPackage = sshRelayRuntimeWatcherPackage(tuple) + const paths = [ + ...COMMON_FILES, + tuple.startsWith('win32-') ? 'bin/node.exe' : 'bin/node', + `node_modules/${watcherPackage}/package.json`, + `node_modules/${watcherPackage}/watcher.node`, + ...(tuple.startsWith('win32-') ? WINDOWS_NODE_PTY_FILES : POSIX_NODE_PTY_FILES) + ] + if (tuple.startsWith('darwin-')) { + paths.push('node_modules/node-pty/build/Release/spawn-helper') + } + return paths.sort().map((path) => { + const role = sshRelayRuntimeFileRole(path) + const mode = ['node', 'node-pty-native', 'parcel-watcher-native', 'native-runtime'].includes( + role + ) + ? 0o755 + : 0o644 + return { path, type: 'file', role, mode } + }) +} + +function expectedDirectories(files) { + const paths = new Set() + for (const file of files) { + const segments = file.path.split('/') + for (let depth = 1; depth < segments.length; depth += 1) { + paths.add(segments.slice(0, depth).join('/')) + } + } + return [...paths].sort().map((path) => ({ path, type: 'directory', mode: 0o755 })) +} + +export function expectedSshRelayRuntimeClosureEntries(tuple) { + const files = expectedFiles(tuple) + return [...expectedDirectories(files), ...files] +} + +function indexEntries(entries, type) { + const indexed = new Map() + for (const entry of entries.filter((candidate) => candidate.type === type)) { + if (indexed.has(entry.path)) { + throw new Error(`Runtime closure contains a duplicate ${type}: ${entry.path}`) + } + indexed.set(entry.path, entry) + } + return indexed +} + +function assertExactEntries(actualEntries, expectedEntries, type) { + const actual = indexEntries(actualEntries, type) + const expected = indexEntries(expectedEntries, type) + for (const [path, contract] of expected) { + const entry = actual.get(path) + if (!entry) { + throw new Error(`Runtime closure is missing required ${type}: ${path}`) + } + for (const field of type === 'file' ? ['role', 'mode'] : ['mode']) { + if (entry[field] !== contract[field]) { + throw new Error(`Runtime closure ${type} has unexpected ${field}: ${path}`) + } + } + actual.delete(path) + } + if (actual.size > 0) { + throw new Error(`Runtime closure contains undeclared ${type}: ${actual.keys().next().value}`) + } +} + +export function assertSshRelayRuntimeClosureEntries(identity) { + if ( + identity.nodeVersion !== NODE_VERSION || + identity.dependencies?.nodePtyVersion !== NODE_PTY_VERSION || + identity.dependencies?.parcelWatcherVersion !== PARCEL_WATCHER_VERSION + ) { + // Why: dependency refreshes require a reviewed desktop release and a new immutable manifest. + throw new Error('Runtime closure dependency versions do not match the reviewed contract') + } + const expected = expectedSshRelayRuntimeClosureEntries(identity.tupleId) + assertExactEntries(identity.entries, expected, 'directory') + assertExactEntries(identity.entries, expected, 'file') +} + +function expectedPackages(tuple) { + const watcherPackage = sshRelayRuntimeWatcherPackage(tuple) + return [ + { name: 'node-pty', version: NODE_PTY_VERSION, main: './lib/index.js', license: 'MIT' }, + { name: '@parcel/watcher', version: PARCEL_WATCHER_VERSION, main: 'index.js', license: 'MIT' }, + { name: watcherPackage, version: PARCEL_WATCHER_VERSION, main: 'watcher.node', license: 'MIT' }, + { name: 'detect-libc', version: '2.1.2', main: 'lib/detect-libc.js', license: 'Apache-2.0' }, + { name: 'is-glob', version: '4.0.3', main: 'index.js', license: 'MIT' }, + { name: 'is-extglob', version: '2.1.1', main: 'index.js', license: 'MIT' }, + { name: 'picomatch', version: '4.0.4', main: 'index.js', license: 'MIT' } + ] +} + +function assertLicenseSections(text, packageNames) { + const matches = [...text.matchAll(/^===== ([^\r\n]+) =====$/gm)] + const names = matches.map((match) => match[1]) + if (!isDeepStrictEqual(names, ['Node.js', ...packageNames])) { + throw new Error('Runtime license bundle does not match the exact package closure') + } + for (let index = 0; index < matches.length; index += 1) { + const bodyStart = matches[index].index + matches[index][0].length + const bodyEnd = matches[index + 1]?.index ?? text.length + if (text.slice(bodyStart, bodyEnd).trim().length === 0) { + throw new Error(`Runtime license bundle has an empty section: ${matches[index][1]}`) + } + } +} + +export async function verifySshRelayRuntimeClosure(runtimeRoot, identity) { + assertSshRelayRuntimeClosureEntries(identity) + const packages = expectedPackages(identity.tupleId) + for (const expected of packages) { + const path = join(runtimeRoot, 'node_modules', ...expected.name.split('/'), 'package.json') + const actual = JSON.parse(await readFile(path, 'utf8')) + if (!isDeepStrictEqual(actual, expected)) { + throw new Error( + `Runtime package metadata does not match the closure contract: ${expected.name}` + ) + } + } + const [licenseText, versionText, metadataText] = await Promise.all([ + readFile(join(runtimeRoot, 'THIRD_PARTY_LICENSES.txt'), 'utf8'), + readFile(join(runtimeRoot, '.version'), 'utf8'), + readFile(join(runtimeRoot, 'runtime-metadata.json'), 'utf8') + ]) + assertLicenseSections( + licenseText, + packages.map((entry) => entry.name) + ) + const metadata = JSON.parse(metadataText) + if ( + metadata.schemaVersion !== 1 || + metadata.tuple !== identity.tupleId || + metadata.nodeVersion !== identity.nodeVersion || + metadata.relayBuildVersion !== versionText.trim() || + !isDeepStrictEqual(metadata.dependencies, { + nodePtyVersion: NODE_PTY_VERSION, + parcelWatcherVersion: PARCEL_WATCHER_VERSION + }) + ) { + throw new Error('Runtime metadata does not match the exact package closure') + } + return { files: identity.entries.filter((entry) => entry.type === 'file').length, packages: 7 } +} diff --git a/config/scripts/ssh-relay-runtime-closure.test.mjs b/config/scripts/ssh-relay-runtime-closure.test.mjs new file mode 100644 index 00000000000..d107a2da4c7 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-closure.test.mjs @@ -0,0 +1,179 @@ +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { + assertSshRelayRuntimeClosureEntries, + expectedSshRelayRuntimeClosureEntries, + verifySshRelayRuntimeClosure +} from './ssh-relay-runtime-closure.mjs' + +const temporaryDirectories = [] + +afterEach(async () => { + await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true }))) +}) + +const watcherPackages = Object.freeze({ + 'linux-x64-glibc': '@parcel/watcher-linux-x64-glibc', + 'linux-arm64-glibc': '@parcel/watcher-linux-arm64-glibc', + 'darwin-x64': '@parcel/watcher-darwin-x64', + 'darwin-arm64': '@parcel/watcher-darwin-arm64', + 'win32-x64': '@parcel/watcher-win32-x64', + 'win32-arm64': '@parcel/watcher-win32-arm64' +}) + +function identity(tuple) { + return { + tupleId: tuple, + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries: expectedSshRelayRuntimeClosureEntries(tuple) + } +} + +function packageRecords(tuple) { + return [ + { name: 'node-pty', version: '1.1.0', main: './lib/index.js', license: 'MIT' }, + { name: '@parcel/watcher', version: '2.5.6', main: 'index.js', license: 'MIT' }, + { name: watcherPackages[tuple], version: '2.5.6', main: 'watcher.node', license: 'MIT' }, + { name: 'detect-libc', version: '2.1.2', main: 'lib/detect-libc.js', license: 'Apache-2.0' }, + { name: 'is-glob', version: '4.0.3', main: 'index.js', license: 'MIT' }, + { name: 'is-extglob', version: '2.1.1', main: 'index.js', license: 'MIT' }, + { name: 'picomatch', version: '4.0.4', main: 'index.js', license: 'MIT' } + ] +} + +async function writeClosureMetadata(tuple = 'linux-x64-glibc') { + const runtimeRoot = await mkdtemp(join(tmpdir(), 'orca-runtime-closure-')) + temporaryDirectories.push(runtimeRoot) + const packages = packageRecords(tuple) + for (const record of packages) { + const path = join(runtimeRoot, 'node_modules', ...record.name.split('/'), 'package.json') + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, `${JSON.stringify(record, null, 2)}\n`) + } + await writeFile(join(runtimeRoot, '.version'), '1.4.140+123456789abc\n') + await writeFile( + join(runtimeRoot, 'runtime-metadata.json'), + `${JSON.stringify( + { + schemaVersion: 1, + tuple, + nodeVersion: '24.18.0', + relayBuildVersion: '1.4.140+123456789abc', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' } + }, + null, + 2 + )}\n` + ) + const licenseNames = ['Node.js', ...packages.map((entry) => entry.name)] + await writeFile( + join(runtimeRoot, 'THIRD_PARTY_LICENSES.txt'), + `${licenseNames.map((name) => `===== ${name} =====\n${name} license text\n`).join('\n')}\n` + ) + return { runtimeRoot, packages } +} + +describe('SSH relay runtime exact closure', () => { + it.each([ + ['linux-x64-glibc', 34], + ['linux-arm64-glibc', 34], + ['darwin-x64', 35], + ['darwin-arm64', 35], + ['win32-x64', 42], + ['win32-arm64', 42] + ])('pins the reviewed %s runtime to %i files and one native watcher', (tuple, fileCount) => { + const entries = expectedSshRelayRuntimeClosureEntries(tuple) + const files = entries.filter((entry) => entry.type === 'file') + expect(files).toHaveLength(fileCount) + expect(files.filter((entry) => entry.role === 'parcel-watcher-native')).toEqual([ + expect.objectContaining({ path: `node_modules/${watcherPackages[tuple]}/watcher.node` }) + ]) + expect(files.map((entry) => entry.path)).not.toEqual( + expect.arrayContaining([ + expect.stringMatching(/(?:^|\/)(?:npm|npx|corepack|pnpm|yarn)(?:\.|\/|$)/i), + expect.stringMatching(/\.(?:map|pdb|ilk|cc|cpp|c|h|hpp|o|obj)$/i) + ]) + ) + expect(() => assertSshRelayRuntimeClosureEntries(identity(tuple))).not.toThrow() + }) + + it('rejects undeclared package managers, source maps, and native build outputs', () => { + const fixture = identity('linux-x64-glibc') + for (const path of [ + 'node_modules/npm/bin/npm-cli.js', + 'relay.js.map', + 'node_modules/node-pty/build/Release/pty.pdb' + ]) { + expect(() => + assertSshRelayRuntimeClosureEntries({ + ...fixture, + entries: [ + ...fixture.entries, + { path, type: 'file', role: 'runtime-javascript', mode: 0o644 } + ] + }) + ).toThrow(/undeclared file/i) + } + }) + + it('rejects a missing native dependency, wrong role, or unreviewed dependency refresh', () => { + const fixture = identity('darwin-arm64') + expect(() => + assertSshRelayRuntimeClosureEntries({ + ...fixture, + entries: fixture.entries.filter((entry) => !entry.path.endsWith('/watcher.node')) + }) + ).toThrow(/missing required file/i) + expect(() => + assertSshRelayRuntimeClosureEntries({ + ...fixture, + entries: fixture.entries.map((entry) => + entry.path === 'bin/node' ? { ...entry, role: 'runtime-javascript' } : entry + ) + }) + ).toThrow(/unexpected role/i) + expect(() => + assertSshRelayRuntimeClosureEntries({ ...fixture, nodeVersion: '24.19.0' }) + ).toThrow(/dependency versions/i) + }) + + it('verifies exact package metadata, runtime metadata, and non-empty license sections', async () => { + const fixture = await writeClosureMetadata() + await expect( + verifySshRelayRuntimeClosure(fixture.runtimeRoot, identity('linux-x64-glibc')) + ).resolves.toEqual({ files: 34, packages: 7 }) + + const packagePath = join(fixture.runtimeRoot, 'node_modules', 'node-pty', 'package.json') + const packageJson = JSON.parse(await readFile(packagePath, 'utf8')) + await writeFile(packagePath, `${JSON.stringify({ ...packageJson, version: '1.1.1' })}\n`) + await expect( + verifySshRelayRuntimeClosure(fixture.runtimeRoot, identity('linux-x64-glibc')) + ).rejects.toThrow(/node-pty/i) + }) + + it('rejects omitted and empty dependency license sections', async () => { + const fixture = await writeClosureMetadata() + const licensePath = join(fixture.runtimeRoot, 'THIRD_PARTY_LICENSES.txt') + const licenseText = await readFile(licensePath, 'utf8') + await writeFile(licensePath, licenseText.replace(/===== picomatch =====[\s\S]*$/, '')) + await expect( + verifySshRelayRuntimeClosure(fixture.runtimeRoot, identity('linux-x64-glibc')) + ).rejects.toThrow(/license bundle/i) + + const second = await writeClosureMetadata() + const secondLicensePath = join(second.runtimeRoot, 'THIRD_PARTY_LICENSES.txt') + const secondText = await readFile(secondLicensePath, 'utf8') + await writeFile( + secondLicensePath, + secondText.replace('===== picomatch =====\npicomatch license text', '===== picomatch =====') + ) + await expect( + verifySshRelayRuntimeClosure(second.runtimeRoot, identity('linux-x64-glibc')) + ).rejects.toThrow(/empty section/i) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-tree.mjs b/config/scripts/ssh-relay-runtime-tree.mjs index e524e3ea199..ffe15e33f05 100644 --- a/config/scripts/ssh-relay-runtime-tree.mjs +++ b/config/scripts/ssh-relay-runtime-tree.mjs @@ -4,6 +4,11 @@ import { createRequire } from 'node:module' import { dirname, join, relative, sep } from 'node:path' import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' +import { + sshRelayRuntimeFileRole, + sshRelayRuntimeWatcherPackage, + verifySshRelayRuntimeClosure +} from './ssh-relay-runtime-closure.mjs' const require = createRequire(import.meta.url) const MAX_FILES = 5_000 @@ -53,15 +58,6 @@ const COMPATIBILITY = Object.freeze({ } }) -const WATCHER_PACKAGES = Object.freeze({ - 'linux-x64-glibc': '@parcel/watcher-linux-x64-glibc', - 'linux-arm64-glibc': '@parcel/watcher-linux-arm64-glibc', - 'darwin-x64': '@parcel/watcher-darwin-x64', - 'darwin-arm64': '@parcel/watcher-darwin-arm64', - 'win32-x64': '@parcel/watcher-win32-x64', - 'win32-arm64': '@parcel/watcher-win32-arm64' -}) - function packageDirectory(name) { return dirname(require.resolve(`${name}/package.json`)) } @@ -176,10 +172,7 @@ async function copyPackageRuntime(name, runtimeRoot, relativePaths) { } async function copyParcelWatcher(runtimeRoot, tuple) { - const watcherPackage = WATCHER_PACKAGES[tuple] - if (!watcherPackage) { - throw new Error(`No bundled Parcel watcher package is defined for ${tuple}`) - } + const watcherPackage = sshRelayRuntimeWatcherPackage(tuple) await copyPackageRuntime('@parcel/watcher', runtimeRoot, ['index.js', 'wrapper.js']) await copyPackageRuntime(watcherPackage, runtimeRoot, ['watcher.node']) await chmod( @@ -221,39 +214,6 @@ async function writeLicenses(runtimeRoot, nodeRoot, packages) { await chmod(join(runtimeRoot, 'THIRD_PARTY_LICENSES.txt'), 0o644) } -function runtimeRole(path) { - if (path === 'bin/node' || path === 'bin/node.exe') { - return 'node' - } - if (path === 'relay.js') { - return 'relay' - } - if (path === 'relay-watcher.js') { - return 'relay-watcher' - } - if ( - path.endsWith('/pty.node') || - path.endsWith('/conpty.node') || - path.endsWith('/conpty_console_list.node') - ) { - return 'node-pty-native' - } - if (path.endsWith('/watcher.node')) { - return 'parcel-watcher-native' - } - if ( - path.endsWith('/spawn-helper') || - path.endsWith('/conpty/conpty.dll') || - path.endsWith('/conpty/OpenConsole.exe') - ) { - return 'native-runtime' - } - if (path === 'THIRD_PARTY_LICENSES.txt') { - return 'license' - } - return 'runtime-javascript' -} - function assertPortableRuntimePath(path) { const segments = path.split('/') if ( @@ -301,7 +261,7 @@ async function collectEntries(runtimeRoot) { if (metadata.size > MAX_FILE_BYTES) { throw new Error(`Runtime file exceeds size limit: ${path}`) } - const role = runtimeRole(path) + const role = sshRelayRuntimeFileRole(path) // Why: NTFS has no POSIX execute bits; ZIP metadata carries the canonical runtime mode. const mode = process.platform === 'win32' && @@ -392,5 +352,6 @@ export async function assembleSshRelayRuntimeTree({ dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, entries: tree.entries } + await verifySshRelayRuntimeClosure(runtimeRoot, identity) return { ...identity, contentId: computeSshRelayRuntimeContentId(identity), ...tree } } diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 553a01217e1..1815d229a8e 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -61,6 +61,10 @@ describe('SSH relay runtime artifact workflow', () => { expect(source).toContain('foreach ($output in @($firstOutput, $secondOutput))') expect(source).toContain('ssh-relay-runtime-reproducibility.mjs') expect(source).toContain('ssh-relay-runtime-reproducibility.test.mjs') + expect(source.match(/ssh-relay-runtime-closure\.test\.mjs/g)).toHaveLength(4) + expect( + source.match(/node --check config\/scripts\/ssh-relay-runtime-closure\.mjs/g) + ).toHaveLength(2) expect(source).toContain('ssh-relay-runtime-windows-pe-diagnostic.mjs') expect(source).toContain('ssh-relay-runtime-windows-pe-diagnostic.test.mjs') expect(source).toContain('llvm-objdump.exe') From ace3d4f416b1c710d601aa72c4b45824266b5cad Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 16:54:50 -0400 Subject: [PATCH 056/267] docs(ssh): record exact runtime closure gate Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 7 +- ...github-release-implementation-checklist.md | 130 +++++++++++++++--- 2 files changed, 115 insertions(+), 22 deletions(-) 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 b5945925010..6f64636b468 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 @@ -18,8 +18,9 @@ Approved design decisions are listed separately and are not presented as impleme - Latest native proof: [GitHub Actions run 29365815434](https://github.com/stablyai/orca/actions/runs/29365815434) passed clean-build equality and runtime smoke on Linux x64/arm64, macOS x64/arm64, and Windows x64/arm64. -- Active work package: finish the artifact/runtime closure audit, then address archive safety, - oldest-baseline execution, and native trust. +- Active work package: exact runtime closure enforcement is locally green and awaiting all-six + native proof; SBOM/provenance completeness, archive safety, oldest-baseline execution, and native + trust remain open. ## Locked rollout decisions @@ -73,6 +74,8 @@ Approved design decisions are listed separately and are not presented as impleme - [x] Prove bundled Node, real PTY input/resize/exit, watcher events, and Windows resource settlement. - [x] Prove two clean builds compare exactly on all six native GitHub runner families. - [x] Keep `/guard:cf`; fix Windows arm64 drift with copied-artifact-only `/INCREMENTAL:NO`. +- [x] Define and locally test an exact 34/35/42-file per-tuple closure that rejects undeclared + package managers, sources, maps, build outputs, package drift, and missing licenses. - [ ] Complete the runtime closure, license, SBOM, provenance, and prohibited-content allowlist audit. - [ ] Prove each candidate on its oldest supported OS/libc/kernel baseline. - [ ] Sign macOS and Windows native bytes and verify target-native trust policy. 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 88a07cc83e2..a5d9228a9db 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 runtime closure — exact-head run [29365815434](https://github.com/stablyai/orca/actions/runs/29365815434) at ledger head `3ec1a48af` passes all six target-native jobs under E-M3-REPRODUCIBILITY-CI-001; both Windows clean builds propagate exactly one copied-artifact `/INCREMENTAL:NO`, retain `/guard:cf`, emit no target `.ilk`, complete bundled-Node/native/PTy/watcher smoke, compare the full runtime/archive/identity/SBOM/provenance exactly, and upload; the Windows arm64 2,879-thunk drift is closed without normalization or a repository-wide change; the active artifact-only slice is auditing and closing the remaining runtime closure/SBOM/provenance checklist claims before oldest-baseline and native-trust work; production/default behavior and every tuple state remain unchanged; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 runtime closure — exact implementation commit `ec5461aff` moves the exact per-tuple closure/prohibited-content/package/license gate into the artifact builder and is locally green under E-M3-RUNTIME-CLOSURE-LOCAL-001; all six prior candidate archives from run [29365815434](https://github.com/stablyai/orca/actions/runs/29365815434) satisfy the new 34/35/42-file closure, but the new builder gate has not yet run target-natively and no Milestone 3 completion box is closed by local/static reuse alone; the next exact-head six-cell run must retain the proven content IDs, enforce the closure before archiving, complete smoke and clean-build equality, and upload before this slice is accepted; SBOM/provenance completeness, compiler/toolchain records, oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      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) @@ -175,11 +175,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: audit the exact staged runtime closure, licenses, SPDX, provenance, - compiler/toolchain records, and prohibited-content allowlist against the current candidate bytes. - Check only claims supported by exact purpose tests and evidence IDs, then proceed to the - oldest-baseline and native-trust gates. Preserve the artifact-only boundary, legacy/default path, - and every other release gate. +- Next required action: push exact implementation commit `ec5461aff` plus its evidence-ledger head + and rerun all six target-native cells. Require the builder-enforced closure, unchanged runtime + content IDs, complete smoke, clean-build equality, and upload before checking the related + Milestone 3 claims. Then fix the separately identified SBOM/provenance/toolchain gaps. Preserve the + artifact-only boundary, legacy/default path, and every other release gate. ## Non-Negotiable Invariants @@ -763,6 +763,14 @@ the full bundled runtime smoke, compare every runtime/archive/identity/SBOM/prov and upload. Windows arm64 now has one stable content ID and archive digest, closing the native clean-build reproducibility gap without post-build normalization (E-M3-REPRODUCIBILITY-CI-001). +Exact implementation commit `ec5461aff` replaces workflow-only prohibited-file checks as the +authoritative boundary with an exact builder-enforced per-tuple closure. It pins 34 Linux, 35 macOS, +and 42 Windows files; exact package metadata and dependency versions; exactly one tuple-native +watcher; file roles/modes; runtime metadata; and ordered, non-empty license sections. The local +hostile suite and all six downloaded prior candidate archives pass under +E-M3-RUNTIME-CLOSURE-LOCAL-001. Target-native execution of the new gate remains required before the +related implementation claims are checked. + Each runtime must contain only the executable closure required by the relay. - [ ] Replace or extend `config/scripts/build-relay.mjs` without weakening its existing relay and @@ -1367,16 +1375,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; all-six reproducibility green; closure/trust pending | `3ec1a48af`; E-M3-REPRODUCIBILITY-CI-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; exact closure locally green; native closure/trust pending | `ec5461aff`; E-M3-RUNTIME-CLOSURE-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 | — | Every PR must document: @@ -1433,6 +1441,7 @@ focused commands as their scripts/tests are introduced. - [x] `node config/scripts/verify-ssh-relay-node-release-inputs.mjs --inputs-directory --archive win32-x64` (E-M3-WINDOWS-INPUT-001; signed real Node ZIP, headers, and import library; no Windows execution) - [x] `node config/scripts/build-ssh-relay-runtime.mjs --tuple linux-arm64-glibc --inputs-directory --output-directory --work-directory --source-date-epoch --git-commit ` (E-M3-RUNTIME-LOCAL-001, E-M3-REPRODUCIBILITY-LINKER-LOCAL-001; local native Linux arm64 history plus current stable-work contract) - [x] `node config/scripts/verify-ssh-relay-runtime.mjs --runtime-directory --identity --archive ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) +- [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) ### Commands/scripts that must be added or formally identified @@ -5973,6 +5982,86 @@ the bounded depth` before opening or parsing a candidate. Starting at the comple allowlist and purpose tests, then address oldest-baseline and native-trust gates. Preserve the artifact-only boundary and all legacy/default behavior. +### E-M3-RUNTIME-CLOSURE-LOCAL-001 — Exact per-tuple closure fails before archiving + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `ec5461aff61d6868c18d4db1ce27f409a43ecf47`; stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: local macOS 26.2 build 25C56, Darwin 25.2.0 arm64 on Apple Silicon; Node v26.0.0 and + pnpm 10.24.0 for the pure contract suite. Candidate inputs came from all six target-native jobs in + prior run [29365815434](https://github.com/stablyai/orca/actions/runs/29365815434); the new builder + gate has not yet executed on those native runners. +- Remote and transport: none; artifact-only local validation of downloaded unpublished evidence +- Exact evidence commands: + + ```sh + gh run download 29365815434 --repo stablyai/orca \ + --dir /tmp/orca-ssh-relay-runtime-29365815434 + for archive in /tmp/orca-ssh-relay-runtime-29365815434/*/*.tar.xz; do + tar -tf "$archive" + done + for archive in /tmp/orca-ssh-relay-runtime-29365815434/*/*.zip; do + unzip -Z1 "$archive" + done + rm -rf /tmp/orca-ssh-relay-runtime-29365815434/extracted + mkdir -p /tmp/orca-ssh-relay-runtime-29365815434/extracted + for directory in /tmp/orca-ssh-relay-runtime-29365815434/ssh-relay-runtime-*; do + tuple=${directory##*/ssh-relay-runtime-} + destination=/tmp/orca-ssh-relay-runtime-29365815434/extracted/$tuple + mkdir -p "$destination" + archive=$(find "$directory" -maxdepth 1 -type f \ + \( -name '*.tar.xz' -o -name '*.zip' \) -print -quit) + case "$archive" in + *.tar.xz) tar -xf "$archive" -C "$destination" ;; + *.zip) unzip -q "$archive" -d "$destination" ;; + esac + done + node --input-type=module -e 'import {readdir,readFile} from "node:fs/promises"; import {join} from "node:path"; import {verifySshRelayRuntimeClosure} from "./config/scripts/ssh-relay-runtime-closure.mjs"; const root="/tmp/orca-ssh-relay-runtime-29365815434"; for (const dir of (await readdir(root)).filter((name)=>name.startsWith("ssh-relay-runtime-")).sort()) { const tuple=dir.slice("ssh-relay-runtime-".length); const names=await readdir(join(root,dir)); const identity=JSON.parse(await readFile(join(root,dir,names.find((name)=>name.endsWith(".identity.json"))),"utf8")); const result=await verifySshRelayRuntimeClosure(join(root,"extracted",tuple),identity); console.log(JSON.stringify({tuple,...result,contentId:identity.contentId})); }' + pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-*.test.mjs + pnpm exec oxlint config/scripts/ssh-relay-runtime-closure.mjs \ + config/scripts/ssh-relay-runtime-closure.test.mjs \ + config/scripts/ssh-relay-runtime-tree.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm exec oxfmt --check .github/workflows/ssh-relay-runtime-artifacts.yml \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-closure.mjs \ + config/scripts/ssh-relay-runtime-closure.test.mjs \ + config/scripts/ssh-relay-runtime-tree.mjs + pnpm run check:max-lines-ratchet + git diff --check + ``` + +- Result: PASS. Sixteen artifact test files passed 68 tests. The new closure suite fixes the exact + candidate file counts at 34 for Linux, 35 for macOS, and 42 for Windows; admits exactly one + tuple-native `@parcel/watcher@2.5.6` package; pins `node-pty@1.1.0` and all runtime JavaScript + dependency metadata; requires canonical roles/modes, runtime metadata, and eight ordered, + non-empty license sections; and rejects an undeclared package manager, source map, PDB, missing + native dependency, wrong role, version refresh, omitted license, or empty license before archive + creation. Focused syntax, lint, formatting, max-lines ratchet, and diff checks passed. +- Downloaded-candidate audit: all six prior candidate identities and extracted metadata satisfy the + new contract without changing their content IDs: Linux x64 + `sha256:960546cd96c67fcf9bb0a61e96ecdbecbffd9104d3a495578f8bb19dd810649a`, Linux arm64 + `sha256:aa3aa8ae8b42334ba7b0dbe5c43fd1184e36b3f4f4a9bec0e990e9b78f090756`, macOS x64 + `sha256:585ea6034cdd07487d8667059f975a877c795a45dc0d6eeee1617f2e3749faa2`, macOS arm64 + `sha256:40ff5d2036784b794e7b09f78596409f63f3145280c530bece5280d40897f6cb`, Windows x64 + `sha256:6f7cbeb120e67766037649f6079099346220973e6158e1429b6ebf42729f1564`, and Windows arm64 + `sha256:741765a10ddc824cd305b9a50c8efd91477517c05d9cfe1ca46342c002652186`. +- Oracle proved: the artifact builder now has one cross-platform, fail-closed, exact closure boundary + rather than depending on incomplete POSIX/PowerShell workflow blocklists; dependency refreshes and + new runtime files require an explicit reviewed contract change; the prior six candidate trees + conform to that contract. +- Does not prove: execution of this new gate on any target-native runner, that the current SBOM has + complete package/file relationships, complete compiler/toolchain/image provenance, oldest- + baseline execution, native trust, SSH transfer/install, publication, fallback, UI, or any enabled + tuple. +- Checklist items satisfied: local exact-closure/prohibited-content implementation and purpose tests + only. The broader Milestone 3 closure/SBOM/provenance items remain unchecked until the new gate + passes all six native cells and the metadata audit closes its residual gaps. +- Follow-up: push the implementation plus this exact evidence ledger, rerun all six native jobs, and + require unchanged runtime content IDs, builder-enforced closure, complete smoke, clean-build + equality, and upload. Then fix and prove SBOM/provenance completeness separately. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -6030,10 +6119,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Audit the exact staged runtime closure, licenses, SPDX, provenance, compiler/toolchain records, and -prohibited-content allowlist against the current candidate bytes. Add or tighten purpose-named tests -for every unsupported claim and record exact evidence before checking the remaining Milestone 3 -items. Then proceed to oldest-baseline and native-trust proof. +Push exact implementation commit `ec5461aff` plus its evidence-ledger head and rerun all six target- +native cells. Require the builder-enforced exact closure, unchanged runtime content IDs, complete +smoke, clean-build equality, and upload before checking the related Milestone 3 claims. Then correct +and prove SBOM/provenance/toolchain completeness as a separate artifact-only slice before proceeding +to oldest-baseline and native-trust proof. 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 From 26bbd7b67a035cdf309ccdbbaab5985f4cc797fe Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 17:06:44 -0400 Subject: [PATCH 057/267] build(ssh): complete runtime artifact provenance Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 25 ++ config/scripts/build-ssh-relay-runtime.mjs | 40 +-- .../scripts/ssh-relay-runtime-provenance.mjs | 78 +----- .../ssh-relay-runtime-provenance.test.mjs | 158 +++++++++++ config/scripts/ssh-relay-runtime-sbom.mjs | 125 +++++++++ .../scripts/ssh-relay-runtime-sbom.test.mjs | 122 +++++++++ .../scripts/ssh-relay-runtime-toolchain.mjs | 252 ++++++++++++++++++ .../ssh-relay-runtime-toolchain.test.mjs | 112 ++++++++ .../ssh-relay-runtime-workflow.test.mjs | 4 + 9 files changed, 815 insertions(+), 101 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-provenance.test.mjs create mode 100644 config/scripts/ssh-relay-runtime-sbom.mjs create mode 100644 config/scripts/ssh-relay-runtime-sbom.test.mjs create mode 100644 config/scripts/ssh-relay-runtime-toolchain.mjs create mode 100644 config/scripts/ssh-relay-runtime-toolchain.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 428196d010d..b811e9c7998 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -43,6 +43,8 @@ jobs: node_archive: node-v24.18.0-darwin-arm64.tar.xz runs-on: ${{ matrix.runner }} timeout-minutes: 20 + env: + ORCA_RUNTIME_REQUESTED_RUNNER: ${{ matrix.runner }} steps: - name: Checkout exact source revision @@ -109,6 +111,11 @@ jobs: node --check config/scripts/ssh-relay-runtime-build.test.mjs node --check config/scripts/ssh-relay-runtime-closure.mjs node --check config/scripts/ssh-relay-runtime-closure.test.mjs + node --check config/scripts/ssh-relay-runtime-sbom.mjs + node --check config/scripts/ssh-relay-runtime-sbom.test.mjs + node --check config/scripts/ssh-relay-runtime-provenance.test.mjs + node --check config/scripts/ssh-relay-runtime-toolchain.mjs + node --check config/scripts/ssh-relay-runtime-toolchain.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 \ @@ -122,6 +129,9 @@ jobs: config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs \ + config/scripts/ssh-relay-runtime-provenance.test.mjs \ + config/scripts/ssh-relay-runtime-sbom.test.mjs \ + config/scripts/ssh-relay-runtime-toolchain.test.mjs \ config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ config/scripts/ssh-relay-runtime-zip.test.mjs \ config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -208,6 +218,8 @@ jobs: msvc_arch: arm64 runs-on: ${{ matrix.runner }} timeout-minutes: 30 + env: + ORCA_RUNTIME_REQUESTED_RUNNER: ${{ matrix.runner }} steps: - name: Checkout exact source revision @@ -286,6 +298,16 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime closure script syntax check failed' } node --check config/scripts/ssh-relay-runtime-closure.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime closure test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-sbom.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime SBOM script syntax check failed' } + node --check config/scripts/ssh-relay-runtime-sbom.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime SBOM test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-provenance.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime provenance test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-toolchain.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime toolchain script syntax check failed' } + node --check config/scripts/ssh-relay-runtime-toolchain.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime toolchain 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 ` @@ -299,6 +321,9 @@ jobs: config/scripts/ssh-relay-runtime-pty-smoke.test.mjs ` config/scripts/ssh-relay-runtime-reproducibility.test.mjs ` config/scripts/ssh-relay-runtime-resource-diagnostics.test.mjs ` + config/scripts/ssh-relay-runtime-provenance.test.mjs ` + config/scripts/ssh-relay-runtime-sbom.test.mjs ` + config/scripts/ssh-relay-runtime-toolchain.test.mjs ` config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs ` config/scripts/ssh-relay-runtime-windows-tree.test.mjs ` config/scripts/ssh-relay-runtime-zip.test.mjs ` diff --git a/config/scripts/build-ssh-relay-runtime.mjs b/config/scripts/build-ssh-relay-runtime.mjs index 2ae076aafc0..46906f11591 100644 --- a/config/scripts/build-ssh-relay-runtime.mjs +++ b/config/scripts/build-ssh-relay-runtime.mjs @@ -11,6 +11,11 @@ import { inspectSshRelayRuntimeArchive } from './ssh-relay-runtime-archive.mjs' import { writeSshRelayRuntimeMetadata } from './ssh-relay-runtime-provenance.mjs' +import { + collectSshRelayRuntimeToolchain, + sshRelayRuntimeBuilderIdentity, + sshRelayRuntimeRunnerIdentity +} from './ssh-relay-runtime-toolchain.mjs' import { assembleSshRelayRuntimeTree } from './ssh-relay-runtime-tree.mjs' import { validateSshRelayNodeReleaseContract } from './ssh-relay-node-release-verification.mjs' import { verifyNodeReleaseInputs } from './verify-ssh-relay-node-release-inputs.mjs' @@ -130,36 +135,6 @@ async function run(command, args, options = {}) { return result.stdout.trim() } -async function toolchainDetails(nodePath) { - const commands = - process.platform === 'win32' - ? [ - ['bundledNode', nodePath, ['--version']], - ['compiler', 'cl.exe', []], - ['python', 'python.exe', ['--version']] - ] - : [ - ['bundledNode', nodePath, ['--version']], - ['xz', 'xz', ['--version']], - ['compiler', 'c++', ['--version']], - [ - 'strip', - process.platform === 'darwin' ? 'xcrun' : 'strip', - process.platform === 'darwin' ? ['--find', 'strip'] : ['--version'] - ], - ['python', 'python3', ['--version']] - ] - const details = {} - for (const [name, command, args] of commands) { - details[name] = (await run(command, args)).split(/\r?\n/)[0] - } - if (process.platform === 'win32') { - details.zip = 'yazl 3.3.1' - } - details.buildNode = process.version - return details -} - function relayPlatform(tuple) { return tuple.replace('-glibc', '') } @@ -241,7 +216,7 @@ export async function buildSshRelayRuntime(options) { sourceDateEpoch: options.sourceDateEpoch }) const inspection = await inspectSshRelayRuntimeArchive(archive.path, identity) - const toolchain = await toolchainDetails(extracted.nodePath) + const toolchain = await collectSshRelayRuntimeToolchain(extracted.nodePath) const metadata = await writeSshRelayRuntimeMetadata({ outputDirectory: options.outputDirectory, identity, @@ -249,7 +224,8 @@ export async function buildSshRelayRuntime(options) { nodeRelease: release, sourceDateEpoch: options.sourceDateEpoch, gitCommit: options.gitCommit, - builder: process.env.GITHUB_WORKFLOW_REF ?? `local://${process.platform}/${process.arch}`, + builder: sshRelayRuntimeBuilderIdentity({ gitCommit: options.gitCommit }), + runner: sshRelayRuntimeRunnerIdentity(), toolchain }) return { diff --git a/config/scripts/ssh-relay-runtime-provenance.mjs b/config/scripts/ssh-relay-runtime-provenance.mjs index 9582dd2256c..ee460e39fea 100644 --- a/config/scripts/ssh-relay-runtime-provenance.mjs +++ b/config/scripts/ssh-relay-runtime-provenance.mjs @@ -1,43 +1,14 @@ import { createHash } from 'node:crypto' import { readFile, stat, writeFile } from 'node:fs/promises' -import { createRequire } from 'node:module' import { join } from 'node:path' -const require = createRequire(import.meta.url) +import { createSshRelayRuntimeSbom } from './ssh-relay-runtime-sbom.mjs' +import { assertSshRelayRuntimeToolchain } from './ssh-relay-runtime-toolchain.mjs' function sha256(bytes) { return `sha256:${createHash('sha256').update(bytes).digest('hex')}` } -function spdxId(value) { - return `SPDXRef-${value.replace(/[^A-Za-z0-9.-]/g, '-')}` -} - -function packageRecord(name, version, license) { - return { - name, - SPDXID: spdxId(`Package-${name}`), - versionInfo: version, - downloadLocation: 'NOASSERTION', - filesAnalyzed: false, - licenseConcluded: license, - licenseDeclared: license, - copyrightText: 'NOASSERTION' - } -} - -function installedPackage(name) { - const parsed = require(`${name}/package.json`) - return packageRecord(parsed.name, parsed.version, parsed.license ?? 'NOASSERTION') -} - -function watcherNativePackage(tuple) { - if (tuple.startsWith('linux-')) { - return `@parcel/watcher-linux-${tuple.slice('linux-'.length)}` - } - return `@parcel/watcher-${tuple}` -} - async function writeJson(path, value) { const bytes = Buffer.from(`${JSON.stringify(value, null, 2)}\n`, 'utf8') await writeFile(path, bytes, { flag: 'wx', mode: 0o600 }) @@ -52,48 +23,14 @@ export async function writeSshRelayRuntimeMetadata({ sourceDateEpoch, gitCommit, builder, + runner, toolchain }) { - const createdAt = new Date(sourceDateEpoch * 1000).toISOString() + assertSshRelayRuntimeToolchain(toolchain, identity.tupleId) const sbomName = `orca-ssh-relay-runtime-${identity.tupleId}.spdx.json` const provenanceName = `orca-ssh-relay-runtime-${identity.tupleId}.provenance.json` const identityName = `orca-ssh-relay-runtime-${identity.tupleId}.identity.json` - const files = identity.entries - .filter((entry) => entry.type === 'file') - .map((entry, index) => ({ - fileName: entry.path, - SPDXID: `SPDXRef-File-${index + 1}`, - checksums: [{ algorithm: 'SHA256', checksumValue: entry.sha256.slice('sha256:'.length) }], - licenseConcluded: 'NOASSERTION', - copyrightText: 'NOASSERTION' - })) - const packages = [ - packageRecord('node', identity.nodeVersion, 'NOASSERTION'), - packageRecord('orca-ssh-relay', '0.1.0', 'NOASSERTION'), - installedPackage('node-pty'), - installedPackage('@parcel/watcher'), - installedPackage(watcherNativePackage(identity.tupleId)), - installedPackage('detect-libc'), - installedPackage('is-glob'), - installedPackage('is-extglob'), - installedPackage('picomatch') - ] - const documentId = identity.contentId.slice('sha256:'.length) - const sbom = { - spdxVersion: 'SPDX-2.3', - dataLicense: 'CC0-1.0', - SPDXID: 'SPDXRef-DOCUMENT', - name: `Orca SSH relay runtime ${identity.tupleId}`, - documentNamespace: `https://github.com/stablyai/orca/ssh-relay-runtime/${documentId}`, - creationInfo: { created: createdAt, creators: ['Organization: Stably AI'] }, - packages, - files, - relationships: packages.map((component) => ({ - spdxElementId: 'SPDXRef-DOCUMENT', - relationshipType: 'DESCRIBES', - relatedSpdxElement: component.SPDXID - })) - } + const sbom = createSshRelayRuntimeSbom({ identity, archive, sourceDateEpoch }) const nativeFiles = identity.entries .filter( (entry) => @@ -139,7 +76,10 @@ export async function writeSshRelayRuntimeMetadata({ }, runDetails: { builder: { id: builder }, - metadata: { invocationId: process.env.GITHUB_RUN_ID ?? 'local-unpublished-build' }, + metadata: { + invocationId: process.env.GITHUB_RUN_ID ?? 'local-unpublished-build', + runner + }, byproducts: [{ name: 'native-files', content: nativeFiles }] } } diff --git a/config/scripts/ssh-relay-runtime-provenance.test.mjs b/config/scripts/ssh-relay-runtime-provenance.test.mjs new file mode 100644 index 00000000000..ba5340d6704 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-provenance.test.mjs @@ -0,0 +1,158 @@ +import { mkdtemp, readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { writeSshRelayRuntimeMetadata } from './ssh-relay-runtime-provenance.mjs' + +const temporaryDirectories = [] + +afterEach(async () => { + await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true }))) +}) + +const sha256 = (character) => `sha256:${character.repeat(64)}` + +function file(path, role, character) { + return { path, type: 'file', role, size: 1, mode: 0o644, sha256: sha256(character) } +} + +function fixture() { + const tuple = 'linux-x64-glibc' + const identity = { + tupleId: tuple, + os: 'linux', + architecture: 'x64', + nodeVersion: '24.18.0', + contentId: sha256('f'), + fileCount: 5, + expandedSize: 5, + entries: [ + file('bin/node', 'node', '1'), + file('relay.js', 'relay', '2'), + file('node_modules/node-pty/build/Release/pty.node', 'node-pty-native', '3'), + file( + 'node_modules/@parcel/watcher-linux-x64-glibc/watcher.node', + 'parcel-watcher-native', + '4' + ), + file('THIRD_PARTY_LICENSES.txt', 'license', '5') + ] + } + return { + tuple, + identity, + archive: { + name: `orca-ssh-relay-runtime-v1-${tuple}-${'f'.repeat(64)}.tar.xz`, + size: 123, + sha256: sha256('a') + }, + nodeRelease: { + baseUrl: 'https://nodejs.org/dist/v24.18.0', + archives: { + [tuple]: { name: 'node-v24.18.0-linux-x64.tar.xz', sha256: 'b'.repeat(64) } + }, + signature: { + key: { + sourceUrl: 'https://github.com/nodejs/release-keys/raw/commit/gpg/key.asc', + sha256: 'c'.repeat(64) + } + } + } + } +} + +describe('SSH relay runtime published metadata', () => { + it('binds SPDX, SLSA provenance, and identity to the exact archive and runner', async () => { + const outputDirectory = await mkdtemp(join(tmpdir(), 'orca-runtime-metadata-')) + temporaryDirectories.push(outputDirectory) + const input = fixture() + const runner = { + os: 'Linux', + architecture: 'X64', + environment: 'github-hosted', + requestedLabel: 'ubuntu-24.04', + image: { os: 'ubuntu24', version: '20260705.232.1' } + } + const toolchain = Object.fromEntries( + [ + 'buildNode', + 'bundledNode', + 'compiler', + 'buildSystem', + 'python', + 'archive', + 'nodeAddonApi', + 'nodeGyp', + 'strip' + ].map((name, index) => [ + name, + { version: `${name} version`, sha256: sha256((index + 6).toString(16)) } + ]) + ) + const builder = `https://github.com/stablyai/orca/blob/${'1'.repeat(40)}/.github/workflows/ssh-relay-runtime-artifacts.yml` + + const assets = await writeSshRelayRuntimeMetadata({ + outputDirectory, + identity: input.identity, + archive: input.archive, + nodeRelease: input.nodeRelease, + sourceDateEpoch: 1_752_710_400, + gitCommit: '1'.repeat(40), + builder, + runner, + toolchain + }) + const sbom = JSON.parse(await readFile(join(outputDirectory, assets.sbom.name), 'utf8')) + const provenance = JSON.parse( + await readFile(join(outputDirectory, assets.provenance.name), 'utf8') + ) + const identity = JSON.parse(await readFile(join(outputDirectory, assets.identity.name), 'utf8')) + + expect(sbom.files).toHaveLength(input.identity.fileCount) + expect( + sbom.relationships.filter((entry) => entry.relationshipType === 'CONTAINS') + ).toHaveLength(input.identity.fileCount) + expect(provenance.subject).toEqual([ + { name: input.archive.name, digest: { sha256: 'a'.repeat(64) } } + ]) + expect(provenance.predicate.runDetails).toMatchObject({ + builder: { id: builder }, + metadata: { runner } + }) + expect(provenance.predicate.buildDefinition).toMatchObject({ + internalParameters: { toolchain }, + resolvedDependencies: expect.arrayContaining([ + { + uri: 'https://nodejs.org/dist/v24.18.0/node-v24.18.0-linux-x64.tar.xz', + digest: { sha256: 'b'.repeat(64) } + }, + { + uri: `git+https://github.com/stablyai/orca@${'1'.repeat(40)}`, + digest: { gitCommit: '1'.repeat(40) } + } + ]) + }) + expect(identity.archive).toEqual({ + name: input.archive.name, + size: 123, + expandedSize: 5, + fileCount: 5, + sha256: sha256('a') + }) + await expect( + writeSshRelayRuntimeMetadata({ + outputDirectory, + identity: input.identity, + archive: input.archive, + nodeRelease: input.nodeRelease, + sourceDateEpoch: 1_752_710_400, + gitCommit: '1'.repeat(40), + builder, + runner, + toolchain + }) + ).rejects.toThrow() + }) +}) diff --git a/config/scripts/ssh-relay-runtime-sbom.mjs b/config/scripts/ssh-relay-runtime-sbom.mjs new file mode 100644 index 00000000000..a3a29cad6e3 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-sbom.mjs @@ -0,0 +1,125 @@ +import { createRequire } from 'node:module' + +import { sshRelayRuntimeWatcherPackage } from './ssh-relay-runtime-closure.mjs' + +const require = createRequire(import.meta.url) + +function spdxId(value) { + return `SPDXRef-${value.replace(/[^A-Za-z0-9.-]/g, '-')}` +} + +function packageRecord(name, version, license) { + return { + name, + SPDXID: spdxId(`Package-${name}`), + versionInfo: version, + downloadLocation: 'NOASSERTION', + filesAnalyzed: false, + licenseConcluded: license, + licenseDeclared: license, + copyrightText: 'NOASSERTION' + } +} + +function installedPackage(name) { + const parsed = require(`${name}/package.json`) + return packageRecord(parsed.name, parsed.version, parsed.license ?? 'NOASSERTION') +} + +function fileType(role) { + return ['node', 'node-pty-native', 'parcel-watcher-native', 'native-runtime'].includes(role) + ? 'BINARY' + : role === 'license' + ? 'TEXT' + : 'SOURCE' +} + +function ownerPackage(path, packages) { + const packagePath = packages.find((entry) => path.startsWith(`node_modules/${entry.name}/`)) + if (packagePath) { + return packagePath + } + if (path.startsWith('node_modules/')) { + return undefined + } + if (path === 'bin/node' || path === 'bin/node.exe') { + return packages.find((entry) => entry.name === 'node') + } + return packages.find((entry) => entry.name === 'orca-ssh-relay') +} + +export function createSshRelayRuntimeSbom({ identity, archive, sourceDateEpoch }) { + if (!/^sha256:[0-9a-f]{64}$/.test(archive?.sha256 ?? '')) { + throw new Error('Runtime SBOM requires the exact archive SHA-256') + } + const watcherPackage = sshRelayRuntimeWatcherPackage(identity.tupleId) + const packages = [ + packageRecord('node', identity.nodeVersion, 'MIT'), + packageRecord('orca-ssh-relay', identity.contentId, 'NOASSERTION'), + installedPackage('node-pty'), + installedPackage('@parcel/watcher'), + installedPackage(watcherPackage), + installedPackage('detect-libc'), + installedPackage('is-glob'), + installedPackage('is-extglob'), + installedPackage('picomatch') + ] + const files = identity.entries + .filter((entry) => entry.type === 'file') + .map((entry) => ({ + fileName: entry.path, + SPDXID: spdxId(`File-${entry.path}`), + fileTypes: [fileType(entry.role)], + checksums: [{ algorithm: 'SHA256', checksumValue: entry.sha256.slice('sha256:'.length) }], + licenseConcluded: 'NOASSERTION', + copyrightText: 'NOASSERTION' + })) + const identifiers = new Set() + for (const element of [...packages, ...files]) { + if (identifiers.has(element.SPDXID)) { + throw new Error(`Runtime SBOM contains a duplicate SPDX identifier: ${element.SPDXID}`) + } + identifiers.add(element.SPDXID) + } + const contains = files.map((file) => { + const owner = ownerPackage(file.fileName, packages) + if (!owner) { + throw new Error(`Runtime SBOM cannot assign a package owner: ${file.fileName}`) + } + return { + spdxElementId: owner.SPDXID, + relationshipType: 'CONTAINS', + relatedSpdxElement: file.SPDXID + } + }) + const relay = packages.find((entry) => entry.name === 'orca-ssh-relay') + const relationships = [ + ...packages.map((component) => ({ + spdxElementId: 'SPDXRef-DOCUMENT', + relationshipType: 'DESCRIBES', + relatedSpdxElement: component.SPDXID + })), + ...packages + .filter((component) => component !== relay) + .map((component) => ({ + spdxElementId: relay.SPDXID, + relationshipType: 'DEPENDS_ON', + relatedSpdxElement: component.SPDXID + })), + ...contains + ] + return { + spdxVersion: 'SPDX-2.3', + dataLicense: 'CC0-1.0', + SPDXID: 'SPDXRef-DOCUMENT', + name: `Orca SSH relay runtime ${identity.tupleId}`, + documentNamespace: `https://github.com/stablyai/orca/ssh-relay-runtime/spdx/${archive.sha256.slice('sha256:'.length)}`, + creationInfo: { + created: new Date(sourceDateEpoch * 1000).toISOString(), + creators: ['Organization: Stably AI'] + }, + packages, + files, + relationships + } +} diff --git a/config/scripts/ssh-relay-runtime-sbom.test.mjs b/config/scripts/ssh-relay-runtime-sbom.test.mjs new file mode 100644 index 00000000000..c03be12ecc9 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-sbom.test.mjs @@ -0,0 +1,122 @@ +import { describe, expect, it } from 'vitest' + +import { createSshRelayRuntimeSbom } from './ssh-relay-runtime-sbom.mjs' + +const sha256 = (character) => `sha256:${character.repeat(64)}` + +function file(path, role, character) { + return { path, type: 'file', role, size: 1, mode: 0o644, sha256: sha256(character) } +} + +function identity(tuple = 'linux-arm64-glibc') { + return { + tupleId: tuple, + nodeVersion: '24.18.0', + contentId: sha256('f'), + entries: [ + file('bin/node', 'node', '1'), + file('relay.js', 'relay', '2'), + file('THIRD_PARTY_LICENSES.txt', 'license', '3'), + file('node_modules/node-pty/lib/index.js', 'runtime-javascript', '4'), + file('node_modules/@parcel/watcher/index.js', 'runtime-javascript', '5'), + file(`node_modules/@parcel/watcher-${tuple}/watcher.node`, 'parcel-watcher-native', '6'), + file('node_modules/detect-libc/lib/detect-libc.js', 'runtime-javascript', '7'), + file('node_modules/is-glob/index.js', 'runtime-javascript', '8'), + file('node_modules/is-extglob/index.js', 'runtime-javascript', '9'), + file('node_modules/picomatch/index.js', 'runtime-javascript', 'a') + ] + } +} + +const archive = { sha256: sha256('e') } + +function ownerName(sbom, fileName) { + const fileId = sbom.files.find((entry) => entry.fileName === fileName).SPDXID + const owners = sbom.relationships.filter( + (entry) => entry.relationshipType === 'CONTAINS' && entry.relatedSpdxElement === fileId + ) + expect(owners).toHaveLength(1) + return sbom.packages.find((entry) => entry.SPDXID === owners[0].spdxElementId).name +} + +describe('SSH relay runtime SPDX SBOM', () => { + it('inventories every runtime file under exactly one package', () => { + const fixture = identity() + const sbom = createSshRelayRuntimeSbom({ + identity: fixture, + archive, + sourceDateEpoch: 1_752_710_400 + }) + + expect(sbom.spdxVersion).toBe('SPDX-2.3') + expect(sbom.documentNamespace).toBe( + `https://github.com/stablyai/orca/ssh-relay-runtime/spdx/${'e'.repeat(64)}` + ) + expect(sbom.creationInfo.created).toBe('2025-07-17T00:00:00.000Z') + expect(sbom.packages.map((entry) => entry.name)).toEqual([ + 'node', + 'orca-ssh-relay', + 'node-pty', + '@parcel/watcher', + '@parcel/watcher-linux-arm64-glibc', + 'detect-libc', + 'is-glob', + 'is-extglob', + 'picomatch' + ]) + expect(sbom.packages.find((entry) => entry.name === 'node')).toMatchObject({ + versionInfo: '24.18.0', + licenseDeclared: 'MIT' + }) + expect(sbom.packages.find((entry) => entry.name === 'orca-ssh-relay')).toMatchObject({ + versionInfo: fixture.contentId, + licenseDeclared: 'NOASSERTION' + }) + expect(sbom.files).toHaveLength(fixture.entries.length) + expect(new Set([...sbom.packages, ...sbom.files].map((entry) => entry.SPDXID)).size).toBe( + sbom.packages.length + sbom.files.length + ) + expect(ownerName(sbom, 'bin/node')).toBe('node') + expect(ownerName(sbom, 'relay.js')).toBe('orca-ssh-relay') + expect(ownerName(sbom, 'THIRD_PARTY_LICENSES.txt')).toBe('orca-ssh-relay') + expect(ownerName(sbom, 'node_modules/node-pty/lib/index.js')).toBe('node-pty') + expect(ownerName(sbom, 'node_modules/@parcel/watcher/index.js')).toBe('@parcel/watcher') + expect(ownerName(sbom, 'node_modules/@parcel/watcher-linux-arm64-glibc/watcher.node')).toBe( + '@parcel/watcher-linux-arm64-glibc' + ) + expect(ownerName(sbom, 'node_modules/detect-libc/lib/detect-libc.js')).toBe('detect-libc') + expect(ownerName(sbom, 'node_modules/is-glob/index.js')).toBe('is-glob') + expect(ownerName(sbom, 'node_modules/is-extglob/index.js')).toBe('is-extglob') + expect(ownerName(sbom, 'node_modules/picomatch/index.js')).toBe('picomatch') + expect( + sbom.relationships.filter((entry) => entry.relationshipType === 'DEPENDS_ON') + ).toHaveLength(8) + }) + + it('rejects an unowned node_modules path and colliding SPDX identifiers', () => { + const unowned = identity() + unowned.entries.push(file('node_modules/unreviewed/index.js', 'runtime-javascript', 'b')) + expect(() => + createSshRelayRuntimeSbom({ identity: unowned, archive, sourceDateEpoch: 1_752_710_400 }) + ).toThrow(/cannot assign a package owner/i) + + const collision = identity() + collision.entries.push( + file('relay/a+b.js', 'runtime-javascript', 'b'), + file('relay/a@b.js', 'runtime-javascript', 'c') + ) + expect(() => + createSshRelayRuntimeSbom({ identity: collision, archive, sourceDateEpoch: 1_752_710_400 }) + ).toThrow(/duplicate SPDX identifier/i) + }) + + it('requires the exact archive digest to make the document namespace immutable', () => { + expect(() => + createSshRelayRuntimeSbom({ + identity: identity(), + archive: { sha256: 'missing-prefix' }, + sourceDateEpoch: 1_752_710_400 + }) + ).toThrow(/archive SHA-256/i) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-toolchain.mjs b/config/scripts/ssh-relay-runtime-toolchain.mjs new file mode 100644 index 00000000000..1583b86c011 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-toolchain.mjs @@ -0,0 +1,252 @@ +import { execFile } from 'node:child_process' +import { createHash } from 'node:crypto' +import { createReadStream } from 'node:fs' +import { readdir, realpath, stat } from 'node:fs/promises' +import { createRequire } from 'node:module' +import { dirname, isAbsolute, join, relative } from 'node:path' +import { isDeepStrictEqual, promisify } from 'node:util' + +const require = createRequire(import.meta.url) +const execFileAsync = promisify(execFile) +const MAX_TOOL_BYTES = 300 * 1024 * 1024 +const TOOL_TIMEOUT_MS = 60_000 +const TOOL_OUTPUT_BYTES = 1024 * 1024 +const MAX_PACKAGE_FILES = 500 +const MAX_PACKAGE_BYTES = 32 * 1024 * 1024 + +async function sha256File(path) { + const metadata = await stat(path) + if (!metadata.isFile() || metadata.size > MAX_TOOL_BYTES) { + throw new Error(`Runtime build tool is not a bounded regular file: ${path}`) + } + const hash = createHash('sha256') + for await (const chunk of createReadStream(path)) { + hash.update(chunk) + } + return `sha256:${hash.digest('hex')}` +} + +async function capture(command, args) { + try { + return await execFileAsync(command, args, { + encoding: 'utf8', + maxBuffer: TOOL_OUTPUT_BYTES, + timeout: TOOL_TIMEOUT_MS, + windowsHide: true + }) + } catch (error) { + if (typeof error?.stdout === 'string' || typeof error?.stderr === 'string') { + return { stdout: error.stdout ?? '', stderr: error.stderr ?? '' } + } + throw error + } +} + +export function selectSshRelayRuntimeToolVersion({ stdout = '', stderr = '' }, pattern) { + const lines = `${stderr}\n${stdout}` + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + const version = pattern ? lines.find((line) => pattern.test(line)) : lines[0] + if (!version || Buffer.byteLength(version) > 512) { + throw new Error('Runtime build tool did not report a bounded version line') + } + return version +} + +async function locateExecutable(command, { xcrun = false } = {}) { + if (isAbsolute(command)) { + return realpath(command) + } + const locator = xcrun + ? ['xcrun', ['--find', command]] + : process.platform === 'win32' + ? ['where.exe', [command]] + : ['which', [command]] + const result = await capture(...locator) + const path = result.stdout + .split(/\r?\n/) + .map((line) => line.trim()) + .find(Boolean) + if (!path || !isAbsolute(path)) { + throw new Error(`Runtime build tool executable could not be resolved: ${command}`) + } + return realpath(path) +} + +async function executableRecord({ + command, + args, + versionCommand, + versionArgs, + versionPattern, + xcrun = false +}) { + const path = await locateExecutable(command, { xcrun }) + const result = await capture(versionCommand ?? path, versionArgs ?? args) + return { + version: selectSshRelayRuntimeToolVersion(result, versionPattern), + sha256: await sha256File(path) + } +} + +async function packageTreeRecord(name) { + const parsed = require(`${name}/package.json`) + const root = dirname(require.resolve(`${name}/package.json`)) + const files = [] + let totalBytes = 0 + async function visit(directory) { + const entries = await readdir(directory, { withFileTypes: true }) + entries.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) + for (const entry of entries) { + const path = join(directory, entry.name) + if (entry.isDirectory()) { + await visit(path) + } else if (entry.isFile()) { + files.push(path) + totalBytes += (await stat(path)).size + } else { + throw new Error(`Runtime build package contains a special entry: ${name}`) + } + if (files.length > MAX_PACKAGE_FILES) { + throw new Error(`Runtime build package exceeds the file-count bound: ${name}`) + } + if (totalBytes > MAX_PACKAGE_BYTES) { + throw new Error(`Runtime build package exceeds the byte bound: ${name}`) + } + } + } + await visit(root) + const hash = createHash('sha256') + for (const path of files) { + hash.update(relative(root, path).replaceAll('\\', '/')) + hash.update('\0') + hash.update(await sha256File(path)) + hash.update('\n') + } + return { version: parsed.version, sha256: `sha256:${hash.digest('hex')}`, files: files.length } +} + +export function sshRelayRuntimeBuilderIdentity({ gitCommit, env = process.env }) { + if (env.GITHUB_ACTIONS !== 'true') { + return `local://${process.platform}/${process.arch}` + } + if ( + !/^[0-9a-f]{40}$/.test(gitCommit) || + !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(env.GITHUB_REPOSITORY ?? '') + ) { + throw new Error('GitHub runtime builder identity is incomplete') + } + const workflow = (env.GITHUB_WORKFLOW_REF ?? '').split('@')[0] + const prefix = `${env.GITHUB_REPOSITORY}/` + if ( + !workflow.startsWith(prefix) || + workflow.slice(prefix.length) !== '.github/workflows/ssh-relay-runtime-artifacts.yml' + ) { + throw new Error('GitHub runtime builder workflow identity is malformed') + } + return `https://github.com/${env.GITHUB_REPOSITORY}/blob/${gitCommit}/${workflow.slice(prefix.length)}` +} + +export function sshRelayRuntimeRunnerIdentity({ env = process.env } = {}) { + const github = env.GITHUB_ACTIONS === 'true' + const identity = { + os: env.RUNNER_OS ?? process.platform, + architecture: env.RUNNER_ARCH ?? process.arch, + environment: env.RUNNER_ENVIRONMENT ?? (github ? undefined : 'local'), + requestedLabel: env.ORCA_RUNTIME_REQUESTED_RUNNER ?? (github ? undefined : 'local'), + image: { + os: env.ImageOS ?? (github ? undefined : process.platform), + version: env.ImageVersion ?? (github ? undefined : 'local') + } + } + if ( + !identity.os || + !identity.architecture || + !identity.environment || + !identity.requestedLabel || + !identity.image.os || + !identity.image.version + ) { + throw new Error('Runtime runner identity is incomplete') + } + return identity +} + +export function assertSshRelayRuntimeToolchain(toolchain, tuple) { + const common = ['buildNode', 'bundledNode', 'compiler', 'buildSystem', 'python', 'archive'] + const expected = tuple.startsWith('win32-') + ? [...common, 'linker', 'nodeAddonApi', 'nodeGyp'] + : [...common, 'nodeAddonApi', 'nodeGyp', 'strip'] + if (!isDeepStrictEqual(Object.keys(toolchain).sort(), expected.sort())) { + throw new Error(`Runtime toolchain record is incomplete for ${tuple}`) + } + for (const [name, record] of Object.entries(toolchain)) { + const keys = Object.keys(record).sort() + const expectedKeys = + record.files === undefined ? ['sha256', 'version'] : ['files', 'sha256', 'version'] + if ( + !isDeepStrictEqual(keys, expectedKeys) || + typeof record.version !== 'string' || + record.version.length === 0 || + Buffer.byteLength(record.version) > 512 || + record.version.includes('\n') || + !/^sha256:[0-9a-f]{64}$/.test(record.sha256) || + (record.files !== undefined && (!Number.isSafeInteger(record.files) || record.files <= 0)) + ) { + throw new Error(`Runtime toolchain record is malformed: ${name}`) + } + } +} + +export async function collectSshRelayRuntimeToolchain(nodePath) { + const common = { + bundledNode: await executableRecord({ command: nodePath, args: ['--version'] }), + buildNode: await executableRecord({ command: process.execPath, args: ['--version'] }), + nodeGyp: await packageTreeRecord('node-gyp'), + nodeAddonApi: await packageTreeRecord('node-addon-api') + } + if (process.platform === 'win32') { + const toolchain = { + ...common, + compiler: await executableRecord({ + command: 'cl.exe', + args: [], + versionPattern: /Compiler Version/i + }), + linker: await executableRecord({ + command: 'link.exe', + args: [], + versionPattern: /Linker Version/i + }), + buildSystem: await executableRecord({ + command: 'msbuild.exe', + args: ['-version', '-nologo'] + }), + python: await executableRecord({ command: 'python.exe', args: ['--version'] }), + archive: await packageTreeRecord('yazl') + } + assertSshRelayRuntimeToolchain(toolchain, 'win32-x64') + return toolchain + } + const toolchain = { + ...common, + compiler: await executableRecord({ + command: process.platform === 'darwin' ? 'clang++' : 'c++', + args: ['--version'], + xcrun: process.platform === 'darwin' + }), + buildSystem: await executableRecord({ command: 'make', args: ['--version'] }), + python: await executableRecord({ command: 'python3', args: ['--version'] }), + archive: await executableRecord({ command: 'xz', args: ['--version'] }), + strip: await executableRecord({ + command: 'strip', + args: [], + versionCommand: process.platform === 'darwin' ? 'xcodebuild' : undefined, + versionArgs: process.platform === 'darwin' ? ['-version'] : undefined, + xcrun: process.platform === 'darwin' + }) + } + assertSshRelayRuntimeToolchain(toolchain, `${process.platform}-x64`) + return toolchain +} diff --git a/config/scripts/ssh-relay-runtime-toolchain.test.mjs b/config/scripts/ssh-relay-runtime-toolchain.test.mjs new file mode 100644 index 00000000000..eb6795eea09 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-toolchain.test.mjs @@ -0,0 +1,112 @@ +import { describe, expect, it } from 'vitest' + +import { + assertSshRelayRuntimeToolchain, + collectSshRelayRuntimeToolchain, + selectSshRelayRuntimeToolVersion, + sshRelayRuntimeBuilderIdentity, + sshRelayRuntimeRunnerIdentity +} from './ssh-relay-runtime-toolchain.mjs' + +const commit = 'a'.repeat(40) + +describe('SSH relay runtime build provenance', () => { + it('selects the real Windows compiler version instead of its stdout usage line', () => { + expect( + selectSshRelayRuntimeToolVersion( + { + stdout: 'usage: cl [ option... ] filename... [ /link linkoption... ]', + stderr: [ + 'Microsoft (R) C/C++ Optimizing Compiler Version 19.44.35228 for ARM64', + 'Copyright (C) Microsoft Corporation.' + ].join('\r\n') + }, + /Compiler Version/i + ) + ).toBe('Microsoft (R) C/C++ Optimizing Compiler Version 19.44.35228 for ARM64') + }) + + it('pins GitHub builder identity to the exact source commit', () => { + expect( + sshRelayRuntimeBuilderIdentity({ + gitCommit: commit, + env: { + GITHUB_ACTIONS: 'true', + GITHUB_REPOSITORY: 'stablyai/orca', + GITHUB_WORKFLOW_REF: + 'stablyai/orca/.github/workflows/ssh-relay-runtime-artifacts.yml@refs/pull/8741/merge' + } + }) + ).toBe( + `https://github.com/stablyai/orca/blob/${commit}/.github/workflows/ssh-relay-runtime-artifacts.yml` + ) + expect(() => + sshRelayRuntimeBuilderIdentity({ + gitCommit: commit, + env: { + GITHUB_ACTIONS: 'true', + GITHUB_REPOSITORY: 'stablyai/orca', + GITHUB_WORKFLOW_REF: 'other/repo/.github/workflows/untrusted.yml@refs/heads/main' + } + }) + ).toThrow(/workflow identity/i) + }) + + it('requires the resolved runner label, architecture, environment, and image identity', () => { + expect( + sshRelayRuntimeRunnerIdentity({ + env: { + GITHUB_ACTIONS: 'true', + RUNNER_OS: 'Windows', + RUNNER_ARCH: 'ARM64', + RUNNER_ENVIRONMENT: 'github-hosted', + ORCA_RUNTIME_REQUESTED_RUNNER: 'windows-11-arm', + ImageOS: 'win11-arm64', + ImageVersion: '20260706.102.1' + } + }) + ).toEqual({ + os: 'Windows', + architecture: 'ARM64', + environment: 'github-hosted', + requestedLabel: 'windows-11-arm', + image: { os: 'win11-arm64', version: '20260706.102.1' } + }) + expect(() => + sshRelayRuntimeRunnerIdentity({ + env: { + GITHUB_ACTIONS: 'true', + RUNNER_OS: 'Windows', + RUNNER_ARCH: 'ARM64' + } + }) + ).toThrow(/runner identity/i) + }) + + it('records bounded native tool versions and SHA-256 executable or code digests', async () => { + const toolchain = await collectSshRelayRuntimeToolchain(process.execPath) + const tuple = process.platform === 'win32' ? 'win32-x64' : 'linux-x64-glibc' + expect(() => assertSshRelayRuntimeToolchain(toolchain, tuple)).not.toThrow() + expect(toolchain).toMatchObject({ + bundledNode: { version: process.version }, + buildNode: { version: process.version }, + nodeGyp: { version: expect.any(String) }, + nodeAddonApi: { version: expect.any(String) }, + compiler: { version: expect.any(String) }, + buildSystem: { version: expect.any(String) }, + python: { version: expect.any(String) }, + archive: { version: expect.any(String) } + }) + for (const record of Object.values(toolchain)) { + expect(record.sha256).toMatch(/^sha256:[0-9a-f]{64}$/) + expect(Buffer.byteLength(record.version)).toBeGreaterThan(0) + expect(Buffer.byteLength(record.version)).toBeLessThanOrEqual(512) + } + expect(() => + assertSshRelayRuntimeToolchain( + { ...toolchain, compiler: { version: toolchain.compiler.version } }, + tuple + ) + ).toThrow(/compiler/i) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 1815d229a8e..91c351c51fb 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -28,6 +28,7 @@ describe('SSH relay runtime artifact workflow', () => { expect(posixJob['timeout-minutes']).toBe(20) expect(windowsJob['timeout-minutes']).toBe(30) for (const job of [posixJob, windowsJob]) { + expect(job.env.ORCA_RUNTIME_REQUESTED_RUNNER).toBe('${{ matrix.runner }}') expect(job.steps[0].with.ref).toBe('${{ github.event.pull_request.head.sha || github.sha }}') for (const step of job.steps.filter((candidate) => candidate.uses)) { expect(step.uses).toMatch(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+@[0-9a-f]{40}$/) @@ -62,6 +63,9 @@ describe('SSH relay runtime artifact workflow', () => { expect(source).toContain('ssh-relay-runtime-reproducibility.mjs') expect(source).toContain('ssh-relay-runtime-reproducibility.test.mjs') expect(source.match(/ssh-relay-runtime-closure\.test\.mjs/g)).toHaveLength(4) + expect(source.match(/ssh-relay-runtime-sbom\.test\.mjs/g)).toHaveLength(4) + expect(source.match(/ssh-relay-runtime-provenance\.test\.mjs/g)).toHaveLength(4) + expect(source.match(/ssh-relay-runtime-toolchain\.test\.mjs/g)).toHaveLength(4) expect( source.match(/node --check config\/scripts\/ssh-relay-runtime-closure\.mjs/g) ).toHaveLength(2) From 27e932c8cbc44f09d9b410d3aca519e7d1b8e9fe Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 17:09:18 -0400 Subject: [PATCH 058/267] docs(ssh): record runtime closure and metadata gates Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 15 +- ...github-release-implementation-checklist.md | 193 +++++++++++++++--- 2 files changed, 169 insertions(+), 39 deletions(-) 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 6f64636b468..4f53e5377c4 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md @@ -15,12 +15,12 @@ Approved design decisions are listed separately and are not presented as impleme - The bundled runtime path is not connected to production and no tuple is enabled. - The future bundled path is a per-SSH-target Beta option, off by default. - No runtime artifact has been published to a GitHub Release. -- Latest native proof: [GitHub Actions run 29365815434](https://github.com/stablyai/orca/actions/runs/29365815434) +- Latest native proof: [GitHub Actions run 29367559831](https://github.com/stablyai/orca/actions/runs/29367559831) passed clean-build equality and runtime smoke on Linux x64/arm64, macOS x64/arm64, and Windows x64/arm64. -- Active work package: exact runtime closure enforcement is locally green and awaiting all-six - native proof; SBOM/provenance completeness, archive safety, oldest-baseline execution, and native - trust remain open. +- Active work package: exact runtime closure is green on all six native runner families; the SPDX, + builder/runner identity, and hashed-toolchain correction is locally green and awaiting the same + native proof. Archive safety, oldest-baseline execution, and native trust remain open. ## Locked rollout decisions @@ -74,8 +74,11 @@ Approved design decisions are listed separately and are not presented as impleme - [x] Prove bundled Node, real PTY input/resize/exit, watcher events, and Windows resource settlement. - [x] Prove two clean builds compare exactly on all six native GitHub runner families. - [x] Keep `/guard:cf`; fix Windows arm64 drift with copied-artifact-only `/INCREMENTAL:NO`. -- [x] Define and locally test an exact 34/35/42-file per-tuple closure that rejects undeclared - package managers, sources, maps, build outputs, package drift, and missing licenses. +- [x] Enforce and prove an exact 34/35/42-file per-tuple closure that rejects undeclared package + managers, sources, maps, build outputs, package drift, and missing licenses on all six native + runner families. +- [x] Define and locally test file-to-package SPDX ownership, immutable archive-scoped SBOM identity, + exact-commit builder identity, runner identity, and bounded SHA-256 toolchain records. - [ ] Complete the runtime closure, license, SBOM, provenance, and prohibited-content allowlist audit. - [ ] Prove each candidate on its oldest supported OS/libc/kernel baseline. - [ ] Sign macOS and Windows native bytes and verify target-native trust policy. 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 a5d9228a9db..3f621096a53 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 runtime closure — exact implementation commit `ec5461aff` moves the exact per-tuple closure/prohibited-content/package/license gate into the artifact builder and is locally green under E-M3-RUNTIME-CLOSURE-LOCAL-001; all six prior candidate archives from run [29365815434](https://github.com/stablyai/orca/actions/runs/29365815434) satisfy the new 34/35/42-file closure, but the new builder gate has not yet run target-natively and no Milestone 3 completion box is closed by local/static reuse alone; the next exact-head six-cell run must retain the proven content IDs, enforce the closure before archiving, complete smoke and clean-build equality, and upload before this slice is accepted; SBOM/provenance completeness, compiler/toolchain records, oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 SBOM/provenance closure — exact-head run [29367559831](https://github.com/stablyai/orca/actions/runs/29367559831) at ledger head `ace3d4f41` passes the builder-enforced 34/35/42-file closure, unchanged runtime content IDs, complete smoke, clean-build equality, and upload on all six native runner families under E-M3-RUNTIME-CLOSURE-CI-001; exact metadata implementation commit `26bbd7b67` adds file-to-package SPDX ownership/dependency relationships, archive-scoped document identity, exact-commit builder identity, resolved runner image/architecture, and bounded SHA-256 records for native executables and complete build-package trees, and is locally green under E-M3-METADATA-LOCAL-001; the next exact-head six-cell run must prove those records and metadata equality target-natively before the remaining SBOM/provenance/toolchain boxes are checked; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      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) @@ -175,11 +175,12 @@ 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: push exact implementation commit `ec5461aff` plus its evidence-ledger head - and rerun all six target-native cells. Require the builder-enforced closure, unchanged runtime - content IDs, complete smoke, clean-build equality, and upload before checking the related - Milestone 3 claims. Then fix the separately identified SBOM/provenance/toolchain gaps. Preserve the - artifact-only boundary, legacy/default path, and every other release gate. +- Next required action: push exact metadata implementation commit `26bbd7b67` plus its evidence- + ledger head and rerun all six target-native cells. Require exact SPDX ownership, archive-scoped + document identity, commit-pinned builder identity, resolved runner identity, complete bounded tool + records with SHA-256, clean-build metadata equality, smoke, and upload before checking the remaining + Milestone 3 metadata/toolchain claims. Preserve the artifact-only boundary, legacy/default path, + and every other release gate. ## Non-Negotiable Invariants @@ -773,8 +774,8 @@ related implementation claims are checked. Each runtime must contain only the executable closure required by the relay. -- [ ] Replace or extend `config/scripts/build-relay.mjs` without weakening its existing relay and - watcher content-hash guarantees. +- [x] Replace or extend `config/scripts/build-relay.mjs` without weakening its existing relay and + watcher content-hash guarantees. (E-M3-RUNTIME-CLOSURE-CI-001) - [x] Add a clearly named runtime assembly script, for example `config/scripts/build-ssh-relay-runtime.mjs`. (E-M3-RUNTIME-LOCAL-001) @@ -784,23 +785,27 @@ Each runtime must contain only the executable closure required by the relay. headers archive and tuple-specific `node.lib`; verify/copy them into an exclusive local build root and configure `node-gyp` to fail rather than fetch an unstaged input. (E-M3-WINDOWS-INPUT-001; successful target-native offline build remains a per-tuple gate) -- [ ] Build Orca’s patched `node-pty@1.1.0` against the exact bundled Node runtime. -- [ ] Assert Orca-required patched exports/diagnostics exist; do not silently use an upstream - prebuild that omits the patch. +- [x] Build Orca’s patched `node-pty@1.1.0` against the exact bundled Node runtime. + (E-M3-RUNTIME-CLOSURE-CI-001) +- [x] Assert Orca-required patched exports/diagnostics exist; do not silently use an upstream + prebuild that omits the patch. (E-M3-RUNTIME-CLOSURE-CI-001) - [x] For Windows, copy the tuple-architecture `conpty.dll` and `OpenConsole.exe` from the pinned node-pty source into `build/Release/conpty`, hash them into the runtime identity as native runtime files, and prove PTY spawn/resize/exit with `useConptyDll: true`. Do not substitute a system-ConPTY smoke for the production path. (E-M3-WINDOWS-CONPTY-GAP-001, E-M3-WINDOWS-CI-001) -- [ ] Include exactly one compatible `@parcel/watcher@2.5.6` native optional package. -- [ ] Include relay JavaScript, watcher child, required runtime JavaScript closure, licenses, SBOM, - provenance, and runtime metadata. -- [ ] Exclude package managers, development dependencies, compilers, sources, caches, build +- [x] Include exactly one compatible `@parcel/watcher@2.5.6` native optional package. + (E-M3-RUNTIME-CLOSURE-CI-001) +- [x] Include relay JavaScript, watcher child, required runtime JavaScript closure, licenses, SBOM, + provenance, and runtime metadata. (E-M3-RUNTIME-CLOSURE-CI-001; metadata completeness remains + a separate open gate) +- [x] Exclude package managers, development dependencies, compilers, sources, caches, build directories, and Orca-built debug symbols unless an approved diagnostics requirement needs them. Preserve verified official Node executable bytes without stripping or rewriting them, - even when the upstream binary contains debug metadata. -- [x] Make POSIX `tar.xz` output deterministic for the same tree, epoch, and pinned compression - toolchain; keep Windows zip determinism as an open per-tuple gate. (E-M3-RUNTIME-LOCAL-001) + even when the upstream binary contains debug metadata. (E-M3-RUNTIME-CLOSURE-CI-001) +- [x] Make POSIX `tar.xz` and Windows ZIP output deterministic for the same tree, epoch, and pinned + compression toolchain. (E-M3-RUNTIME-LOCAL-001, E-M3-REPRODUCIBILITY-CI-001, + E-M3-RUNTIME-CLOSURE-CI-001) - [x] Verify required executable modes before archiving. (E-M3-RUNTIME-LOCAL-001) - [ ] Sign native code according to the platform decisions in Milestone 1. - [ ] Verify platform-native signatures/policy on target-native runners after signing and before @@ -1375,16 +1380,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; exact closure locally green; native closure/trust pending | `ec5461aff`; E-M3-RUNTIME-CLOSURE-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 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 | — | Every PR must document: @@ -1442,6 +1447,7 @@ focused commands as their scripts/tests are introduced. - [x] `node config/scripts/build-ssh-relay-runtime.mjs --tuple linux-arm64-glibc --inputs-directory --output-directory --work-directory --source-date-epoch --git-commit ` (E-M3-RUNTIME-LOCAL-001, E-M3-REPRODUCIBILITY-LINKER-LOCAL-001; local native Linux arm64 history plus current stable-work contract) - [x] `node config/scripts/verify-ssh-relay-runtime.mjs --runtime-directory --identity --archive ` (E-M3-RUNTIME-LOCAL-001; local native Linux arm64 only) - [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) ### Commands/scripts that must be added or formally identified @@ -6062,6 +6068,127 @@ the bounded depth` before opening or parsing a candidate. Starting at the comple require unchanged runtime content IDs, builder-enforced closure, complete smoke, clean-build equality, and upload. Then fix and prove SBOM/provenance completeness separately. +### E-M3-RUNTIME-CLOSURE-CI-001 — Exact closure passes all six target-native builds + +- Date: 2026-07-14 +- Commit SHA / PR: exact ledger head `ace3d4f416b1c710d601aa72c4b45824266b5cad`, containing exact + closure implementation commit `ec5461aff61d6868c18d4db1ce27f409a43ecf47`; stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Run and jobs: [run 29367559831](https://github.com/stablyai/orca/actions/runs/29367559831), + conclusion `success`; Windows arm64 `87202773590`, Windows x64 `87202773643`, macOS arm64 + `87202773645`, macOS x64 `87202773658`, Linux x64 `87202773669`, and Linux arm64 `87202773674` + all passed +- Runners: `windows-11-arm` / `win11-arm64` `20260706.102.1` native ARM64; + `windows-2022` / `win22` `20260706.237.1` native X64; `macos-15` / `macos15` + `20260706.0213.1` native ARM64; `macos-15-intel` / `macos15` `20260629.0276.1` native X64; + `ubuntu-24.04` / `ubuntu24` `20260705.232.1` native X64; and `ubuntu-24.04-arm` / + `ubuntu24-arm64` `20260706.52.2` native ARM64. Every job checked out the exact head above in a + GitHub-hosted environment. +- Remote and transport: none; target-native artifact build/inspection/smoke and unpublished + seven-day Actions artifacts only +- Exact evidence commands: + + ```sh + gh run view 29367559831 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh run view 29367559831 --repo stablyai/orca --log | \ + rg 'requested_runner=|resolved_image_os=|resolved_image_version=|runner_arch=|runner_environment=|source_commit=' + gh run view 29367559831 --repo stablyai/orca --log | \ + rg 'Test Files|"contentId"|windows_node_pty_msbuild_settings|clean_build_output=|runtime-evidence/' + gh api 'repos/stablyai/orca/actions/runs/29367559831/artifacts?per_page=100' + ``` + +- Result: PASS. All four POSIX jobs passed 15 artifact-contract files and both Windows jobs passed 16. Each clean build then executed the builder-enforced exact closure before archiving, archive + inspection, bundled Node/native PTY/watcher smoke, strict clean-output comparison, and upload. + Both Windows builds retained exactly one `/INCREMENTAL:NO`, `/guard:cf`, no target `.ilk`, and + identical native output. No tuple changed content identity: Linux x64 + `sha256:960546cd96c67fcf9bb0a61e96ecdbecbffd9104d3a495578f8bb19dd810649a`, Linux arm64 + `sha256:aa3aa8ae8b42334ba7b0dbe5c43fd1184e36b3f4f4a9bec0e990e9b78f090756`, macOS x64 + `sha256:585ea6034cdd07487d8667059f975a877c795a45dc0d6eeee1617f2e3749faa2`, macOS arm64 + `sha256:40ff5d2036784b794e7b09f78596409f63f3145280c530bece5280d40897f6cb`, Windows x64 + `sha256:6f7cbeb120e67766037649f6079099346220973e6158e1429b6ebf42729f1564`, and Windows arm64 + `sha256:741765a10ddc824cd305b9a50c8efd91477517c05d9cfe1ca46342c002652186`. +- Uploaded evidence: exactly six unpublished artifacts: Windows arm64 `8324892474`, 33,083,332 + bytes, `sha256:1b90d23c5573116cf855926db294dff906407a5703260099d0d0f58bb500eb6a`; + Windows x64 `8324831155`, 37,033,206 bytes, + `sha256:013fa42e0bf29a36f378dffe6d327090a042be9e5947e55e6e097d3382418c5d`; macOS arm64 + `8324786647`, 24,734,978 bytes, + `sha256:87e2632c542964df47ab4f981da0909532c51be6b2131692e539aaa1c77d409c`; macOS x64 + `8324870620`, 26,400,500 bytes, + `sha256:d9ddf9b00d77aa05785e0813e3a61fcb07cd8f5f06b270e9703d5f2e28c03370`; Linux arm64 + `8324781748`, 28,205,869 bytes, + `sha256:47d40a3eacf149270efabe7688f700dcfca7c1e155bbc2188298c7e78948469b`; and Linux x64 + `8324772777`, 29,281,599 bytes, + `sha256:5b1552f4e2d28db1f552068e3bad6eb174e83ad88800101120a92cf2f0e44121`. +- Duration and resource metrics: job wall times were 7m51s Windows arm64, 5m26s Windows x64, 3m38s + macOS arm64, 6m58s macOS x64, 3m06s Linux x64, and 3m23s Linux arm64. SSH channels/files, + cancellation, fallback delay, and connection latency remain outside this artifact-only run. +- Oracle proved: the exact closure is enforced inside the artifact builder on all six native runner + families; the reviewed Node, patched native PTY, one tuple-native watcher, relay/watcher JavaScript, + runtime metadata, and license bundle are present; package managers, development/build/source/map/ + PDB/ILK content and any unreviewed path are absent; complete smoke, reproducibility, and no-upload- + before-success ordering remain intact. +- Does not prove: the subsequent SPDX/toolchain correction at `26bbd7b67`, oldest-baseline execution, + native signing/trust, SSH transfer/install, publication, fallback, UI, performance, or any enabled + tuple. +- Checklist items satisfied: build the exact patched `node-pty`; assert patch markers; include one + compatible watcher and the complete declared runtime/license/metadata closure; exclude all + undeclared content. No tuple or production path is enabled. +- Follow-up: prove the separately committed metadata correction on all six native cells before + checking SBOM/toolchain completeness, then proceed to oldest-baseline and native-trust gates. + +### E-M3-METADATA-LOCAL-001 — SPDX ownership and bounded toolchain provenance + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `26bbd7b67a035cdf309ccdbbaab5985f4cc797fe`; stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: local macOS 26.2 build 25C56, Darwin 25.2.0 arm64 on Apple Silicon; Node v26.0.0 and + pnpm 10.24.0 for pure metadata/tool-discovery tests +- Remote and transport: none; artifact-only local contract tests +- Exact evidence commands: + + ```sh + 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 + pnpm exec oxfmt --check .github/workflows/ssh-relay-runtime-artifacts.yml \ + config/scripts/build-ssh-relay-runtime.mjs \ + config/scripts/ssh-relay-runtime-provenance.mjs \ + config/scripts/ssh-relay-runtime-provenance.test.mjs \ + config/scripts/ssh-relay-runtime-sbom.mjs \ + config/scripts/ssh-relay-runtime-sbom.test.mjs \ + config/scripts/ssh-relay-runtime-toolchain.mjs \ + config/scripts/ssh-relay-runtime-toolchain.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + git diff --check + ``` + +- Result: PASS. Nineteen artifact test files passed 76 tests; typecheck, full lint and its + reliability/localization/line-budget sub-gates, focused formatting, and diff checks passed. The + SPDX builder assigns every runtime file to exactly one package with `CONTAINS`, records relay + `DEPENDS_ON` relationships, uses the immutable content ID as relay package version, uses the exact + archive SHA-256 in the unique document namespace, and rejects unowned paths, identifier collisions, + or a missing archive digest. The provenance collector pins the builder URL to the exact Git commit, + requires the requested/resolved runner label, OS, architecture, hosted environment, and image + version, selects the real Windows compiler/linker version from stderr rather than the usage line, + and records bounded SHA-256 plus version for bundled/build Node, compiler, linker where applicable, + build system, Python, archive tool, strip tool where applicable, and complete bounded trees for + `node-gyp`, `node-addon-api`, and `yazl` where applicable. +- Oracle proved: the prior placeholder relay SBOM version, unowned file inventory, content-ID-only + document namespace, merge-ref builder identity, missing runner identity, un-hashed tool strings, + and Windows compiler usage-string record are corrected by one fail-closed metadata boundary; local + native discovery produces only bounded version/digest records without absolute host paths. +- Does not prove: Windows tool discovery/version parsing, all six runner-image records, clean-build + equality of the new SBOM/provenance bytes, uploaded metadata, an external SPDX consumer, native + signing/trust, oldest baselines, SSH, publication, or any enabled tuple. +- Checklist items satisfied: local SBOM/provenance/toolchain implementation and purpose tests only; + native metadata/toolchain boxes remain unchecked. +- Follow-up: push the exact implementation plus this ledger head, rerun all six native cells, inspect + each uploaded SPDX/provenance document, and require complete bounded records and clean-build + equality before closing the remaining Milestone 3 metadata/toolchain claims. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -6119,11 +6246,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push exact implementation commit `ec5461aff` plus its evidence-ledger head and rerun all six target- -native cells. Require the builder-enforced exact closure, unchanged runtime content IDs, complete -smoke, clean-build equality, and upload before checking the related Milestone 3 claims. Then correct -and prove SBOM/provenance/toolchain completeness as a separate artifact-only slice before proceeding -to oldest-baseline and native-trust proof. +Push exact metadata implementation commit `26bbd7b67` plus its evidence-ledger head and rerun all six +target-native cells. Require exact SPDX ownership, archive-scoped document identity, commit-pinned +builder identity, resolved runner identity, complete bounded tool records with SHA-256, clean-build +metadata equality, smoke, and upload before checking the remaining Milestone 3 metadata/toolchain +claims. Then proceed to oldest-baseline and native-trust proof. 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 From e3f76d3ba70c860242b5300aa765fbf75ef21317 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 17:21:26 -0400 Subject: [PATCH 059/267] build(ssh): correct native runtime metadata contracts Co-authored-by: Orca --- .../scripts/ssh-relay-runtime-toolchain.mjs | 15 ++++++-- .../ssh-relay-runtime-toolchain.test.mjs | 25 ++++++++++++- config/scripts/ssh-relay-runtime-tree.mjs | 4 +- .../ssh-relay-runtime-windows-tree.test.mjs | 9 ++++- config/scripts/ssh-relay-runtime-zip.test.mjs | 2 +- ...elease-implementation-checklist-summary.md | 7 ++-- ...github-release-implementation-checklist.md | 19 ++++++---- ...6-07-14-ssh-relay-github-release-plan.html | 6 ++- .../ssh/ssh-relay-artifact-selector.test.ts | 37 ++++++++++++++++--- 9 files changed, 96 insertions(+), 28 deletions(-) diff --git a/config/scripts/ssh-relay-runtime-toolchain.mjs b/config/scripts/ssh-relay-runtime-toolchain.mjs index 1583b86c011..72ee65e002e 100644 --- a/config/scripts/ssh-relay-runtime-toolchain.mjs +++ b/config/scripts/ssh-relay-runtime-toolchain.mjs @@ -173,6 +173,14 @@ export function sshRelayRuntimeRunnerIdentity({ env = process.env } = {}) { return identity } +export function sshRelayRuntimeStripVersionProbe(platform) { + // Why: GNU strip is silent/versionless without --version, while Apple strip delegates its + // version identity to the selected Xcode toolchain. + return platform === 'darwin' + ? { versionCommand: 'xcodebuild', versionArgs: ['-version'] } + : { args: ['--version'] } +} + export function assertSshRelayRuntimeToolchain(toolchain, tuple) { const common = ['buildNode', 'bundledNode', 'compiler', 'buildSystem', 'python', 'archive'] const expected = tuple.startsWith('win32-') @@ -216,7 +224,8 @@ export async function collectSshRelayRuntimeToolchain(nodePath) { }), linker: await executableRecord({ command: 'link.exe', - args: [], + // Why: direct no-argument invocation is silent on current hosted MSVC runners. + args: ['/?'], versionPattern: /Linker Version/i }), buildSystem: await executableRecord({ @@ -241,9 +250,7 @@ export async function collectSshRelayRuntimeToolchain(nodePath) { archive: await executableRecord({ command: 'xz', args: ['--version'] }), strip: await executableRecord({ command: 'strip', - args: [], - versionCommand: process.platform === 'darwin' ? 'xcodebuild' : undefined, - versionArgs: process.platform === 'darwin' ? ['-version'] : undefined, + ...sshRelayRuntimeStripVersionProbe(process.platform), xcrun: process.platform === 'darwin' }) } diff --git a/config/scripts/ssh-relay-runtime-toolchain.test.mjs b/config/scripts/ssh-relay-runtime-toolchain.test.mjs index eb6795eea09..6fd6d4c3881 100644 --- a/config/scripts/ssh-relay-runtime-toolchain.test.mjs +++ b/config/scripts/ssh-relay-runtime-toolchain.test.mjs @@ -5,7 +5,8 @@ import { collectSshRelayRuntimeToolchain, selectSshRelayRuntimeToolVersion, sshRelayRuntimeBuilderIdentity, - sshRelayRuntimeRunnerIdentity + sshRelayRuntimeRunnerIdentity, + sshRelayRuntimeStripVersionProbe } from './ssh-relay-runtime-toolchain.mjs' const commit = 'a'.repeat(40) @@ -26,6 +27,20 @@ describe('SSH relay runtime build provenance', () => { ).toBe('Microsoft (R) C/C++ Optimizing Compiler Version 19.44.35228 for ARM64') }) + it('selects the Windows linker version from bounded help output', () => { + expect( + selectSshRelayRuntimeToolVersion( + { + stdout: [ + 'Microsoft (R) Incremental Linker Version 14.44.35228.0', + 'usage: LINK [options] [files]' + ].join('\r\n') + }, + /Linker Version/i + ) + ).toBe('Microsoft (R) Incremental Linker Version 14.44.35228.0') + }) + it('pins GitHub builder identity to the exact source commit', () => { expect( sshRelayRuntimeBuilderIdentity({ @@ -83,6 +98,14 @@ describe('SSH relay runtime build provenance', () => { ).toThrow(/runner identity/i) }) + it('requests an actual GNU strip version and pins Apple strip to Xcode', () => { + expect(sshRelayRuntimeStripVersionProbe('linux')).toEqual({ args: ['--version'] }) + expect(sshRelayRuntimeStripVersionProbe('darwin')).toEqual({ + versionCommand: 'xcodebuild', + versionArgs: ['-version'] + }) + }) + it('records bounded native tool versions and SHA-256 executable or code digests', async () => { const toolchain = await collectSshRelayRuntimeToolchain(process.execPath) const tuple = process.platform === 'win32' ? 'win32-x64' : 'linux-x64-glibc' diff --git a/config/scripts/ssh-relay-runtime-tree.mjs b/config/scripts/ssh-relay-runtime-tree.mjs index ffe15e33f05..1316f81ef08 100644 --- a/config/scripts/ssh-relay-runtime-tree.mjs +++ b/config/scripts/ssh-relay-runtime-tree.mjs @@ -44,14 +44,14 @@ const COMPATIBILITY = Object.freeze({ 'darwin-arm64': { kind: 'darwin', minimumVersion: '13.5' }, 'win32-x64': { kind: 'win32', - minimumBuild: 20348, + minimumBuild: 19045, minimumOpenSshVersion: '8.1p1', minimumPowerShellVersion: '5.1', minimumDotNetFrameworkRelease: 528040 }, 'win32-arm64': { kind: 'win32', - minimumBuild: 20348, + minimumBuild: 26100, minimumOpenSshVersion: '8.1p1', minimumPowerShellVersion: '5.1', minimumDotNetFrameworkRelease: 528040 diff --git a/config/scripts/ssh-relay-runtime-windows-tree.test.mjs b/config/scripts/ssh-relay-runtime-windows-tree.test.mjs index 0575696ef56..ddcf23bab92 100644 --- a/config/scripts/ssh-relay-runtime-windows-tree.test.mjs +++ b/config/scripts/ssh-relay-runtime-windows-tree.test.mjs @@ -48,7 +48,10 @@ async function createWindowsNodePtyBuild(root) { } describe('SSH relay Windows runtime tree', () => { - it('carries the production ConPTY runtime closure in the hashed identity', async () => { + it.each([ + ['win32-x64', 19045], + ['win32-arm64', 26100] + ])('carries the %s ConPTY closure and reviewed build floor', async (tuple, minimumBuild) => { const directory = await mkdtemp(join(tmpdir(), 'orca-runtime-windows-tree-')) temporaryDirectories.push(directory) const nodeRoot = join(directory, 'node') @@ -72,7 +75,7 @@ describe('SSH relay Windows runtime tree', () => { ]) const identity = await assembleSshRelayRuntimeTree({ - tuple: 'win32-x64', + tuple, nodeRoot, nodePtyBuildDirectory, relayDirectory, @@ -80,6 +83,8 @@ describe('SSH relay Windows runtime tree', () => { nodeVersion: '24.18.0' }) + expect(identity.compatibility.minimumBuild).toBe(minimumBuild) + for (const name of ['conpty.dll', 'OpenConsole.exe']) { const path = `node_modules/node-pty/build/Release/conpty/${name}` expect(identity.entries).toContainEqual( diff --git a/config/scripts/ssh-relay-runtime-zip.test.mjs b/config/scripts/ssh-relay-runtime-zip.test.mjs index 5aa3669bc49..00dcab442c8 100644 --- a/config/scripts/ssh-relay-runtime-zip.test.mjs +++ b/config/scripts/ssh-relay-runtime-zip.test.mjs @@ -34,7 +34,7 @@ async function fixture() { architecture: 'x64', compatibility: { kind: 'win32', - minimumBuild: 20348, + minimumBuild: 19045, minimumOpenSshVersion: '8.1p1', minimumPowerShellVersion: '5.1', minimumDotNetFrameworkRelease: 528040 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 4f53e5377c4..9400fd1623b 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 @@ -18,9 +18,10 @@ Approved design decisions are listed separately and are not presented as impleme - Latest native proof: [GitHub Actions run 29367559831](https://github.com/stablyai/orca/actions/runs/29367559831) passed clean-build equality and runtime smoke on Linux x64/arm64, macOS x64/arm64, and Windows x64/arm64. -- Active work package: exact runtime closure is green on all six native runner families; the SPDX, - builder/runner identity, and hashed-toolchain correction is locally green and awaiting the same - native proof. Archive safety, oldest-baseline execution, and native trust remain open. +- Active work package: exact runtime closure is green on all six native runner families. The first + metadata run failed closed before Windows build/upload because its linker-version probe was + silent; the bounded probe correction and reviewed Windows 19045/26100 build-floor correction are + in progress. Archive safety, oldest-baseline execution, and native trust remain open. ## Locked rollout decisions 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 3f621096a53..1fcbcb51144 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 SBOM/provenance closure — exact-head run [29367559831](https://github.com/stablyai/orca/actions/runs/29367559831) at ledger head `ace3d4f41` passes the builder-enforced 34/35/42-file closure, unchanged runtime content IDs, complete smoke, clean-build equality, and upload on all six native runner families under E-M3-RUNTIME-CLOSURE-CI-001; exact metadata implementation commit `26bbd7b67` adds file-to-package SPDX ownership/dependency relationships, archive-scoped document identity, exact-commit builder identity, resolved runner image/architecture, and bounded SHA-256 records for native executables and complete build-package trees, and is locally green under E-M3-METADATA-LOCAL-001; the next exact-head six-cell run must prove those records and metadata equality target-natively before the remaining SBOM/provenance/toolchain boxes are checked; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 SBOM/provenance closure — **In progress — 2026-07-14, Codex implementation owner**; exact-head run [29368482959](https://github.com/stablyai/orca/actions/runs/29368482959) proves the corrected metadata path on the completed POSIX cells but fails closed in both Windows contract-test cells before build or upload because direct no-argument `link.exe` invocation returns no selectable version line; the active correction requests bounded linker help explicitly and must pass both native Windows architectures plus clean-build metadata equality before any remaining SBOM/provenance/toolchain box is checked; the same evidence audit exposed that generated x64/arm64 Windows compatibility floors were both 20348 despite the reviewed 19045/26100 contract, so the plan now makes the single monotonic manifest floors explicit before correcting code and regenerating native identities; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      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) @@ -282,7 +282,9 @@ No production implementation begins until every blocking item has an owner and d - [x] Define macOS 13.5 as the minimum for both x64 and arm64 candidates. (E-M1-BASELINE-001) - [x] Define the initial Windows candidates as Windows 10 22H2 build 19045 or Server 2022 build 20348 for x64, and Windows 11 24H2 build 26100 for arm64, with OpenSSH for Windows 8.1p1, - Windows PowerShell 5.1, and .NET Framework 4.8 as minimum bootstrap primitives. + Windows PowerShell 5.1, and .NET Framework 4.8 as minimum bootstrap primitives. Because the + manifest has one monotonic `minimumBuild` per tuple, encode 19045 for x64 (which admits Server + 2022 build 20348 and newer) and 26100 for arm64. (E-M1-BASELINE-001) - [x] Treat a Rosetta-translated shell as x64 process architecture, but keep it on legacy until an x64 artifact passes a live Rosetta SSH cell. A native arm64 shell selects arm64; detection @@ -836,6 +838,8 @@ Rules: dependencies, spawns a real PTY, performs input/resize/exit, and observes create/modify/rename/delete watcher events. (E-M3-REPRODUCIBILITY-CI-001) - [ ] Every build records compiler/toolchain/container image digests and runner architecture. + **In progress — 2026-07-14, Codex implementation owner:** run 29368482959 failed closed on + both Windows architectures at the linker-version probe; no Windows build or upload followed. ## Milestone 4 — Make Relay Artifacts Prerequisites of Desktop Builds @@ -6246,11 +6250,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push exact metadata implementation commit `26bbd7b67` plus its evidence-ledger head and rerun all six -target-native cells. Require exact SPDX ownership, archive-scoped document identity, commit-pinned -builder identity, resolved runner identity, complete bounded tool records with SHA-256, clean-build -metadata equality, smoke, and upload before checking the remaining Milestone 3 metadata/toolchain -claims. Then proceed to oldest-baseline and native-trust proof. +Finish the bounded Windows linker-version correction and the reviewed 19045/26100 Windows +compatibility-floor correction, run the focused local gates, and rerun all six target-native cells. +Require exact SPDX ownership, archive-scoped document identity, commit-pinned builder identity, +resolved runner identity, complete bounded tool records with SHA-256, clean-build metadata equality, +smoke, upload, and regenerated Windows content identities before checking the remaining Milestone 3 +metadata/toolchain claims. Then proceed to oldest-baseline and native-trust proof. 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 diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 09adbb1be72..64eb43b7ab2 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1124,8 +1124,10 @@

      Build for the runtime, architecture, and libc together.

      candidates require kernel 4.18, glibc 2.28, and libstdc++ 6.0.25 with GLIBCXX_3.4.25; macOS candidates require 13.5. Initial Windows x64 candidates require Windows 10 22H2 build 19045 or Server 2022 build 20348, and arm64 - requires Windows 11 24H2 build 26100. Windows bootstrap additionally requires OpenSSH - for Windows 8.1p1, Windows PowerShell 5.1, and .NET Framework 4.8. + requires Windows 11 24H2 build 26100. The immutable manifest's single monotonic + minimumBuild is therefore 19045 for x64 (which also admits Server 2022 + build 20348 and newer builds) and 26100 for arm64. Windows bootstrap additionally + requires OpenSSH for Windows 8.1p1, Windows PowerShell 5.1, and .NET Framework 4.8.

      Node's detached checksum signature is verified against a build-input-pinned official diff --git a/src/main/ssh/ssh-relay-artifact-selector.test.ts b/src/main/ssh/ssh-relay-artifact-selector.test.ts index b63a095143f..522a0e791e6 100644 --- a/src/main/ssh/ssh-relay-artifact-selector.test.ts +++ b/src/main/ssh/ssh-relay-artifact-selector.test.ts @@ -16,16 +16,22 @@ const compatibleLinuxHost = { glibcxxVersion: '3.4.33' } -function windowsManifest() { +function windowsManifest({ + architecture = 'x64', + minimumBuild = architecture === 'x64' ? 19045 : 26100 +}: { + architecture?: 'x64' | 'arm64' + minimumBuild?: number +} = {}) { const manifest = createSshRelayArtifactTestManifest() const tuple = structuredClone(manifest.tuples[0]) as unknown as Record Object.assign(tuple, { - tupleId: 'win32-x64', + tupleId: `win32-${architecture}`, os: 'win32', - architecture: 'x64', + architecture, compatibility: { kind: 'windows', - minimumBuild: 20348, + minimumBuild, minimumOpenSshVersion: '8.1p1', minimumPowerShellVersion: '5.1', minimumDotNetFrameworkRelease: 528040 @@ -39,7 +45,7 @@ const compatibleWindowsHost = { os: 'win32' as const, architecture: 'x64' as const, processTranslated: false, - build: 20348, + build: 19045, openSshVersion: '8.1p1', powerShellVersion: '5.1', dotNetFrameworkRelease: 528040 @@ -130,7 +136,7 @@ describe('SSH relay artifact selector', () => { it('checks Windows bootstrap versions before selection', () => { const manifest = windowsManifest() expect(selectSshRelayArtifact(manifest, compatibleWindowsHost).kind).toBe('selected') - expect(selectSshRelayArtifact(manifest, { ...compatibleWindowsHost, build: 20347 })).toEqual({ + expect(selectSshRelayArtifact(manifest, { ...compatibleWindowsHost, build: 19044 })).toEqual({ kind: 'legacy', reason: 'os-too-old' }) @@ -154,6 +160,25 @@ describe('SSH relay artifact selector', () => { ).toEqual({ kind: 'legacy', reason: 'dotnet-too-old' }) }) + it.each([ + ['x64', 19044, 19045], + ['arm64', 26099, 26100] + ] as const)( + 'enforces the reviewed Windows %s build boundary', + (architecture, rejectedBuild, acceptedBuild) => { + const manifest = windowsManifest({ architecture }) + const host = { ...compatibleWindowsHost, architecture } + + expect(selectSshRelayArtifact(manifest, { ...host, build: rejectedBuild })).toEqual({ + kind: 'legacy', + reason: 'os-too-old' + }) + expect(selectSshRelayArtifact(manifest, { ...host, build: acceptedBuild }).kind).toBe( + 'selected' + ) + } + ) + it('selects legacy when Windows bootstrap evidence is absent or malformed', () => { const manifest = windowsManifest() expect( From c906c21ce117021885ec4db68c62ef28c141d7ba Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 17:23:11 -0400 Subject: [PATCH 060/267] docs(ssh): record native metadata probe failures Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 5 +- ...github-release-implementation-checklist.md | 128 ++++++++++++++++-- 2 files changed, 118 insertions(+), 15 deletions(-) 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 9400fd1623b..c40bbd3e6c7 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 @@ -20,8 +20,9 @@ Approved design decisions are listed separately and are not presented as impleme x64/arm64. - Active work package: exact runtime closure is green on all six native runner families. The first metadata run failed closed before Windows build/upload because its linker-version probe was - silent; the bounded probe correction and reviewed Windows 19045/26100 build-floor correction are - in progress. Archive safety, oldest-baseline execution, and native trust remain open. + silent and direct inspection found versionless Linux strip records. The bounded probe and reviewed + Windows 19045/26100 build-floor corrections are locally green and await a fresh all-six native + proof. Archive safety, oldest-baseline execution, and native trust remain open. ## Locked rollout decisions 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 1fcbcb51144..d9b7c2762cb 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 SBOM/provenance closure — **In progress — 2026-07-14, Codex implementation owner**; exact-head run [29368482959](https://github.com/stablyai/orca/actions/runs/29368482959) proves the corrected metadata path on the completed POSIX cells but fails closed in both Windows contract-test cells before build or upload because direct no-argument `link.exe` invocation returns no selectable version line; the active correction requests bounded linker help explicitly and must pass both native Windows architectures plus clean-build metadata equality before any remaining SBOM/provenance/toolchain box is checked; the same evidence audit exposed that generated x64/arm64 Windows compatibility floors were both 20348 despite the reviewed 19045/26100 contract, so the plan now makes the single monotonic manifest floors explicit before correcting code and regenerating native identities; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 SBOM/provenance closure — **In progress — 2026-07-14, Codex implementation owner**; exact-head run [29368482959](https://github.com/stablyai/orca/actions/runs/29368482959) proves exact metadata/build equality and upload on all four POSIX cells but fails closed in both Windows contract-test cells before build or upload because direct no-argument `link.exe` invocation returns no selectable version line (E-M3-METADATA-CI-RED-001); artifact inspection also shows both Linux `strip` records contain a usage line rather than a version; exact implementation commit `e3f76d3ba` requests bounded linker help and GNU strip version output, encodes the reviewed single monotonic Windows build floors as x64 19045 and arm64 26100, and passes 20 files / 96 tests plus static gates under E-M3-METADATA-CORRECTION-LOCAL-001; the next exact-head run must pass all six native cells and direct metadata inspection before any remaining SBOM/provenance/toolchain box is checked; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      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) @@ -175,12 +175,12 @@ 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: push exact metadata implementation commit `26bbd7b67` plus its evidence- - ledger head and rerun all six target-native cells. Require exact SPDX ownership, archive-scoped - document identity, commit-pinned builder identity, resolved runner identity, complete bounded tool - records with SHA-256, clean-build metadata equality, smoke, and upload before checking the remaining - Milestone 3 metadata/toolchain claims. Preserve the artifact-only boundary, legacy/default path, - and every other release gate. +- Next required action: push exact correction commit `e3f76d3ba` plus its evidence-ledger head and + rerun all six target-native cells. Require exact SPDX ownership, archive-scoped document identity, + commit-pinned builder identity, resolved runner identity, complete bounded tool records with + SHA-256, clean-build metadata equality, smoke, upload, and regenerated Windows identities before + checking the remaining Milestone 3 metadata/toolchain claims. Preserve the artifact-only boundary, + legacy/default path, and every other release gate. ## Non-Negotiable Invariants @@ -6193,6 +6193,108 @@ the bounded depth` before opening or parsing a candidate. Starting at the comple each uploaded SPDX/provenance document, and require complete bounded records and clean-build equality before closing the remaining Milestone 3 metadata/toolchain claims. +### E-M3-METADATA-CI-RED-001 — Native metadata probes fail closed on Windows + +- Date: 2026-07-14 +- Commit SHA / PR: exact tested head `27e932c8cbc44f09d9b410d3aca519e7d1b8e9fe`; + stacked draft PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: [GitHub Actions run 29368482959](https://github.com/stablyai/orca/actions/runs/29368482959), + final conclusion `failure` after 7m56s. Native job IDs: Linux x64 `87205814073`, Linux arm64 + `87205814134`, macOS x64 `87205814075`, macOS arm64 `87205814110`, Windows x64 `87205814074`, + and Windows arm64 `87205814066`. +- Remote and transport: none; target-native artifact-only build workflow +- Exact evidence commands: + + ```sh + gh run view 29368482959 --repo stablyai/orca --json status,conclusion,headSha,url,jobs + gh api repos/stablyai/orca/actions/jobs/87205814074/logs + gh api repos/stablyai/orca/actions/jobs/87205814066/logs + gh api repos/stablyai/orca/actions/runs/29368482959/artifacts + gh run download 29368482959 --repo stablyai/orca --dir /tmp/orca-8450-metadata-red-29368482959 + # Parse every downloaded identity/SPDX/provenance document and hash every archive locally. + ``` + +- Result: EXPECTED RED. Linux x64 completed in 2m58s, Linux arm64 in 3m24s, macOS arm64 in 4m08s, + and macOS x64 in 7m56s; each passed the 19-file/76-test native contract suite, built twice, smoked, + compared every runtime/archive/identity/SPDX/provenance byte exactly, and uploaded. Windows x64 + failed in 1m32s and Windows arm64 in 4m09s at + `ssh-relay-runtime-toolchain.test.mjs` before input download, build, smoke, comparison, or upload: + line 217 could not select a linker version because direct no-argument `link.exe` invocation was + silent on both hosted MSVC environments. The four POSIX artifact IDs are Linux x64 `8325127644`, + Linux arm64 `8325138929`, macOS arm64 `8325153830`, and macOS x64 `8325244087`; no Windows + artifact exists for this run. +- Downloaded-artifact audit: all four archive hashes match identity and provenance; every SPDX + namespace is scoped to the exact archive SHA-256; all 34 Linux and 35 macOS files have exactly one + package `CONTAINS` owner; each document has eight relay dependency relationships; builder URLs pin + exact commit `27e932c8c`; and runner labels, OS, architecture, hosted environment, image OS, and + image version are present and correct. POSIX content IDs remain `960546cd…` Linux x64, + `aa3aa8ae…` Linux arm64, `585ea603…` macOS x64, and `40ff5d20…` macOS arm64. +- Additional defect exposed by direct payload inspection: both Linux provenance documents record + the first no-argument GNU `strip` usage line instead of a version. macOS records Xcode 16.4 and + has no equivalent defect. A green job alone would not have caught this semantic provenance gap. +- Oracle proved: native metadata generation, equality, archive-scoped SPDX identity, package/file + ownership, dependency relationships, commit-pinned builder identity, and runner identity work on + all four POSIX cells; both Windows cells reject incomplete linker provenance before producing or + uploading bytes; direct artifact audit detects semantically wrong tool-version records. +- Does not prove: any Windows metadata/build output, valid GNU strip version records, complete + all-six toolchain provenance, regenerated Windows compatibility identities, oldest baselines, + native trust/signing, SSH, publication, or an enabled tuple. +- Checklist items satisfied: no new completion box; the compiler/toolchain item remains in progress. +- Follow-up: explicitly request bounded linker help, request GNU `strip --version`, correct the + reviewed Windows 19045/26100 compatibility floors, and rerun every native cell from one exact head. + +### E-M3-METADATA-CORRECTION-LOCAL-001 — Native version probes and Windows build floors + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `e3f76d3ba70c860242b5300aa765fbf75ef21317`; stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: local macOS 26.2 build 25C56, Darwin 25.2.0 arm64 on Apple Silicon; Node v26.0.0 and + pnpm 10.24.0 for pure contract tests +- Remote and transport: none; artifact/selector-only local contract tests +- Exact evidence commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-*.test.mjs \ + src/main/ssh/ssh-relay-artifact-selector.test.ts + 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-toolchain.mjs \ + config/scripts/ssh-relay-runtime-toolchain.test.mjs \ + config/scripts/ssh-relay-runtime-tree.mjs \ + config/scripts/ssh-relay-runtime-windows-tree.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs \ + src/main/ssh/ssh-relay-artifact-selector.test.ts \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html \ + 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. Twenty test files passed 96 tests; typecheck, full lint and its + reliability/localization/line-budget sub-gates, max-lines ratchet, focused formatting, and diff + checks passed. The only full-lint diagnostics are pre-existing unrelated warnings. `link.exe` is + now invoked with `/?`, GNU strip with `--version`, and Apple strip remains tied to the selected + Xcode version. Generated Windows runtime identities now encode the reviewed monotonic build floors: + x64 `19045` and arm64 `26100`. Purpose-named selector tests reject x64 `19044` and arm64 `26099` + while accepting x64 `19045` and arm64 `26100`; both runtime-tree variants carry the same floors. +- Oracle proved: the two native probe regressions have bounded explicit invocation contracts; the + artifact producer, ZIP fixture, and desktop selector tests agree with the reviewed Windows + compatibility decision; unknown/older Windows evidence still takes the classified legacy path. +- Does not prove: either corrected probe on a native Windows/Linux runner, actual compiler/linker/ + strip values in uploaded provenance, clean-build equality after Windows identity changes, oldest + Windows execution, native trust/signing, SSH, publication, or an enabled tuple. The Windows + compatibility change intentionally changes both Windows content IDs and requires fresh native + evidence. +- Checklist items satisfied: local correction only; the compiler/toolchain, oldest-baseline, and + native-trust boxes remain unchecked. +- Follow-up: push the exact implementation and ledger head, then require all six native jobs plus + direct inspection of every uploaded metadata document before closing the metadata/toolchain gate. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -6250,12 +6352,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Finish the bounded Windows linker-version correction and the reviewed 19045/26100 Windows -compatibility-floor correction, run the focused local gates, and rerun all six target-native cells. -Require exact SPDX ownership, archive-scoped document identity, commit-pinned builder identity, -resolved runner identity, complete bounded tool records with SHA-256, clean-build metadata equality, -smoke, upload, and regenerated Windows content identities before checking the remaining Milestone 3 -metadata/toolchain claims. Then proceed to oldest-baseline and native-trust proof. +Push exact correction commit `e3f76d3ba` plus this evidence-ledger head and rerun all six +target-native cells. Require exact SPDX ownership, archive-scoped document identity, commit-pinned +builder identity, resolved runner identity, complete bounded tool records with SHA-256, clean-build +metadata equality, smoke, upload, and regenerated Windows content identities before checking the +remaining Milestone 3 metadata/toolchain claims. Then proceed to oldest-baseline and native-trust +proof. 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 From e85f0c700c1bd40e76e08ef5063f9b3128acb600 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 17:27:55 -0400 Subject: [PATCH 061/267] build(ssh): record linker PE file version Co-authored-by: Orca --- .../scripts/ssh-relay-runtime-toolchain.mjs | 27 +++++++++++++--- .../ssh-relay-runtime-toolchain.test.mjs | 32 ++++++++++++------- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/config/scripts/ssh-relay-runtime-toolchain.mjs b/config/scripts/ssh-relay-runtime-toolchain.mjs index 72ee65e002e..04f2976a3ad 100644 --- a/config/scripts/ssh-relay-runtime-toolchain.mjs +++ b/config/scripts/ssh-relay-runtime-toolchain.mjs @@ -80,10 +80,14 @@ async function executableRecord({ versionCommand, versionArgs, versionPattern, + windowsFileVersion = false, xcrun = false }) { const path = await locateExecutable(command, { xcrun }) - const result = await capture(versionCommand ?? path, versionArgs ?? args) + const invocation = windowsFileVersion + ? sshRelayRuntimeWindowsFileVersionInvocation(path) + : { command: versionCommand ?? path, args: versionArgs ?? args } + const result = await capture(invocation.command, invocation.args) return { version: selectSshRelayRuntimeToolVersion(result, versionPattern), sha256: await sha256File(path) @@ -181,6 +185,20 @@ export function sshRelayRuntimeStripVersionProbe(platform) { : { args: ['--version'] } } +export function sshRelayRuntimeWindowsFileVersionInvocation(path) { + return { + command: 'pwsh.exe', + args: [ + '-NoLogo', + '-NoProfile', + '-NonInteractive', + '-Command', + '[System.Diagnostics.FileVersionInfo]::GetVersionInfo($args[0]).FileVersion', + path + ] + } +} + export function assertSshRelayRuntimeToolchain(toolchain, tuple) { const common = ['buildNode', 'bundledNode', 'compiler', 'buildSystem', 'python', 'archive'] const expected = tuple.startsWith('win32-') @@ -224,9 +242,10 @@ export async function collectSshRelayRuntimeToolchain(nodePath) { }), linker: await executableRecord({ command: 'link.exe', - // Why: direct no-argument invocation is silent on current hosted MSVC runners. - args: ['/?'], - versionPattern: /Linker Version/i + args: [], + // Why: hosted link.exe is silent when piped; authenticated PE metadata gives its real version. + windowsFileVersion: true, + versionPattern: /^\d+(?:\.\d+){2,3}$/ }), buildSystem: await executableRecord({ command: 'msbuild.exe', diff --git a/config/scripts/ssh-relay-runtime-toolchain.test.mjs b/config/scripts/ssh-relay-runtime-toolchain.test.mjs index 6fd6d4c3881..4b9ee2c42d6 100644 --- a/config/scripts/ssh-relay-runtime-toolchain.test.mjs +++ b/config/scripts/ssh-relay-runtime-toolchain.test.mjs @@ -6,7 +6,8 @@ import { selectSshRelayRuntimeToolVersion, sshRelayRuntimeBuilderIdentity, sshRelayRuntimeRunnerIdentity, - sshRelayRuntimeStripVersionProbe + sshRelayRuntimeStripVersionProbe, + sshRelayRuntimeWindowsFileVersionInvocation } from './ssh-relay-runtime-toolchain.mjs' const commit = 'a'.repeat(40) @@ -27,18 +28,25 @@ describe('SSH relay runtime build provenance', () => { ).toBe('Microsoft (R) C/C++ Optimizing Compiler Version 19.44.35228 for ARM64') }) - it('selects the Windows linker version from bounded help output', () => { + it('selects a bounded Windows linker file version', () => { expect( - selectSshRelayRuntimeToolVersion( - { - stdout: [ - 'Microsoft (R) Incremental Linker Version 14.44.35228.0', - 'usage: LINK [options] [files]' - ].join('\r\n') - }, - /Linker Version/i - ) - ).toBe('Microsoft (R) Incremental Linker Version 14.44.35228.0') + selectSshRelayRuntimeToolVersion({ stdout: '14.44.35228.0\r\n' }, /^\d+(?:\.\d+){2,3}$/) + ).toBe('14.44.35228.0') + }) + + it('passes the resolved linker path as a non-interpolated PowerShell argument', () => { + const path = String.raw`C:\Program Files\Microsoft Visual Studio\link.exe` + expect(sshRelayRuntimeWindowsFileVersionInvocation(path)).toEqual({ + command: 'pwsh.exe', + args: [ + '-NoLogo', + '-NoProfile', + '-NonInteractive', + '-Command', + '[System.Diagnostics.FileVersionInfo]::GetVersionInfo($args[0]).FileVersion', + path + ] + }) }) it('pins GitHub builder identity to the exact source commit', () => { From 01acba8608e3446bb921cfb812a7026817a53032 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 17:32:32 -0400 Subject: [PATCH 062/267] docs(ssh): record native linker probe evidence Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 5 +- ...github-release-implementation-checklist.md | 91 ++++++++++++++++++- 2 files changed, 91 insertions(+), 5 deletions(-) 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 c40bbd3e6c7..5a40324b38c 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 @@ -21,8 +21,9 @@ Approved design decisions are listed separately and are not presented as impleme - Active work package: exact runtime closure is green on all six native runner families. The first metadata run failed closed before Windows build/upload because its linker-version probe was silent and direct inspection found versionless Linux strip records. The bounded probe and reviewed - Windows 19045/26100 build-floor corrections are locally green and await a fresh all-six native - proof. Archive safety, oldest-baseline execution, and native trust remain open. + Windows 19045/26100 build-floor corrections fixed Linux, but linker help remained silent on both + Windows runners. Reading the hashed linker's PE file version is locally green and awaits a fresh + all-six native proof. Archive safety, oldest-baseline execution, and native trust remain open. ## Locked rollout decisions 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 d9b7c2762cb..e1acae31a28 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 SBOM/provenance closure — **In progress — 2026-07-14, Codex implementation owner**; exact-head run [29368482959](https://github.com/stablyai/orca/actions/runs/29368482959) proves exact metadata/build equality and upload on all four POSIX cells but fails closed in both Windows contract-test cells before build or upload because direct no-argument `link.exe` invocation returns no selectable version line (E-M3-METADATA-CI-RED-001); artifact inspection also shows both Linux `strip` records contain a usage line rather than a version; exact implementation commit `e3f76d3ba` requests bounded linker help and GNU strip version output, encodes the reviewed single monotonic Windows build floors as x64 19045 and arm64 26100, and passes 20 files / 96 tests plus static gates under E-M3-METADATA-CORRECTION-LOCAL-001; the next exact-head run must pass all six native cells and direct metadata inspection before any remaining SBOM/provenance/toolchain box is checked; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 SBOM/provenance closure — **In progress — 2026-07-14, Codex implementation owner**; exact-head run [29369350259](https://github.com/stablyai/orca/actions/runs/29369350259) proves corrected GNU strip versions, exact metadata/build equality, and upload on all four POSIX cells but shows `link.exe /?` is also silent when piped on both Windows architectures, which fail closed before build or upload (E-M3-METADATA-CI-RED-002); exact implementation commit `e85f0c700` retains SHA-256 of the resolved linker and reads its authenticated PE `FileVersion` through non-interactive PowerShell with the path passed as a positional argument, and passes 20 files / 97 tests plus static gates under E-M3-WINDOWS-LINKER-FILE-VERSION-LOCAL-001; the next exact-head run must pass all six native cells and direct metadata inspection before any remaining SBOM/provenance/toolchain box is checked; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      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) @@ -175,7 +175,7 @@ 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: push exact correction commit `e3f76d3ba` plus its evidence-ledger head and +- Next required action: push exact correction commit `e85f0c700` plus its evidence-ledger head and rerun all six target-native cells. Require exact SPDX ownership, archive-scoped document identity, commit-pinned builder identity, resolved runner identity, complete bounded tool records with SHA-256, clean-build metadata equality, smoke, upload, and regenerated Windows identities before @@ -6294,6 +6294,91 @@ the bounded depth` before opening or parsing a candidate. Starting at the comple native-trust boxes remain unchecked. - Follow-up: push the exact implementation and ledger head, then require all six native jobs plus direct inspection of every uploaded metadata document before closing the metadata/toolchain gate. +- Subsequent native evidence: E-M3-METADATA-CI-RED-002 disproves the locally inferred + `link.exe /?` behavior; + both hosted Windows architectures remain silent when the help output is piped by Node. + +### E-M3-METADATA-CI-RED-002 — Linker help remains silent on native Windows + +- Date: 2026-07-14 +- Commit SHA / PR: exact tested head `c906c21ce117021885ec4db68c62ef28c141d7ba`; + stacked draft PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: [GitHub Actions run 29369350259](https://github.com/stablyai/orca/actions/runs/29369350259), + final conclusion `failure` after 6m15s. Native job IDs: Linux x64 `87208675566`, Linux arm64 + `87208675357`, macOS x64 `87208675363`, macOS arm64 `87208675377`, Windows x64 `87208675364`, + and Windows arm64 `87208675391`. +- Remote and transport: none; target-native artifact-only build workflow +- Exact evidence commands: + + ```sh + gh run view 29369350259 --repo stablyai/orca --json status,conclusion,headSha,jobs + gh api repos/stablyai/orca/actions/jobs/87208675364/logs + gh api repos/stablyai/orca/actions/jobs/87208675391/logs + gh api repos/stablyai/orca/actions/runs/29369350259/artifacts + gh run download 29369350259 --repo stablyai/orca --dir /tmp/orca-8450-metadata-red-29369350259 + # Parse every downloaded identity/SPDX/provenance document and hash every archive locally. + ``` + +- Result: EXPECTED RED. Linux x64 completed in 2m53s, Linux arm64 in 3m30s, macOS arm64 in 2m52s, + and macOS x64 in 6m14s; each passed native contracts, built twice, smoked, compared every + runtime/archive/identity/SPDX/provenance byte exactly, and uploaded. Windows x64 failed in 1m24s + and Windows arm64 in 3m51s at the same linker-version selector before input download, build, + smoke, comparison, or upload. Explicit `link.exe /?` is still silent when invoked through the + bounded Node child-process pipe on both hosted MSVC environments. The four POSIX artifact IDs are + Linux x64 `8325461679`, Linux arm64 `8325476952`, macOS arm64 `8325460610`, and macOS x64 + `8325542377`; no Windows artifact exists for this run. +- Downloaded-artifact audit: all four archive hashes match identity and provenance; every SPDX + namespace is scoped to the exact archive SHA-256; all 34 Linux and 35 macOS files have exactly one + package owner and eight dependency relationships; builder URLs pin exact commit `c906c21ce`; + runner identities are complete; Linux x64/arm64 now record `GNU strip (GNU Binutils for Ubuntu) +2.42`; and no tool record contains a usage line. POSIX content IDs remain unchanged. +- Oracle proved: the corrected GNU version probe works and stays reproducible on both Linux + architectures; all four POSIX metadata contracts are complete for this head; direct linker banner + parsing is not a portable Windows provenance source and fails closed on both native architectures. +- Does not prove: Windows metadata/build output, regenerated Windows content identities, native + linker file versions, oldest baselines, native trust/signing, SSH, publication, or an enabled tuple. +- Checklist items satisfied: no new completion box; the compiler/toolchain item remains in progress. +- Follow-up: retain SHA-256 of the resolved linker but obtain its actual PE file version without + relying on linker stdout/stderr, then rerun every native cell from one exact head. + +### E-M3-WINDOWS-LINKER-FILE-VERSION-LOCAL-001 — Authenticated linker PE version + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `e85f0c700c1bd40e76e08ef5063f9b3128acb600`; stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: local macOS 26.2 build 25C56, Darwin 25.2.0 arm64 on Apple Silicon; Node v26.0.0 and + pnpm 10.24.0 for pure invocation/metadata tests +- Remote and transport: none; artifact-only local contract tests +- Exact evidence commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-*.test.mjs \ + src/main/ssh/ssh-relay-artifact-selector.test.ts + pnpm run typecheck + pnpm run lint + pnpm exec oxfmt --check \ + config/scripts/ssh-relay-runtime-toolchain.mjs \ + config/scripts/ssh-relay-runtime-toolchain.test.mjs + git diff --check + ``` + +- Result: PASS. Twenty test files passed 97 tests; typecheck, full lint and its + reliability/localization/line-budget sub-gates, focused formatting, and diff checks passed. The + linker record still hashes the resolved `link.exe` bytes. Its version now comes from + `System.Diagnostics.FileVersionInfo` through `pwsh.exe -NoLogo -NoProfile -NonInteractive`; the + resolved path is a separate positional argument and is never interpolated into executable script + text. Only a bounded numeric three- or four-component file version is accepted. +- Oracle proved: the native-silent banner is no longer a provenance dependency; exact linker bytes + remain authenticated; paths containing spaces cannot alter the PowerShell expression; malformed, + missing, or nonnumeric file versions still fail closed. +- Does not prove: the PE file-version call on either native Windows architecture, the exact returned + linker versions, Windows build/equality/upload, regenerated Windows content IDs, oldest baselines, + native trust/signing, SSH, publication, or an enabled tuple. +- Checklist items satisfied: local correction only; the compiler/toolchain item remains unchecked. +- Follow-up: push this exact implementation plus the ledger head and require all six native jobs and + direct inspection of every uploaded metadata document before closing the metadata/toolchain gate. ## Accepted Gaps @@ -6352,7 +6437,7 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push exact correction commit `e3f76d3ba` plus this evidence-ledger head and rerun all six +Push exact correction commit `e85f0c700` plus this evidence-ledger head and rerun all six target-native cells. Require exact SPDX ownership, archive-scoped document identity, commit-pinned builder identity, resolved runner identity, complete bounded tool records with SHA-256, clean-build metadata equality, smoke, upload, and regenerated Windows content identities before checking the From 714308114dee85a6f241a43b75c6ac9122769c3e Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 17:36:56 -0400 Subject: [PATCH 063/267] build(ssh): isolate linker version path input Co-authored-by: Orca --- .../scripts/ssh-relay-runtime-toolchain.mjs | 18 +++++++++------- .../ssh-relay-runtime-toolchain.test.mjs | 21 +++++++++++++------ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/config/scripts/ssh-relay-runtime-toolchain.mjs b/config/scripts/ssh-relay-runtime-toolchain.mjs index 04f2976a3ad..1fbd5162a65 100644 --- a/config/scripts/ssh-relay-runtime-toolchain.mjs +++ b/config/scripts/ssh-relay-runtime-toolchain.mjs @@ -26,10 +26,11 @@ async function sha256File(path) { return `sha256:${hash.digest('hex')}` } -async function capture(command, args) { +async function capture(command, args, { env } = {}) { try { return await execFileAsync(command, args, { encoding: 'utf8', + ...(env ? { env: { ...process.env, ...env } } : {}), maxBuffer: TOOL_OUTPUT_BYTES, timeout: TOOL_TIMEOUT_MS, windowsHide: true @@ -49,7 +50,8 @@ export function selectSshRelayRuntimeToolVersion({ stdout = '', stderr = '' }, p .filter(Boolean) const version = pattern ? lines.find((line) => pattern.test(line)) : lines[0] if (!version || Buffer.byteLength(version) > 512) { - throw new Error('Runtime build tool did not report a bounded version line') + const diagnostic = lines.slice(0, 3).join(' | ').slice(0, 512) || '' + throw new Error(`Runtime build tool did not report a bounded version line: ${diagnostic}`) } return version } @@ -87,7 +89,7 @@ async function executableRecord({ const invocation = windowsFileVersion ? sshRelayRuntimeWindowsFileVersionInvocation(path) : { command: versionCommand ?? path, args: versionArgs ?? args } - const result = await capture(invocation.command, invocation.args) + const result = await capture(invocation.command, invocation.args, invocation.options) return { version: selectSshRelayRuntimeToolVersion(result, versionPattern), sha256: await sha256File(path) @@ -193,9 +195,9 @@ export function sshRelayRuntimeWindowsFileVersionInvocation(path) { '-NoProfile', '-NonInteractive', '-Command', - '[System.Diagnostics.FileVersionInfo]::GetVersionInfo($args[0]).FileVersion', - path - ] + "[System.Diagnostics.FileVersionInfo]::GetVersionInfo([Environment]::GetEnvironmentVariable('ORCA_SSH_RELAY_TOOL_PATH')).FileVersion" + ], + options: { env: { ORCA_SSH_RELAY_TOOL_PATH: path } } } } @@ -243,9 +245,9 @@ export async function collectSshRelayRuntimeToolchain(nodePath) { linker: await executableRecord({ command: 'link.exe', args: [], - // Why: hosted link.exe is silent when piped; authenticated PE metadata gives its real version. + // Why: hosted link.exe is pipe-silent; authenticated PE metadata gives its real version. windowsFileVersion: true, - versionPattern: /^\d+(?:\.\d+){2,3}$/ + versionPattern: /^\d+(?:\.\d+){2,3}(?:\s.*)?$/ }), buildSystem: await executableRecord({ command: 'msbuild.exe', diff --git a/config/scripts/ssh-relay-runtime-toolchain.test.mjs b/config/scripts/ssh-relay-runtime-toolchain.test.mjs index 4b9ee2c42d6..9903908420c 100644 --- a/config/scripts/ssh-relay-runtime-toolchain.test.mjs +++ b/config/scripts/ssh-relay-runtime-toolchain.test.mjs @@ -30,11 +30,14 @@ describe('SSH relay runtime build provenance', () => { it('selects a bounded Windows linker file version', () => { expect( - selectSshRelayRuntimeToolVersion({ stdout: '14.44.35228.0\r\n' }, /^\d+(?:\.\d+){2,3}$/) - ).toBe('14.44.35228.0') + selectSshRelayRuntimeToolVersion( + { stdout: '14.44.35228.0 built by release pipeline\r\n' }, + /^\d+(?:\.\d+){2,3}(?:\s.*)?$/ + ) + ).toBe('14.44.35228.0 built by release pipeline') }) - it('passes the resolved linker path as a non-interpolated PowerShell argument', () => { + it('passes the resolved linker path through a non-interpolated environment value', () => { const path = String.raw`C:\Program Files\Microsoft Visual Studio\link.exe` expect(sshRelayRuntimeWindowsFileVersionInvocation(path)).toEqual({ command: 'pwsh.exe', @@ -43,12 +46,18 @@ describe('SSH relay runtime build provenance', () => { '-NoProfile', '-NonInteractive', '-Command', - '[System.Diagnostics.FileVersionInfo]::GetVersionInfo($args[0]).FileVersion', - path - ] + "[System.Diagnostics.FileVersionInfo]::GetVersionInfo([Environment]::GetEnvironmentVariable('ORCA_SSH_RELAY_TOOL_PATH')).FileVersion" + ], + options: { env: { ORCA_SSH_RELAY_TOOL_PATH: path } } }) }) + it('bounds failed version diagnostics', () => { + expect(() => + selectSshRelayRuntimeToolVersion({ stderr: `unexpected ${'x'.repeat(1_000)}` }, /version/i) + ).toThrow(/^Runtime build tool did not report a bounded version line: unexpected x{490}/) + }) + it('pins GitHub builder identity to the exact source commit', () => { expect( sshRelayRuntimeBuilderIdentity({ From 756165552d31ee962c99a3a454bce2bebb29092b Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 17:49:41 -0400 Subject: [PATCH 064/267] docs(ssh): record linker environment evidence Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 246 ++++++++---------- ...github-release-implementation-checklist.md | 179 ++++++++++++- 2 files changed, 269 insertions(+), 156 deletions(-) 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 5a40324b38c..8fbc377e255 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,171 +1,131 @@ -# SSH Relay Runtime Distribution — Implementation Checklist +# SSH Relay Runtime Distribution — Short Implementation Checklist Last updated: 2026-07-14 -This is the human-readable project tracker. The +Use this file to track the project. The [detailed evidence ledger](./2026-07-14-ssh-relay-github-release-implementation-checklist.md) -contains exact commands, runner identities, artifact hashes, metrics, and residual gaps. - -A checked box here means the implementation or validation has exact evidence in that ledger. -Approved design decisions are listed separately and are not presented as implemented work. - -## Current state - -- Existing SSH relay behavior remains the default for every target. -- The bundled runtime path is not connected to production and no tuple is enabled. -- The future bundled path is a per-SSH-target Beta option, off by default. -- No runtime artifact has been published to a GitHub Release. -- Latest native proof: [GitHub Actions run 29367559831](https://github.com/stablyai/orca/actions/runs/29367559831) - passed clean-build equality and runtime smoke on Linux x64/arm64, macOS x64/arm64, and Windows - x64/arm64. -- Active work package: exact runtime closure is green on all six native runner families. The first - metadata run failed closed before Windows build/upload because its linker-version probe was - silent and direct inspection found versionless Linux strip records. The bounded probe and reviewed - Windows 19045/26100 build-floor corrections fixed Linux, but linker help remained silent on both - Windows runners. Reading the hashed linker's PE file version is locally green and awaits a fresh - all-six native proof. Archive safety, oldest-baseline execution, and native trust remain open. - -## Locked rollout decisions - -- Legacy behavior remains the persisted and effective default. -- The Beta choice is per SSH target, off by default, and shown on target add/edit surfaces. -- Missing, old, imported, unknown, or malformed settings resolve to legacy. -- A mode change applies only on the next connection or explicit reconnect. -- Legacy remains available; removal or narrowing requires a separate review. -- Only availability/compatibility failures may fall back automatically. Integrity, security, and - corruption failures fail closed. - -## Rollout implementation remaining - -- [ ] Implement the per-target configuration field and migration tests. -- [ ] Add the off-by-default Beta-tagged option to SSH target add/edit UI. -- [ ] Implement `bundled-auto` selection for explicitly opted-in targets. -- [ ] Implement automatic legacy fallback only for classified availability/compatibility failures. -- [ ] Fail closed for signature, hash, archive, tree, native-trust, bundled-Node, and cache-corruption - failures. -- [ ] Add the explicit “disable Beta and reconnect” recovery action for fail-closed Beta errors. -- [ ] Collect privacy-safe Beta success, latency, failure-stage, and fallback-reason telemetry. -- [ ] Require a separate reviewed decision before changing any tuple to default-on. +keeps commands, hashes, runner identities, timings, and failure details. + +A checked box means the work has evidence in the detailed ledger. Design approval alone does not +complete a box. + +## Safety status + +- [x] Existing SSH relay installation remains the default for every target. +- [x] The future bundled runtime is a per-target Beta option, off by default. +- [x] Missing, old, imported, unknown, or malformed settings select legacy behavior. +- [x] No bundled tuple is enabled and no runtime artifact is published. +- [x] Integrity, security, and corruption failures are designed to fail closed, not fall back. +- [x] Legacy removal or a default-on change requires a separate reviewed decision. + +## Active work + +- [ ] **WP2 — Finish target-native artifact metadata and provenance.** + - Four POSIX GitHub runner cells pass build, smoke, equality, and metadata upload. + - Windows x64/arm64 currently fail closed while reading the linker PE file version in + [run 29369925932](https://github.com/stablyai/orca/actions/runs/29369925932). + - Local commit `714308114` isolates the linker path through a child environment variable and is + green across 20 test files / 98 tests plus typecheck, lint, formatting, and diff checks. + - Next gate: push that exact correction, rerun all six native jobs, then inspect every uploaded + artifact before completing the metadata/provenance box. -## Work packages +## Work packages, in required order -### 0 — Existing Node/npm resolver correction +### WP0 — Existing Node/npm resolver correction -- [x] Implement coherent remote Node/npm selection. -- [x] Add focused unit/static proof. -- [x] Prove live Linux arm64 SSH behavior. -- [x] Keep this separate from artifact distribution. -- [ ] Merge the independently reviewable fix from draft PR - [#8724](https://github.com/stablyai/orca/pull/8724). +- [x] Implement and unit-test coherent remote Node/npm selection. +- [x] Prove live Linux arm64 SSH/PTY behavior. +- [x] Keep the fix independently reviewable from runtime distribution. +- [ ] Merge draft PR [#8724](https://github.com/stablyai/orca/pull/8724). -### 1 — Manifest, identity, and platform contracts +### WP1 — Contracts only -- [x] Define signed immutable manifest and canonical content identity. -- [x] Define exact release tag/asset URL rules without mutable `latest` lookup. -- [x] Define conservative OS/architecture/libc selection. -- [x] Add hostile manifest/path/schema tests. +- [x] Define immutable signed manifests, content identity, exact asset URLs, and conservative tuple + selection. +- [x] Add hostile manifest, schema, signature, and path tests. - [ ] Finish archive-safety implementation and hostile archive tests. - [ ] Update mode-qualified remote directory parsing and GC compatibility. -- [ ] Merge the contract package from draft PR - [#8728](https://github.com/stablyai/orca/pull/8728). - -### 2 — Target-native runtime artifacts - -- [x] Pin and verify Node v24.18.0 release inputs and signatures. -- [x] Produce and smoke-test candidate runtimes with patched `node-pty` and the matching - `@parcel/watcher` native package on all six runner families. -- [x] Prove bundled Node, real PTY input/resize/exit, watcher events, and Windows resource settlement. -- [x] Prove two clean builds compare exactly on all six native GitHub runner families. -- [x] Keep `/guard:cf`; fix Windows arm64 drift with copied-artifact-only `/INCREMENTAL:NO`. -- [x] Enforce and prove an exact 34/35/42-file per-tuple closure that rejects undeclared package - managers, sources, maps, build outputs, package drift, and missing licenses on all six native - runner families. -- [x] Define and locally test file-to-package SPDX ownership, immutable archive-scoped SBOM identity, - exact-commit builder identity, runner identity, and bounded SHA-256 toolchain records. -- [ ] Complete the runtime closure, license, SBOM, provenance, and prohibited-content allowlist audit. +- [ ] Merge draft PR [#8728](https://github.com/stablyai/orca/pull/8728). + +### WP2 — Target-native runtime artifacts + +- [x] Pin and verify Node v24.18.0 inputs and signatures. +- [x] Build and smoke-test Node, patched `node-pty`, and `@parcel/watcher` on GitHub runners for + Linux, macOS, and Windows on x64 and arm64. +- [x] Prove exact clean-build equality and exact runtime-tree closure on all six runner families. +- [ ] Complete the all-six SBOM, license, provenance, toolchain, and prohibited-content audit. - [ ] Prove each candidate on its oldest supported OS/libc/kernel baseline. -- [ ] Sign macOS and Windows native bytes and verify target-native trust policy. -- [ ] Record complete compiler/toolchain provenance for release promotion. +- [ ] Sign macOS and Windows bytes and verify native trust on the target OS. - [ ] Merge draft PR [#8741](https://github.com/stablyai/orca/pull/8741). -### 3 — Release build, signing, and publication - -- [ ] Add target-native build jobs as release prerequisites. -- [ ] Add native signing jobs and return signed bytes before final hashing. -- [ ] Add the fail-closed aggregate and manifest-signing job. -- [ ] Embed the exact signed manifest and accepted keys into every desktop build. -- [ ] Upload the exact prebuilt runtime bytes to the matching draft release. -- [ ] Verify draft read-back hashes and execute downloaded archives. -- [ ] Test timeout, retry exhaustion, approval denial/timeout, signing failure, partial output, and - recovered-draft behavior. -- [ ] Block publication whenever any required runtime/signing/manifest gate fails. - -### 4 — Desktop resolver, verification, extraction, and cache - -- [ ] Implement offline tuple selection from the embedded signed manifest. -- [ ] Implement exact direct release URL resolution and safe redirect handling. -- [ ] Stream download with size/time bounds and archive SHA-256 verification. -- [ ] Extract into exclusive staging with full hostile-archive defenses. -- [ ] Verify the complete extracted tree before atomic cache publication. -- [ ] Implement the 2 GiB bounded cache, locking, concurrency, quarantine, and eviction rules. -- [ ] Prove verified client-cache transfer works with the client offline. +### WP3 — Release build and signing + +- [ ] 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. +- [ ] 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. + +### WP4 — Desktop resolver and verified cache + +- [ ] Select tuples offline from the embedded manifest and resolve immutable direct asset URLs. +- [ ] Stream bounded downloads; verify signature, size, archive hash, and extracted tree. +- [ ] Add exclusive staging, atomic publication, quarantine, locking, and the 2 GiB cache policy. +- [ ] Prove verified cached bytes can be transferred while the client is offline. - [ ] Preserve `ORCA_RELAY_PATH` behind the official-build trust boundary. -### 5 — Bounded SSH transfer and remote installation +### WP5 — SSH transfer and remote install -- [ ] Implement bounded/cancellable SFTP transfer with executable-mode restoration. -- [ ] Keep compatible POSIX tar streaming as an optional fast path. -- [ ] Implement POSIX no-tar transfer using only the declared shell/file primitives. +- [ ] Implement bounded, cancellable SFTP transfer. +- [ ] Implement POSIX system-SSH transfer with optional tar and mandatory no-tar support. - [ ] Implement bounded binary PowerShell/.NET transfer for Windows system SSH. -- [ ] Transfer only locally verified bytes into exclusive remote staging. -- [ ] Use bundled Node to hash the complete staged tree before any native probe. -- [ ] Run native probes, PTY/watcher smoke, structured sentinel, atomic publish, and launch in order. -- [ ] Trust the initial-install proof on warm launch under the immutable-directory assumption. -- [ ] Quarantine detected mutation and recover only from freshly verified bundled bytes. -- [ ] Preserve SSH authentication, connection, and relay RPC transport behavior. +- [ ] Transfer verified bytes into exclusive staging and hash the complete staged tree with bundled + Node before native probes. +- [ ] Prove probe → PTY/watcher smoke → sentinel → atomic publish → launch ordering. +- [ ] Trust warm installs under the immutable-directory rule; quarantine detected mutation. +- [ ] Keep SSH authentication, connection, and relay RPC transport unchanged. -### 6 — Modes, fallback, diagnostics, and concurrency +### WP6 — Modes, fallback, diagnostics, and races - [ ] Implement internal `legacy`, `auto`, and forced diagnostic `bundled` modes. -- [ ] Abort and await all bundled work before eligible legacy fallback starts. -- [ ] Keep bundled and legacy identities, locks, partials, sentinels, and generations separate. -- [ ] Add stable stage/failure codes and privacy-safe diagnostics. -- [ ] Prove fallback races, reconnect/reattach, concurrent clients, GC, upgrade, and downgrade. -- [ ] Prove every integrity/security/corruption class fails closed in `auto`. -- [ ] Prove eligible legacy fallback begins within the documented delay budget. - -### 7 — Validation and Beta rollout - -- [ ] Name and pin representative POSIX and Windows remote snapshots for Layer B. -- [ ] For every enabled remote tuple, pass built-in SFTP and system SSH. -- [ ] For every supported client OS/architecture, pass both transfer families against representative - POSIX and Windows remotes. -- [ ] Prove remote GitHub egress is never required. -- [ ] Prove no-tar/no-Node/no-Python/no-Perl/no-hash-tool bootstrap cases. -- [ ] Run full-size, slow-link, cancellation, concurrency, and failure-injection tests. +- [ ] Fall back automatically only for classified availability/compatibility failures. +- [ ] Fail closed for signature, hash, archive/tree, native-trust, bundled-Node, and cache-corruption + failures. +- [ ] Abort and await bundled work before eligible legacy fallback begins. +- [ ] Separate bundled/legacy identities, locks, staging, sentinels, and generations. +- [ ] Test reconnect, reattach, concurrent clients, cancellation, GC, upgrades, and downgrades. + +### WP7 — Per-target Beta and validation + +- [ ] Add the per-target mode field with safe migration/default tests. +- [ ] Add the Beta-tagged option to SSH target add/edit UI, off by default. +- [ ] Apply a mode change only on next connection or explicit reconnect. +- [ ] Add actionable fail-closed recovery and privacy-safe Beta telemetry. +- [ ] Run every enabled remote tuple through built-in SFTP and system SSH. +- [ ] Run every supported client OS/architecture against representative POSIX and Windows remotes. +- [ ] Prove no remote GitHub egress, no-tar bootstrap, full-size transfer, slow-link cancellation, + concurrency, RPC, and failure-injection behavior. - [ ] Measure cold/warm latency, memory, channels/files, cancellation settlement, and fallback delay - against legacy baselines. -- [ ] Pass security review, SBOM/provenance, signing, key lifecycle, CVE, and emergency-refresh gates. -- [ ] Ship the per-target Beta with every target still defaulting to legacy. -- [ ] Accumulate approved real-host Beta evidence before any default-on proposal. -- [ ] Require three qualifying RCs plus rollback proof before default-on review. + against legacy. +- [ ] Ship only as per-target, off-by-default Beta; gather real-host evidence before default-on review. +- [ ] Require three qualifying RCs and rollback proof before any default-on proposal. ## External blockers -- [ ] Repository release administrator: approve the representative Layer B remote provider, - snapshots, credentials, egress policy, teardown SLA, and cost/capacity owner. -- [ ] Repository release administrator: provision the protected manifest/native-signing environment, - reviewers, test keys/certificates, and access audit. +- [ ] Release administrator chooses and provisions representative SSH remote snapshots, credentials, + egress rules, teardown SLA, and cost/capacity ownership. +- [ ] Release administrator provisions protected manifest/native-signing environments, reviewers, + test keys/certificates, and access auditing. -Safe behavior while either blocker remains: no bundled tuple is enabled or published; legacy remains -the default, and independent local/native-runner contract work may continue. +While blocked, artifact-only and test work may continue. No tuple may be enabled or published. -## Definition of done +## Final go/no-go gates -- [ ] Every item above that applies to an enabled tuple has exact evidence in the detailed ledger. -- [ ] Every enabled tuple has target-native build, oldest-baseline, native-trust, SFTP, system-SSH, - RPC, security, and performance proof. -- [ ] Every packaged desktop embeds the exact signed manifest for the published immutable assets. -- [ ] The Beta ships off by default per target and rollback to legacy is proven in the same build. -- [ ] No default-on rollout occurs without a separate reviewed decision and completed soak evidence. -- [ ] Legacy remains available until a separately reviewed removal decision. +- [ ] Every enabled tuple has native build, oldest-baseline, native-trust, SFTP, system-SSH, RPC, + security, and performance evidence. +- [ ] Every desktop build embeds the signed manifest for the exact immutable assets it ships. +- [ ] Beta rollback to the existing legacy mechanism is proven in the same build. +- [ ] All required matrix cells have evidence IDs in the detailed ledger. +- [ ] Default-on receives a separate review after the Beta soak; legacy remains available until a + separately reviewed removal decision. 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 e1acae31a28..49003dc7734 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 SBOM/provenance closure — **In progress — 2026-07-14, Codex implementation owner**; exact-head run [29369350259](https://github.com/stablyai/orca/actions/runs/29369350259) proves corrected GNU strip versions, exact metadata/build equality, and upload on all four POSIX cells but shows `link.exe /?` is also silent when piped on both Windows architectures, which fail closed before build or upload (E-M3-METADATA-CI-RED-002); exact implementation commit `e85f0c700` retains SHA-256 of the resolved linker and reads its authenticated PE `FileVersion` through non-interactive PowerShell with the path passed as a positional argument, and passes 20 files / 97 tests plus static gates under E-M3-WINDOWS-LINKER-FILE-VERSION-LOCAL-001; the next exact-head run must pass all six native cells and direct metadata inspection before any remaining SBOM/provenance/toolchain box is checked; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 SBOM/provenance closure — **In progress — 2026-07-14, Codex implementation owner**; exact-head run [29369925932](https://github.com/stablyai/orca/actions/runs/29369925932) and direct payload audit keep Linux x64/arm64 and macOS x64/arm64 green but both Windows cells fail closed before build/upload while selecting the authenticated linker's PE file version (E-M3-METADATA-CI-RED-003); local commit `714308114` passes that linker path through a dedicated child environment variable, accepts only a bounded valid file-version form, preserves bounded failure diagnostics, and passes 20 files / 98 tests plus typecheck, full lint, formatting, max-lines, and diff gates (E-M3-WINDOWS-LINKER-ENV-LOCAL-001); the correction still requires an exact-head all-six native run and direct artifact inspection before any remaining SBOM/provenance/toolchain box is checked; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      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) @@ -175,12 +175,12 @@ 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: push exact correction commit `e85f0c700` plus its evidence-ledger head and - rerun all six target-native cells. Require exact SPDX ownership, archive-scoped document identity, - commit-pinned builder identity, resolved runner identity, complete bounded tool records with - SHA-256, clean-build metadata equality, smoke, upload, and regenerated Windows identities before - checking the remaining Milestone 3 metadata/toolchain claims. Preserve the artifact-only boundary, - legacy/default path, and every other release gate. +- Next required action: finish the direct audit and ledger entry for run 29369925932, record local + proof for commit `714308114`, then push that exact correction plus the ledger head and rerun all + six target-native cells. Require exact SPDX ownership, archive-scoped document identity, + commit-pinned builder identity, resolved runner identity, complete bounded tool records, + regenerated Windows content identities, smoke, exact clean-build equality, and upload. Preserve + the artifact-only boundary, legacy/default path, and every other release gate. ## Non-Negotiable Invariants @@ -6380,6 +6380,158 @@ the bounded depth` before opening or parsing a candidate. Starting at the comple - Follow-up: push this exact implementation plus the ledger head and require all six native jobs and direct inspection of every uploaded metadata document before closing the metadata/toolchain gate. +### E-M3-METADATA-CI-RED-003 — Positional PE-version lookup remains empty on Windows + +- Date: 2026-07-14 +- Commit SHA / PR: exact tested head `01acba8608e3446bb921cfb812a7026817a53032`; + stacked draft PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: [GitHub Actions run 29369925932](https://github.com/stablyai/orca/actions/runs/29369925932), + final conclusion `failure` after 7m05s. Native job IDs and wall durations: Linux x64 + `87210560419` / 3m03s, Linux arm64 `87210560395` / 3m21s, macOS x64 `87210560388` / + 6m59s, macOS arm64 `87210560695` / 3m18s, Windows x64 `87210560429` / 1m24s, and + Windows arm64 `87210560403` / 4m49s. +- Remote and transport: none; target-native artifact-only build workflow +- Exact evidence commands: + + ```sh + gh run view 29369925932 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh api repos/stablyai/orca/actions/runs/29369925932/artifacts + gh api repos/stablyai/orca/actions/jobs/87210560429/logs + gh api repos/stablyai/orca/actions/jobs/87210560403/logs + gh run download 29369925932 --repo stablyai/orca \ + --dir /tmp/orca-8450-metadata-red-29369925932 + find /tmp/orca-8450-metadata-red-29369925932 -maxdepth 3 -type f -print + bash -eu -o pipefail <<'SH' + evidence=/tmp/orca-8450-metadata-red-29369925932 + commit=01acba8608e3446bb921cfb812a7026817a53032 + run_id=29369925932 + find "$evidence" -type f \( -name '*.tar.xz' -o -name '*.zip' \) \ + -exec shasum -a 256 {} + + for directory in "$evidence"/*; do + identity=$(find "$directory" -type f -name '*.identity.json') + provenance=$(find "$directory" -type f -name '*.provenance.json') + spdx=$(find "$directory" -type f -name '*.spdx.json') + archive=$(find "$directory" -type f \( -name '*.tar.xz' -o -name '*.zip' \)) + archive_sha=$(shasum -a 256 "$archive" | cut -d ' ' -f 1) + jq -e --arg sha "$archive_sha" --arg commit "$commit" --arg run "$run_id" \ + --slurpfile provenance "$provenance" --slurpfile spdx "$spdx" ' + ($spdx[0].relationships | map(select(.relationshipType == "CONTAINS")) | + group_by(.relatedSpdxElement) | + map({key: .[0].relatedSpdxElement, value: length}) | from_entries) as $owners | + .archive.sha256 == ("sha256:" + $sha) and + .fileCount == ($spdx[0].files | length) and + ([.entries[] | select(.type == "file")] | length) == ($spdx[0].files | length) and + all($spdx[0].files[]; $owners[.SPDXID] == 1) and + ($spdx[0].relationships | map(select(.relationshipType == "DEPENDS_ON")) | length) == 8 and + $spdx[0].documentNamespace == + ("https://github.com/stablyai/orca/ssh-relay-runtime/spdx/" + $sha) and + $provenance[0].subject[0].digest.sha256 == $sha and + $provenance[0].predicate.runDetails.metadata.invocationId == $run and + $provenance[0].predicate.runDetails.builder.id == + ("https://github.com/stablyai/orca/blob/" + $commit + + "/.github/workflows/ssh-relay-runtime-artifacts.yml") and + ($provenance[0].predicate.buildDefinition.resolvedDependencies | + any(.digest.gitCommit == $commit and (.uri | endswith("@" + $commit)))) and + ($provenance[0].predicate.runDetails.metadata.runner | + .os != "" and .architecture != "" and .environment == "github-hosted" and + .requestedLabel != "" and .image.os != "" and .image.version != "") and + ($provenance[0].predicate.buildDefinition.internalParameters.toolchain | to_entries | + all(.[]; (.value.version | length) > 0 and + (.value.version | test("usage:"; "i") | not) and + (.value.sha256 | test("^sha256:[0-9a-f]{64}$")))) and + all(.entries[]; + (.path | test( + "(^|/)(npm|npx|corepack)([.](cmd|exe))?$|[.](map|cc|cpp|h|hpp|o|obj|pdb|lib)$"; + "i" + ) | not)) + ' "$identity" >/dev/null + jq -c '{tupleId, contentId, fileCount, archive}' "$identity" + done + SH + ``` + +- Result: EXPECTED RED. All four POSIX cells passed native contracts, built twice, inspected and + smoked both outputs, compared runtime/archive/identity/SPDX/provenance bytes exactly, and uploaded. + Windows x64 and arm64 each failed in `ssh-relay-runtime-toolchain.test.mjs` with `Runtime build +tool did not report a bounded version line`; input download, build, smoke, comparison, and upload + were skipped. The two Windows failures occurred after target-native MSVC setup and dependency + install, so the x64 `windows-2022` and arm64 `windows-11-arm` environments independently disprove + the positional PowerShell path transport. No Windows artifact exists for this run. +- Downloaded-artifact audit: + + | Tuple | Artifact ID | Archive SHA-256 | Content ID prefix | Files | Runner image | + | ----------------- | ------------ | ------------------------------------------------------------------ | ----------------- | ----- | ------------------------------ | + | linux-x64-glibc | `8325687973` | `c71778c6ea716eb694b8d961b6bcf0c2a379761a535fa069434a7e755903b06a` | `960546cd…` | 34 | `ubuntu24` 20260705.232.1 | + | linux-arm64-glibc | `8325696189` | `6dc95917b828675a4700444378b96c89ff5b538e1848b3a167cc10c4702794d7` | `aa3aa8ae…` | 34 | `ubuntu24-arm64` 20260706.52.2 | + | darwin-x64 | `8325779710` | `332510ffba04959fbc16fdce54334fc9cca55f7b87fb19d78f1e4ce570c5b388` | `585ea603…` | 35 | `macos15` 20260629.0276.1 | + | darwin-arm64 | `8325693875` | `1474f808f8d1e5668060468284236ba4c81e395931fa1bf5d3dcbfed4b45a994` | `40ff5d20…` | 35 | `macos15` 20260706.0213.1 | + + Every archive hash matches its identity and provenance subject. Every SPDX namespace ends in the + exact archive hash; every file has exactly one package owner; each document has nine packages and + eight dependency relationships. Builder and source identities pin exact commit `01acba860`; the + invocation is `29369925932`; requested runner labels, native architectures, environment, and image + versions are present. Linux records GCC 13.3.0, GNU Make 4.3, Python 3.12.3, XZ 5.4.5, and GNU + strip 2.42. macOS records Apple clang 17.0.0, GNU Make 3.81, Python 3.14.6, XZ 5.8.3, and Xcode + 16.4. Every tool record carries a SHA-256 and no version contains a usage line. + +- Oracle proved: all four POSIX metadata payloads remain semantically complete after the PE-version + change; direct audit agrees with workflow equality; both target-native Windows architectures + reject an empty PE-version result before unverified/incompletely described bytes can be built or + uploaded. +- Does not prove: a working native Windows PE-version lookup, Windows build/metadata/equality/upload, + regenerated Windows content IDs for the 19045/26100 floors, oldest baselines, native trust/signing, + SSH, publication, or an enabled tuple. +- Checklist items satisfied: no new completion box; the all-six SBOM/provenance/toolchain item + remains in progress. +- Follow-up: transport the resolved linker path outside PowerShell argument parsing, retain exact + linker-byte hashing and bounded output validation, then rerun and directly audit all six cells. + +### E-M3-WINDOWS-LINKER-ENV-LOCAL-001 — Environment-isolated linker path transport + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `714308114dee85a6f241a43b75c6ac9122769c3e`; stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: local macOS 26.2 build 25C56, Darwin 25.2.0 arm64 on Apple Silicon; Node v26.0.0 and + pnpm 10.24.0 for pure invocation/metadata tests +- Remote and transport: none; artifact-only local contracts +- Exact evidence commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-*.test.mjs \ + src/main/ssh/ssh-relay-artifact-selector.test.ts + pnpm run typecheck + pnpm run lint + pnpm run check:max-lines-ratchet + pnpm exec oxfmt --check \ + config/scripts/ssh-relay-runtime-toolchain.mjs \ + config/scripts/ssh-relay-runtime-toolchain.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. Twenty focused test files passed 98 tests in 5.90s. Typecheck passed all three + TypeScript projects. Full lint passed oxlint, switch exhaustiveness, styled-scrollbar, + reliability-gate, max-lines, bundled-skill, localization-catalog, and localization-coverage gates; + its diagnostics remain pre-existing unrelated warnings. The independent max-lines ratchet, + focused formatting, and diff checks passed. +- Oracle proved: the PowerShell expression no longer relies on `$args` parsing and never + interpolates the linker path into executable script text; the exact resolved path is supplied in + a dedicated child-only environment value; the child inherits the required native build + environment; three- and four-component PE file versions with bounded release suffixes are + accepted; missing, malformed, oversized, or unrelated output remains rejected with at most 512 + diagnostic bytes; and the resolved linker executable remains SHA-256 authenticated. +- Does not prove: the environment-value lookup on either native Windows architecture, the actual + linker version, regenerated Windows metadata/content IDs, build/smoke/equality/upload, oldest + Windows execution, native trust/signing, SSH, publication, or an enabled tuple. +- Checklist items satisfied: local correction only; the all-six compiler/toolchain item remains + unchecked. +- Follow-up: push this exact implementation and ledger head, then require all six native jobs and + direct inspection of every uploaded artifact before closing any metadata/provenance claim. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -6437,12 +6589,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push exact correction commit `e85f0c700` plus this evidence-ledger head and rerun all six -target-native cells. Require exact SPDX ownership, archive-scoped document identity, commit-pinned -builder identity, resolved runner identity, complete bounded tool records with SHA-256, clean-build -metadata equality, smoke, upload, and regenerated Windows content identities before checking the -remaining Milestone 3 metadata/toolchain claims. Then proceed to oldest-baseline and native-trust -proof. +Finish the direct artifact audit and evidence entry for exact-head run 29369925932, record local +proof for correction commit `714308114`, then push that exact correction plus this ledger head and +rerun all six target-native cells. Require exact SPDX ownership, archive-scoped document identity, +commit-pinned builder identity, resolved runner identity, complete bounded tool records with +SHA-256, clean-build metadata equality, smoke, upload, and regenerated Windows content identities +before checking the remaining Milestone 3 metadata/toolchain claims. Then proceed to oldest-baseline +and native-trust proof. 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 From 1b775e6afe730c237f49b2d203591e07db11bfd8 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 17:58:42 -0400 Subject: [PATCH 065/267] build(ssh): read numeric linker PE version Co-authored-by: Orca --- config/scripts/ssh-relay-runtime-toolchain.mjs | 9 ++++++--- .../ssh-relay-runtime-toolchain.test.mjs | 17 +++++++++++++---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/config/scripts/ssh-relay-runtime-toolchain.mjs b/config/scripts/ssh-relay-runtime-toolchain.mjs index 1fbd5162a65..3045f7e0446 100644 --- a/config/scripts/ssh-relay-runtime-toolchain.mjs +++ b/config/scripts/ssh-relay-runtime-toolchain.mjs @@ -188,6 +188,8 @@ export function sshRelayRuntimeStripVersionProbe(platform) { } export function sshRelayRuntimeWindowsFileVersionInvocation(path) { + const versionInfo = + "[System.Diagnostics.FileVersionInfo]::GetVersionInfo([Environment]::GetEnvironmentVariable('ORCA_SSH_RELAY_TOOL_PATH'))" return { command: 'pwsh.exe', args: [ @@ -195,7 +197,8 @@ export function sshRelayRuntimeWindowsFileVersionInvocation(path) { '-NoProfile', '-NonInteractive', '-Command', - "[System.Diagnostics.FileVersionInfo]::GetVersionInfo([Environment]::GetEnvironmentVariable('ORCA_SSH_RELAY_TOOL_PATH')).FileVersion" + // Why: hosted link.exe returns an empty FileVersion string; its numeric PE fields are stable. + `$versionInfo = ${versionInfo}; '{0}.{1}.{2}.{3}' -f $versionInfo.FileMajorPart, $versionInfo.FileMinorPart, $versionInfo.FileBuildPart, $versionInfo.FilePrivatePart` ], options: { env: { ORCA_SSH_RELAY_TOOL_PATH: path } } } @@ -245,9 +248,9 @@ export async function collectSshRelayRuntimeToolchain(nodePath) { linker: await executableRecord({ command: 'link.exe', args: [], - // Why: hosted link.exe is pipe-silent; authenticated PE metadata gives its real version. + // Why: hosted link.exe is pipe-silent; authenticated numeric PE metadata gives its version. windowsFileVersion: true, - versionPattern: /^\d+(?:\.\d+){2,3}(?:\s.*)?$/ + versionPattern: /^(?!0\.0\.0\.0$)\d+(?:\.\d+){3}$/ }), buildSystem: await executableRecord({ command: 'msbuild.exe', diff --git a/config/scripts/ssh-relay-runtime-toolchain.test.mjs b/config/scripts/ssh-relay-runtime-toolchain.test.mjs index 9903908420c..b175b17b453 100644 --- a/config/scripts/ssh-relay-runtime-toolchain.test.mjs +++ b/config/scripts/ssh-relay-runtime-toolchain.test.mjs @@ -31,10 +31,19 @@ describe('SSH relay runtime build provenance', () => { it('selects a bounded Windows linker file version', () => { expect( selectSshRelayRuntimeToolVersion( - { stdout: '14.44.35228.0 built by release pipeline\r\n' }, - /^\d+(?:\.\d+){2,3}(?:\s.*)?$/ + { stdout: '14.44.35228.0\r\n' }, + /^(?!0\.0\.0\.0$)\d+(?:\.\d+){3}$/ ) - ).toBe('14.44.35228.0 built by release pipeline') + ).toBe('14.44.35228.0') + }) + + it('rejects an absent Windows linker numeric file version', () => { + expect(() => + selectSshRelayRuntimeToolVersion( + { stdout: '0.0.0.0\r\n' }, + /^(?!0\.0\.0\.0$)\d+(?:\.\d+){3}$/ + ) + ).toThrow(/0\.0\.0\.0/) }) it('passes the resolved linker path through a non-interpolated environment value', () => { @@ -46,7 +55,7 @@ describe('SSH relay runtime build provenance', () => { '-NoProfile', '-NonInteractive', '-Command', - "[System.Diagnostics.FileVersionInfo]::GetVersionInfo([Environment]::GetEnvironmentVariable('ORCA_SSH_RELAY_TOOL_PATH')).FileVersion" + "$versionInfo = [System.Diagnostics.FileVersionInfo]::GetVersionInfo([Environment]::GetEnvironmentVariable('ORCA_SSH_RELAY_TOOL_PATH')); '{0}.{1}.{2}.{3}' -f $versionInfo.FileMajorPart, $versionInfo.FileMinorPart, $versionInfo.FileBuildPart, $versionInfo.FilePrivatePart" ], options: { env: { ORCA_SSH_RELAY_TOOL_PATH: path } } }) From 3c3d47fc4bf06610d43dcdb267eadfed46627b0f Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 18:00:31 -0400 Subject: [PATCH 066/267] docs(ssh): record empty native PE versions Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 11 +- ...github-release-implementation-checklist.md | 130 ++++++++++++++++-- 2 files changed, 122 insertions(+), 19 deletions(-) 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 8fbc377e255..876247d59a2 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 @@ -21,11 +21,12 @@ complete a box. ## Active work - [ ] **WP2 — Finish target-native artifact metadata and provenance.** - - Four POSIX GitHub runner cells pass build, smoke, equality, and metadata upload. - - Windows x64/arm64 currently fail closed while reading the linker PE file version in - [run 29369925932](https://github.com/stablyai/orca/actions/runs/29369925932). - - Local commit `714308114` isolates the linker path through a child environment variable and is - green across 20 test files / 98 tests plus typecheck, lint, formatting, and diff checks. + - Four POSIX GitHub runner cells pass build, smoke, equality, metadata upload, and direct payload + audit in [run 29370926985](https://github.com/stablyai/orca/actions/runs/29370926985). + - Windows x64/arm64 fail closed because hosted `link.exe` returns an empty string-valued PE + `FileVersion`; neither cell downloads inputs or produces an artifact. + - Local commit `1b775e6af` formats the PE version from bounded numeric fields and rejects an absent + all-zero version. It is green across 20 test files / 99 tests plus static gates. - Next gate: push that exact correction, rerun all six native jobs, then inspect every uploaded artifact before completing the metadata/provenance box. 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 49003dc7734..2c599d53538 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 SBOM/provenance closure — **In progress — 2026-07-14, Codex implementation owner**; exact-head run [29369925932](https://github.com/stablyai/orca/actions/runs/29369925932) and direct payload audit keep Linux x64/arm64 and macOS x64/arm64 green but both Windows cells fail closed before build/upload while selecting the authenticated linker's PE file version (E-M3-METADATA-CI-RED-003); local commit `714308114` passes that linker path through a dedicated child environment variable, accepts only a bounded valid file-version form, preserves bounded failure diagnostics, and passes 20 files / 98 tests plus typecheck, full lint, formatting, max-lines, and diff gates (E-M3-WINDOWS-LINKER-ENV-LOCAL-001); the correction still requires an exact-head all-six native run and direct artifact inspection before any remaining SBOM/provenance/toolchain box is checked; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 SBOM/provenance closure — **In progress — 2026-07-14, Codex implementation owner**; exact-head run [29370926985](https://github.com/stablyai/orca/actions/runs/29370926985) and direct payload audit keep Linux x64/arm64 and macOS x64/arm64 green but both Windows cells fail closed before build/upload because the authenticated linker's string-valued PE `FileVersion` is empty on both hosted architectures (E-M3-METADATA-CI-RED-004); exact implementation commit `1b775e6af` formats the four numeric PE version fields, rejects the absent `0.0.0.0` form, preserves exact linker-byte hashing and environment-isolated path transport, and passes 20 files / 99 tests plus typecheck, full lint, formatting, max-lines, and diff gates (E-M3-WINDOWS-LINKER-NUMERIC-LOCAL-001); the correction still requires an exact-head all-six native run and direct artifact inspection before any remaining SBOM/provenance/toolchain box is checked; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      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) @@ -175,12 +175,12 @@ 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: finish the direct audit and ledger entry for run 29369925932, record local - proof for commit `714308114`, then push that exact correction plus the ledger head and rerun all - six target-native cells. Require exact SPDX ownership, archive-scoped document identity, - commit-pinned builder identity, resolved runner identity, complete bounded tool records, - regenerated Windows content identities, smoke, exact clean-build equality, and upload. Preserve - the artifact-only boundary, legacy/default path, and every other release gate. +- Next required action: push exact numeric PE-version correction commit `1b775e6af` plus its evidence + ledger head and rerun all six target-native cells. Require a nonzero four-component linker version, + exact SPDX ownership, archive-scoped document identity, commit-pinned builder identity, resolved + runner identity, complete bounded tool records, regenerated Windows content identities, smoke, + exact clean-build equality, and upload. Preserve the artifact-only boundary, legacy/default path, + and every other release gate. ## Non-Negotiable Invariants @@ -6532,6 +6532,109 @@ tool did not report a bounded version line`; input download, build, smoke, compa - Follow-up: push this exact implementation and ledger head, then require all six native jobs and direct inspection of every uploaded artifact before closing any metadata/provenance claim. +### E-M3-METADATA-CI-RED-004 — Native PE FileVersion strings are empty + +- Date: 2026-07-14 +- Commit SHA / PR: exact tested head `756165552d31ee962c99a3a454bce2bebb29092b`; + stacked draft PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: [GitHub Actions run 29370926985](https://github.com/stablyai/orca/actions/runs/29370926985), + final conclusion `failure` after 7m09s. Native job IDs and wall durations: Linux x64 + `87213791478` / 3m13s, Linux arm64 `87213791425` / 3m34s, macOS x64 `87213791432` / + 7m04s, macOS arm64 `87213791418` / 4m32s, Windows x64 `87213791403` / 2m28s, and + Windows arm64 `87213791472` / 3m52s. +- Remote and transport: none; target-native artifact-only build workflow +- Exact evidence commands: + + ```sh + gh run view 29370926985 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh api repos/stablyai/orca/actions/runs/29370926985/artifacts + gh api repos/stablyai/orca/actions/jobs/87213791403/logs + gh api repos/stablyai/orca/actions/jobs/87213791472/logs + gh run download 29370926985 --repo stablyai/orca \ + --dir /tmp/orca-8450-metadata-red-29370926985 + # Repeat the exact fail-closed jq/shasum audit in E-M3-METADATA-CI-RED-003 with: + # evidence=/tmp/orca-8450-metadata-red-29370926985 + # commit=756165552d31ee962c99a3a454bce2bebb29092b + # run_id=29370926985 + ``` + +- Result: EXPECTED RED. All four POSIX cells passed contracts, two clean native builds, archive/tree + inspection, bundled runtime smoke, byte-for-byte comparison, and upload. Both Windows cells passed + MSVC setup, runner-identity collection, dependency install, and 77 contract tests, then the live + toolchain contract failed with the now-bounded diagnostic `Runtime build tool did not report a +bounded version line: `. The PE lookup returned successfully but the `FileVersion` string + emitted no stdout on either architecture. Input download, build, smoke, comparison, and upload + remained skipped; no Windows artifact exists. +- Downloaded-artifact audit: + + | Tuple | Artifact ID | Archive SHA-256 | Content ID prefix | Files | + | ----------------- | ------------ | ------------------------------------------------------------------ | ----------------- | ----- | + | linux-x64-glibc | `8326078824` | `3fbd5895c66a81cef6e97c0fcc1efd210377fee6fdbb5bc56548a36377d373e5` | `960546cd…` | 34 | + | linux-arm64-glibc | `8326088493` | `2f1ae08f794d9a8154b0e0714d394e8da447cff7958febc904d3f68cae493a68` | `aa3aa8ae…` | 34 | + | darwin-x64 | `8326164493` | `b84c3d8a7c8e0c195d82185d0476f77685d171dde13ea5b5652b1ce12917564a` | `585ea603…` | 35 | + | darwin-arm64 | `8326107962` | `d940054bb24d79fe31b5eaad6269fcc6bbaef6ca2c1e4703b398b631dbe85081` | `40ff5d20…` | 35 | + + All archive/subject hashes, archive-scoped SPDX namespaces, one-owner-per-file counts, eight + dependency relationships, prohibited-content assertions, exact commit/run identities, runner + labels/images/architectures, and bounded tool version/hash records passed. Content IDs and native + tool versions remain identical to E-M3-METADATA-CI-RED-003; builder/source identities now pin + exact head `756165552` and invocation `29370926985`. + +- Oracle proved: environment-isolated path transport does not fix the semantic defect because the + string `FileVersion` property itself is empty on both native hosted Windows environments; the + bounded diagnostic distinguishes that from command failure or unexpected output; all four POSIX + controls remain complete and reproducible at the exact tested head. +- Does not prove: that numeric PE version fields are populated, any Windows artifact or metadata, + regenerated Windows content IDs, oldest baselines, native trust/signing, SSH, publication, or an + enabled tuple. +- Checklist items satisfied: no new completion box; the all-six SBOM/provenance/toolchain item + remains in progress. +- Follow-up: format the four integer PE version fields, reject `0.0.0.0`, retain exact linker-byte + hashing and environment-isolated path transport, then rerun and directly audit all six cells. + +### E-M3-WINDOWS-LINKER-NUMERIC-LOCAL-001 — Bounded numeric PE file version + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `1b775e6afe730c237f49b2d203591e07db11bfd8`; stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: local macOS 26.2 build 25C56, Darwin 25.2.0 arm64 on Apple Silicon; Node v26.0.0 and + pnpm 10.24.0 for pure invocation/metadata tests +- Remote and transport: none; artifact-only local contracts +- Exact evidence commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-*.test.mjs \ + src/main/ssh/ssh-relay-artifact-selector.test.ts + pnpm run typecheck + pnpm run lint + pnpm run check:max-lines-ratchet + pnpm exec oxfmt --check \ + config/scripts/ssh-relay-runtime-toolchain.mjs \ + config/scripts/ssh-relay-runtime-toolchain.test.mjs + git diff --check + ``` + +- Result: PASS. Twenty focused test files passed 99 tests in 1.85s. Typecheck passed all three + TypeScript projects. Full lint passed oxlint, switch exhaustiveness, styled-scrollbar, + reliability-gate, max-lines, bundled-skill, localization-catalog, and localization-coverage gates; + its diagnostics remain pre-existing unrelated warnings. The independent max-lines ratchet, + focused formatting, and diff checks passed. +- Oracle proved: the PowerShell expression formats `FileMajorPart`, `FileMinorPart`, `FileBuildPart`, + and `FilePrivatePart` as one exact four-component value; the selector rejects malformed versions, + suffixes, missing output, and the version-resource-absent `0.0.0.0` form; the resolved linker path + remains a child-only environment value and never enters executable script text; the exact resolved + `link.exe` bytes remain SHA-256 authenticated. +- Does not prove: that either native Windows linker exposes nonzero numeric fields, the actual native + version, Windows build/smoke/equality/upload, regenerated Windows content IDs, oldest Windows + execution, native trust/signing, SSH, publication, or an enabled tuple. +- Checklist items satisfied: local correction only; the all-six compiler/toolchain item remains + unchecked. +- Follow-up: push this exact implementation and ledger head, then require all six native jobs and + direct inspection of every uploaded artifact before closing any metadata/provenance claim. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -6589,13 +6692,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Finish the direct artifact audit and evidence entry for exact-head run 29369925932, record local -proof for correction commit `714308114`, then push that exact correction plus this ledger head and -rerun all six target-native cells. Require exact SPDX ownership, archive-scoped document identity, -commit-pinned builder identity, resolved runner identity, complete bounded tool records with -SHA-256, clean-build metadata equality, smoke, upload, and regenerated Windows content identities -before checking the remaining Milestone 3 metadata/toolchain claims. Then proceed to oldest-baseline -and native-trust proof. +Push exact numeric PE-version correction commit `1b775e6af` plus this evidence-ledger head and rerun +all six target-native cells. Require a nonzero four-component linker version, exact SPDX ownership, +archive-scoped document identity, commit-pinned builder identity, resolved runner identity, complete +bounded tool records with SHA-256, clean-build metadata equality, smoke, upload, and regenerated +Windows content identities before checking the remaining Milestone 3 metadata/toolchain claims. +Then proceed to oldest-baseline and native-trust proof. 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 From 18d10da2750af66ae3486b61382ab90d5c410f8a Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 18:09:32 -0400 Subject: [PATCH 067/267] build(ssh): identify linker by MSVC toolset Co-authored-by: Orca --- .../scripts/ssh-relay-runtime-toolchain.mjs | 51 ++++++++++--------- .../ssh-relay-runtime-toolchain.test.mjs | 45 +++++++--------- 2 files changed, 44 insertions(+), 52 deletions(-) diff --git a/config/scripts/ssh-relay-runtime-toolchain.mjs b/config/scripts/ssh-relay-runtime-toolchain.mjs index 3045f7e0446..c2f24ca8d30 100644 --- a/config/scripts/ssh-relay-runtime-toolchain.mjs +++ b/config/scripts/ssh-relay-runtime-toolchain.mjs @@ -3,7 +3,7 @@ import { createHash } from 'node:crypto' import { createReadStream } from 'node:fs' import { readdir, realpath, stat } from 'node:fs/promises' import { createRequire } from 'node:module' -import { dirname, isAbsolute, join, relative } from 'node:path' +import { dirname, isAbsolute, join, relative, win32 } from 'node:path' import { isDeepStrictEqual, promisify } from 'node:util' const require = createRequire(import.meta.url) @@ -26,11 +26,10 @@ async function sha256File(path) { return `sha256:${hash.digest('hex')}` } -async function capture(command, args, { env } = {}) { +async function capture(command, args) { try { return await execFileAsync(command, args, { encoding: 'utf8', - ...(env ? { env: { ...process.env, ...env } } : {}), maxBuffer: TOOL_OUTPUT_BYTES, timeout: TOOL_TIMEOUT_MS, windowsHide: true @@ -82,14 +81,13 @@ async function executableRecord({ versionCommand, versionArgs, versionPattern, - windowsFileVersion = false, + windowsMsvcToolsetVersion = false, xcrun = false }) { const path = await locateExecutable(command, { xcrun }) - const invocation = windowsFileVersion - ? sshRelayRuntimeWindowsFileVersionInvocation(path) - : { command: versionCommand ?? path, args: versionArgs ?? args } - const result = await capture(invocation.command, invocation.args, invocation.options) + const result = windowsMsvcToolsetVersion + ? { stdout: sshRelayRuntimeWindowsMsvcToolsetVersion(path) } + : await capture(versionCommand ?? path, versionArgs ?? args) return { version: selectSshRelayRuntimeToolVersion(result, versionPattern), sha256: await sha256File(path) @@ -187,21 +185,25 @@ export function sshRelayRuntimeStripVersionProbe(platform) { : { args: ['--version'] } } -export function sshRelayRuntimeWindowsFileVersionInvocation(path) { - const versionInfo = - "[System.Diagnostics.FileVersionInfo]::GetVersionInfo([Environment]::GetEnvironmentVariable('ORCA_SSH_RELAY_TOOL_PATH'))" - return { - command: 'pwsh.exe', - args: [ - '-NoLogo', - '-NoProfile', - '-NonInteractive', - '-Command', - // Why: hosted link.exe returns an empty FileVersion string; its numeric PE fields are stable. - `$versionInfo = ${versionInfo}; '{0}.{1}.{2}.{3}' -f $versionInfo.FileMajorPart, $versionInfo.FileMinorPart, $versionInfo.FileBuildPart, $versionInfo.FilePrivatePart` - ], - options: { env: { ORCA_SSH_RELAY_TOOL_PATH: path } } +export function sshRelayRuntimeWindowsMsvcToolsetVersion(path) { + const normalized = win32.normalize(path) + const segments = normalized.split(win32.sep) + const msvcIndexes = segments.flatMap((segment, index) => + segment.toLowerCase() === 'msvc' ? [index] : [] + ) + const msvcIndex = msvcIndexes[0] + const version = segments[msvcIndex + 1] + if ( + !win32.isAbsolute(normalized) || + msvcIndexes.length !== 1 || + !/^\d+\.\d+\.\d+$/.test(version ?? '') || + segments[msvcIndex + 2]?.toLowerCase() !== 'bin' || + segments.at(-1)?.toLowerCase() !== 'link.exe' + ) { + throw new Error('Resolved Windows linker is not in a bounded MSVC toolset path') } + // Why: hosted link.exe has no version resource; its resolved toolset path plus byte hash is exact. + return `MSVC ${version}` } export function assertSshRelayRuntimeToolchain(toolchain, tuple) { @@ -248,9 +250,8 @@ export async function collectSshRelayRuntimeToolchain(nodePath) { linker: await executableRecord({ command: 'link.exe', args: [], - // Why: hosted link.exe is pipe-silent; authenticated numeric PE metadata gives its version. - windowsFileVersion: true, - versionPattern: /^(?!0\.0\.0\.0$)\d+(?:\.\d+){3}$/ + windowsMsvcToolsetVersion: true, + versionPattern: /^MSVC \d+\.\d+\.\d+$/ }), buildSystem: await executableRecord({ command: 'msbuild.exe', diff --git a/config/scripts/ssh-relay-runtime-toolchain.test.mjs b/config/scripts/ssh-relay-runtime-toolchain.test.mjs index b175b17b453..655a75e9615 100644 --- a/config/scripts/ssh-relay-runtime-toolchain.test.mjs +++ b/config/scripts/ssh-relay-runtime-toolchain.test.mjs @@ -7,7 +7,7 @@ import { sshRelayRuntimeBuilderIdentity, sshRelayRuntimeRunnerIdentity, sshRelayRuntimeStripVersionProbe, - sshRelayRuntimeWindowsFileVersionInvocation + sshRelayRuntimeWindowsMsvcToolsetVersion } from './ssh-relay-runtime-toolchain.mjs' const commit = 'a'.repeat(40) @@ -28,37 +28,28 @@ describe('SSH relay runtime build provenance', () => { ).toBe('Microsoft (R) C/C++ Optimizing Compiler Version 19.44.35228 for ARM64') }) - it('selects a bounded Windows linker file version', () => { + it('selects a bounded Windows linker toolset version', () => { expect( - selectSshRelayRuntimeToolVersion( - { stdout: '14.44.35228.0\r\n' }, - /^(?!0\.0\.0\.0$)\d+(?:\.\d+){3}$/ - ) - ).toBe('14.44.35228.0') + selectSshRelayRuntimeToolVersion({ stdout: 'MSVC 14.44.35207\r\n' }, /^MSVC \d+\.\d+\.\d+$/) + ).toBe('MSVC 14.44.35207') }) - it('rejects an absent Windows linker numeric file version', () => { + it('derives the version only from a canonical resolved MSVC linker path', () => { + const path = String.raw`C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\bin\HostX64\x64\link.exe` + expect(sshRelayRuntimeWindowsMsvcToolsetVersion(path)).toBe('MSVC 14.44.35207') + expect( + sshRelayRuntimeWindowsMsvcToolsetVersion( + String.raw`C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\bin\HostARM64\ARM64\link.exe` + ) + ).toBe('MSVC 14.44.35207') expect(() => - selectSshRelayRuntimeToolVersion( - { stdout: '0.0.0.0\r\n' }, - /^(?!0\.0\.0\.0$)\d+(?:\.\d+){3}$/ + sshRelayRuntimeWindowsMsvcToolsetVersion(String.raw`C:\tools\14.44.35207\link.exe`) + ).toThrow(/MSVC toolset path/) + expect(() => + sshRelayRuntimeWindowsMsvcToolsetVersion( + String.raw`C:\MSVC\14.44.35207\bin\shadow\MSVC\14.44.1\bin\link.exe` ) - ).toThrow(/0\.0\.0\.0/) - }) - - it('passes the resolved linker path through a non-interpolated environment value', () => { - const path = String.raw`C:\Program Files\Microsoft Visual Studio\link.exe` - expect(sshRelayRuntimeWindowsFileVersionInvocation(path)).toEqual({ - command: 'pwsh.exe', - args: [ - '-NoLogo', - '-NoProfile', - '-NonInteractive', - '-Command', - "$versionInfo = [System.Diagnostics.FileVersionInfo]::GetVersionInfo([Environment]::GetEnvironmentVariable('ORCA_SSH_RELAY_TOOL_PATH')); '{0}.{1}.{2}.{3}' -f $versionInfo.FileMajorPart, $versionInfo.FileMinorPart, $versionInfo.FileBuildPart, $versionInfo.FilePrivatePart" - ], - options: { env: { ORCA_SSH_RELAY_TOOL_PATH: path } } - }) + ).toThrow(/MSVC toolset path/) }) it('bounds failed version diagnostics', () => { From 2ed6c8e3b3609a9cb4785011996988de915e807a Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 18:11:01 -0400 Subject: [PATCH 068/267] docs(ssh): record absent linker version resource Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 10 +- ...github-release-implementation-checklist.md | 129 ++++++++++++++++-- 2 files changed, 121 insertions(+), 18 deletions(-) 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 876247d59a2..3ef82302252 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 @@ -22,11 +22,11 @@ complete a box. - [ ] **WP2 — Finish target-native artifact metadata and provenance.** - Four POSIX GitHub runner cells pass build, smoke, equality, metadata upload, and direct payload - audit in [run 29370926985](https://github.com/stablyai/orca/actions/runs/29370926985). - - Windows x64/arm64 fail closed because hosted `link.exe` returns an empty string-valued PE - `FileVersion`; neither cell downloads inputs or produces an artifact. - - Local commit `1b775e6af` formats the PE version from bounded numeric fields and rejects an absent - all-zero version. It is green across 20 test files / 99 tests plus static gates. + audit in [run 29371551072](https://github.com/stablyai/orca/actions/runs/29371551072). + - Windows x64/arm64 prove hosted `link.exe` has no usable PE version resource (`0.0.0.0`); neither + cell downloads inputs or produces an artifact. + - Local commit `18d10da27` records the bounded MSVC toolset directory from the resolved linker path + plus the exact linker SHA-256. It is green across 20 test files / 98 tests plus static gates. - Next gate: push that exact correction, rerun all six native jobs, then inspect every uploaded artifact before completing the metadata/provenance box. 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 2c599d53538..c509cbe5556 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 SBOM/provenance closure — **In progress — 2026-07-14, Codex implementation owner**; exact-head run [29370926985](https://github.com/stablyai/orca/actions/runs/29370926985) and direct payload audit keep Linux x64/arm64 and macOS x64/arm64 green but both Windows cells fail closed before build/upload because the authenticated linker's string-valued PE `FileVersion` is empty on both hosted architectures (E-M3-METADATA-CI-RED-004); exact implementation commit `1b775e6af` formats the four numeric PE version fields, rejects the absent `0.0.0.0` form, preserves exact linker-byte hashing and environment-isolated path transport, and passes 20 files / 99 tests plus typecheck, full lint, formatting, max-lines, and diff gates (E-M3-WINDOWS-LINKER-NUMERIC-LOCAL-001); the correction still requires an exact-head all-six native run and direct artifact inspection before any remaining SBOM/provenance/toolchain box is checked; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 SBOM/provenance closure — **In progress — 2026-07-14, Codex implementation owner**; exact-head run [29371551072](https://github.com/stablyai/orca/actions/runs/29371551072) and direct payload audit keep Linux x64/arm64 and macOS x64/arm64 green but both Windows cells fail closed before build/upload because the authenticated linker has no usable PE version resource and returns `0.0.0.0` on both hosted architectures (E-M3-METADATA-CI-RED-005); exact implementation commit `18d10da27` derives a bounded toolset identity from the canonical resolved `.../MSVC//bin/.../link.exe` path while retaining the exact linker SHA-256, rejects malformed/ambiguous/non-MSVC paths, and passes 20 files / 98 tests plus typecheck, full lint, formatting, max-lines, and diff gates (E-M3-WINDOWS-LINKER-TOOLSET-LOCAL-001); the correction still requires an exact-head all-six native run and direct artifact inspection before any remaining SBOM/provenance/toolchain box is checked; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      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) @@ -175,12 +175,12 @@ 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: push exact numeric PE-version correction commit `1b775e6af` plus its evidence - ledger head and rerun all six target-native cells. Require a nonzero four-component linker version, - exact SPDX ownership, archive-scoped document identity, commit-pinned builder identity, resolved - runner identity, complete bounded tool records, regenerated Windows content identities, smoke, - exact clean-build equality, and upload. Preserve the artifact-only boundary, legacy/default path, - and every other release gate. +- Next required action: push exact MSVC-toolset linker identity commit `18d10da27` plus its evidence + ledger head and rerun all six target-native cells. Require the expected bounded toolset identity and + exact linker SHA-256, exact SPDX ownership, archive-scoped document identity, commit-pinned builder + identity, resolved runner identity, complete bounded tool records, regenerated Windows content + identities, smoke, exact clean-build equality, and upload. Preserve the artifact-only boundary, + legacy/default path, and every other release gate. ## Non-Negotiable Invariants @@ -6635,6 +6635,109 @@ bounded version line: `. The PE lookup returned successfully but the `Fil - Follow-up: push this exact implementation and ledger head, then require all six native jobs and direct inspection of every uploaded artifact before closing any metadata/provenance claim. +### E-M3-METADATA-CI-RED-005 — Native linkers have no PE version resource + +- Date: 2026-07-14 +- Commit SHA / PR: exact tested head `3c3d47fc4bf06610d43dcdb267eadfed46627b0f`; + stacked draft PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: [GitHub Actions run 29371551072](https://github.com/stablyai/orca/actions/runs/29371551072), + final conclusion `failure` after 7m31s. Native job IDs and wall durations: Linux x64 + `87215769010` / 3m09s, Linux arm64 `87215769039` / 3m16s, macOS x64 `87215769046` / + 7m20s, macOS arm64 `87215769029` / 2m37s, Windows x64 `87215768998` / 1m29s, and + Windows arm64 `87215769056` / 3m55s. +- Remote and transport: none; target-native artifact-only build workflow +- Exact evidence commands: + + ```sh + gh run view 29371551072 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh api repos/stablyai/orca/actions/runs/29371551072/artifacts + gh api repos/stablyai/orca/actions/jobs/87215768998/logs + gh api repos/stablyai/orca/actions/jobs/87215769056/logs + gh run download 29371551072 --repo stablyai/orca \ + --dir /tmp/orca-8450-metadata-red-29371551072 + # Repeat the exact fail-closed jq/shasum audit in E-M3-METADATA-CI-RED-003 with: + # evidence=/tmp/orca-8450-metadata-red-29371551072 + # commit=3c3d47fc4bf06610d43dcdb267eadfed46627b0f + # run_id=29371551072 + ``` + +- Result: EXPECTED RED. All four POSIX cells again passed contracts, two clean native builds, + archive/tree inspection, bundled runtime smoke, byte-for-byte comparison, and upload. Windows x64 + and arm64 each passed 78 contract tests after native MSVC setup, then rejected the exact diagnostic + `Runtime build tool did not report a bounded version line: 0.0.0.0`. This proves both resolved + `link.exe` binaries lack a usable PE version resource rather than merely an empty formatted string. + Input download, build, smoke, comparison, and upload remained skipped; no Windows artifact exists. +- Downloaded-artifact audit: + + | Tuple | Artifact ID | Archive SHA-256 | Content ID prefix | Files | + | ----------------- | ------------ | ------------------------------------------------------------------ | ----------------- | ----- | + | linux-x64-glibc | `8326313178` | `fb30c981c7d8ef32a57485cad75850058b6bb38988c5fe34a269d6e0bd33eb06` | `960546cd…` | 34 | + | linux-arm64-glibc | `8326316547` | `0d70e0646540461676fe4eeb8db38c13d13e4bd747db5609dec90a957c4db79d` | `aa3aa8ae…` | 34 | + | darwin-x64 | `8326400647` | `fe4a79d356baf0523822e4524989df4c3b3153e36e777fc44a09b96d2926df68` | `585ea603…` | 35 | + | darwin-arm64 | `8326301297` | `891489f352ae978aff91c227af908514df827ee7f4aba0513f5f0891a2b99ccd` | `40ff5d20…` | 35 | + + All archive/subject hashes, archive-scoped SPDX namespaces, one-owner-per-file counts, eight + dependency relationships, prohibited-content assertions, exact commit/run identities, runner + labels/images/architectures, and bounded tool version/hash records passed. POSIX content IDs and + native tool versions remain unchanged; builder/source identities pin exact head `3c3d47fc4` and + invocation `29371551072`. + +- Oracle proved: neither string nor integer PE version metadata can identify the hosted MSVC linker; + both architectures fail closed before producing incomplete provenance; four independent POSIX + controls remain complete and reproducible at the exact head. +- Does not prove: a bounded alternative linker identity on Windows, any Windows artifact/metadata, + regenerated Windows content IDs, oldest baselines, native trust/signing, SSH, publication, or an + enabled tuple. +- Checklist items satisfied: no new completion box; the all-six SBOM/provenance/toolchain item + remains in progress. +- Follow-up: derive the vendor toolset version from the canonical resolved MSVC linker directory, + reject malformed/ambiguous/non-MSVC layouts, retain the exact linker SHA-256, and rerun all cells. + +### E-M3-WINDOWS-LINKER-TOOLSET-LOCAL-001 — Resolved MSVC toolset identity + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `18d10da2750af66ae3486b61382ab90d5c410f8a`; stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: local macOS 26.2 build 25C56, Darwin 25.2.0 arm64 on Apple Silicon; Node v26.0.0 and + pnpm 10.24.0 for pure path/provenance tests +- Remote and transport: none; artifact-only local contracts +- Exact evidence commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-*.test.mjs \ + src/main/ssh/ssh-relay-artifact-selector.test.ts + pnpm run typecheck + pnpm run lint + pnpm run check:max-lines-ratchet + pnpm exec oxfmt --check \ + config/scripts/ssh-relay-runtime-toolchain.mjs \ + config/scripts/ssh-relay-runtime-toolchain.test.mjs + git diff --check + ``` + +- Result: PASS. Twenty focused test files passed 98 tests in 1.47s. Typecheck passed all three + TypeScript projects. Full lint passed oxlint, switch exhaustiveness, styled-scrollbar, + reliability-gate, max-lines, bundled-skill, localization-catalog, and localization-coverage gates; + its diagnostics remain pre-existing unrelated warnings. The independent max-lines ratchet, + focused formatting, and diff checks passed. +- Oracle proved: x64 and arm64 canonical Visual Studio paths produce the bounded identity + `MSVC 14.44.35207`; path parsing uses Windows semantics on every client OS; it requires one exact + case-insensitive `MSVC` segment, a three-component numeric version, the immediate `bin` segment, + an absolute path, and terminal `link.exe`; non-MSVC and ambiguous paths fail closed. The resolved + linker executable remains SHA-256 authenticated, so the path version is provenance metadata rather + than the byte-identity boundary. No PowerShell, PE metadata, stdout, or mutable version lookup is + required. +- Does not prove: the exact native hosted paths, a Windows toolchain record/build/smoke/equality/ + upload, regenerated Windows content IDs, oldest Windows execution, native trust/signing, SSH, + publication, or an enabled tuple. +- Checklist items satisfied: local correction only; the all-six compiler/toolchain item remains + unchecked. +- Follow-up: push this exact implementation and ledger head, then require all six native jobs and + direct inspection of every uploaded artifact before closing any metadata/provenance claim. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -6692,12 +6795,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push exact numeric PE-version correction commit `1b775e6af` plus this evidence-ledger head and rerun -all six target-native cells. Require a nonzero four-component linker version, exact SPDX ownership, -archive-scoped document identity, commit-pinned builder identity, resolved runner identity, complete -bounded tool records with SHA-256, clean-build metadata equality, smoke, upload, and regenerated -Windows content identities before checking the remaining Milestone 3 metadata/toolchain claims. -Then proceed to oldest-baseline and native-trust proof. +Push exact MSVC-toolset linker identity commit `18d10da27` plus this evidence-ledger head and rerun +all six target-native cells. Require the expected bounded toolset identity and exact linker SHA-256, +exact SPDX ownership, archive-scoped document identity, commit-pinned builder identity, resolved +runner identity, complete bounded tool records with SHA-256, clean-build metadata equality, smoke, +upload, and regenerated Windows content identities before checking the remaining Milestone 3 +metadata/toolchain claims. Then proceed to oldest-baseline and native-trust proof. 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 From d1eb45d613f504d7363230420f14a0c8126fe125 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 18:18:31 -0400 Subject: [PATCH 069/267] build(ssh): bound linker path diagnostic Co-authored-by: Orca --- config/scripts/ssh-relay-runtime-toolchain.mjs | 3 ++- config/scripts/ssh-relay-runtime-toolchain.test.mjs | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/config/scripts/ssh-relay-runtime-toolchain.mjs b/config/scripts/ssh-relay-runtime-toolchain.mjs index c2f24ca8d30..4d4cef54ca6 100644 --- a/config/scripts/ssh-relay-runtime-toolchain.mjs +++ b/config/scripts/ssh-relay-runtime-toolchain.mjs @@ -200,7 +200,8 @@ export function sshRelayRuntimeWindowsMsvcToolsetVersion(path) { segments[msvcIndex + 2]?.toLowerCase() !== 'bin' || segments.at(-1)?.toLowerCase() !== 'link.exe' ) { - throw new Error('Resolved Windows linker is not in a bounded MSVC toolset path') + const diagnostic = segments.slice(-12).join(win32.sep).slice(-512) || '' + throw new Error(`Resolved Windows linker is not in a bounded MSVC toolset path: ${diagnostic}`) } // Why: hosted link.exe has no version resource; its resolved toolset path plus byte hash is exact. return `MSVC ${version}` diff --git a/config/scripts/ssh-relay-runtime-toolchain.test.mjs b/config/scripts/ssh-relay-runtime-toolchain.test.mjs index 655a75e9615..4f172b5dd24 100644 --- a/config/scripts/ssh-relay-runtime-toolchain.test.mjs +++ b/config/scripts/ssh-relay-runtime-toolchain.test.mjs @@ -58,6 +58,13 @@ describe('SSH relay runtime build provenance', () => { ).toThrow(/^Runtime build tool did not report a bounded version line: unexpected x{490}/) }) + it('bounds rejected Windows linker path diagnostics', () => { + const path = String.raw`C:\${'unexpected\\'.repeat(100)}link.exe` + expect(() => sshRelayRuntimeWindowsMsvcToolsetVersion(path)).toThrow( + /^Resolved Windows linker is not in a bounded MSVC toolset path: .{1,512}$/ + ) + }) + it('pins GitHub builder identity to the exact source commit', () => { expect( sshRelayRuntimeBuilderIdentity({ From 92f9b76101d4214c07291f6d9befa647fbc0dd86 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 18:22:59 -0400 Subject: [PATCH 070/267] docs(ssh): record linker path diagnostic gate Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 14 +-- ...github-release-implementation-checklist.md | 119 ++++++++++++++++-- 2 files changed, 114 insertions(+), 19 deletions(-) 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 3ef82302252..f715ded686a 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 @@ -22,13 +22,13 @@ complete a box. - [ ] **WP2 — Finish target-native artifact metadata and provenance.** - Four POSIX GitHub runner cells pass build, smoke, equality, metadata upload, and direct payload - audit in [run 29371551072](https://github.com/stablyai/orca/actions/runs/29371551072). - - Windows x64/arm64 prove hosted `link.exe` has no usable PE version resource (`0.0.0.0`); neither - cell downloads inputs or produces an artifact. - - Local commit `18d10da27` records the bounded MSVC toolset directory from the resolved linker path - plus the exact linker SHA-256. It is green across 20 test files / 98 tests plus static gates. - - Next gate: push that exact correction, rerun all six native jobs, then inspect every uploaded - artifact before completing the metadata/provenance box. + audit in [run 29372156145](https://github.com/stablyai/orca/actions/runs/29372156145). + - Windows x64/arm64 fail closed because the resolved `link.exe` path differs from the assumed MSVC + layout; neither cell downloads inputs or produces an artifact. + - Local commit `d1eb45d61` adds a bounded path-tail diagnostic and is green across 20 test files / + 99 tests plus static gates. + - Next gate: push the diagnostic, inspect both native path shapes, then make only the evidenced + parser correction and rerun all six jobs. ## Work packages, in required order 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 c509cbe5556..4d3d5cd9497 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 SBOM/provenance closure — **In progress — 2026-07-14, Codex implementation owner**; exact-head run [29371551072](https://github.com/stablyai/orca/actions/runs/29371551072) and direct payload audit keep Linux x64/arm64 and macOS x64/arm64 green but both Windows cells fail closed before build/upload because the authenticated linker has no usable PE version resource and returns `0.0.0.0` on both hosted architectures (E-M3-METADATA-CI-RED-005); exact implementation commit `18d10da27` derives a bounded toolset identity from the canonical resolved `.../MSVC//bin/.../link.exe` path while retaining the exact linker SHA-256, rejects malformed/ambiguous/non-MSVC paths, and passes 20 files / 98 tests plus typecheck, full lint, formatting, max-lines, and diff gates (E-M3-WINDOWS-LINKER-TOOLSET-LOCAL-001); the correction still requires an exact-head all-six native run and direct artifact inspection before any remaining SBOM/provenance/toolchain box is checked; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 SBOM/provenance closure — **In progress — 2026-07-14, Codex implementation owner**; exact-head run [29372156145](https://github.com/stablyai/orca/actions/runs/29372156145) and direct payload audit keep Linux x64/arm64 and macOS x64/arm64 green but both Windows cells fail closed before build/upload because their resolved linker paths do not match the assumed MSVC toolset layout (E-M3-METADATA-CI-RED-006); exact implementation commit `d1eb45d61` retains that strict parser and exact linker SHA-256 while adding only a 512-byte bounded path-tail diagnostic, and passes 20 files / 99 tests plus typecheck, full lint, formatting, max-lines, and diff gates (E-M3-WINDOWS-LINKER-PATH-DIAGNOSTIC-LOCAL-001); the next native run must expose both real path shapes before any parser correction, and the eventual exact head must pass all six cells plus direct artifact inspection before any remaining SBOM/provenance/toolchain box is checked; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      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) @@ -175,11 +175,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: push exact MSVC-toolset linker identity commit `18d10da27` plus its evidence - ledger head and rerun all six target-native cells. Require the expected bounded toolset identity and - exact linker SHA-256, exact SPDX ownership, archive-scoped document identity, commit-pinned builder - identity, resolved runner identity, complete bounded tool records, regenerated Windows content - identities, smoke, exact clean-build equality, and upload. Preserve the artifact-only boundary, +- Next required action: push bounded linker-path diagnostic commit `d1eb45d61` plus its evidence + ledger head and rerun all six target-native cells. Inspect the bounded x64 and arm64 path tails, + then make only the parser correction supported by those native paths. The corrected exact head + must retain the exact linker SHA-256 and pass all metadata, smoke, equality, upload, and direct + payload audits before the all-six provenance item can close. Preserve the artifact-only boundary, legacy/default path, and every other release gate. ## Non-Negotiable Invariants @@ -6738,6 +6738,99 @@ bounded version line: `. The PE lookup returned successfully but the `Fil - Follow-up: push this exact implementation and ledger head, then require all six native jobs and direct inspection of every uploaded artifact before closing any metadata/provenance claim. +### E-M3-METADATA-CI-RED-006 — Hosted linker paths reject the assumed MSVC layout + +- Date: 2026-07-14 +- Commit SHA / PR: exact tested head `2ed6c8e3b3609a9cb4785011996988de915e807a`; + stacked draft PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: [GitHub Actions run 29372156145](https://github.com/stablyai/orca/actions/runs/29372156145), + final conclusion `failure` after 6m15s. Native job IDs and wall durations: Linux x64 + `87217674004` / 3m06s, Linux arm64 `87217674003` / 3m34s, macOS x64 `87217674012` / + 6m09s, macOS arm64 `87217674005` / 3m44s, Windows x64 `87217674050` / 1m29s, and + Windows arm64 `87217674002` / 3m52s. +- Remote and transport: none; target-native artifact-only build workflow +- Exact evidence commands: + + ```sh + gh run view 29372156145 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh api repos/stablyai/orca/actions/runs/29372156145/artifacts + gh api repos/stablyai/orca/actions/jobs/87217674050/logs + gh api repos/stablyai/orca/actions/jobs/87217674002/logs + gh run download 29372156145 --repo stablyai/orca \ + --dir /tmp/orca-8450-metadata-red-29372156145 + # Repeat the exact fail-closed jq/shasum audit in E-M3-METADATA-CI-RED-003 with: + # evidence=/tmp/orca-8450-metadata-red-29372156145 + # commit=2ed6c8e3b3609a9cb4785011996988de915e807a + # run_id=29372156145 + ``` + +- Result: EXPECTED RED. All four POSIX cells passed contracts, two clean native builds, + archive/tree inspection, bundled runtime smoke, byte-for-byte comparison, and upload. Windows x64 + and arm64 each rejected `Resolved Windows linker is not in a bounded MSVC toolset path` during + contract tests after native MSVC setup. Both stopped before input download, build, smoke, + comparison, or upload, so no Windows artifact exists. +- Downloaded-artifact audit: + + | Tuple | Artifact ID | Archive SHA-256 | Content ID prefix | Files | + | ----------------- | ------------ | ------------------------------------------------------------------ | ----------------- | ----- | + | linux-x64-glibc | `8326530355` | `ec09bf6a0068b3a5954af0fa17e538c9972a8389ccf420a525546fc7c63dfeb4` | `960546cd…` | 34 | + | linux-arm64-glibc | `8326541076` | `c13d1454e3e2592bf667a7c5925d147fd06e0f68eec7fb875dbc3750628f88b7` | `aa3aa8ae…` | 34 | + | darwin-x64 | `8326590799` | `5de3e4ae940a47839f8a7a2ec24bc7b63d54954357beccf7916ff7c72fab7ff3` | `585ea603…` | 35 | + | darwin-arm64 | `8326543567` | `8ee92c0d45d25b0fa05360c2dada50c21b2ac4ac837089a5c40c64d22ac2b9f5` | `40ff5d20…` | 35 | + + All archive/subject hashes, archive-scoped SPDX namespaces, one-owner-per-file counts, eight + dependency relationships, prohibited-content assertions, exact commit/run identities, runner + labels/images/architectures, and bounded tool version/hash records passed. + +- Oracle proved: the assumed strict MSVC directory layout does not match either hosted Windows + architecture; both jobs fail closed before consuming inputs; four independent POSIX controls + remain complete at the exact head. +- Does not prove: either actual resolved Windows path shape, a corrected bounded parser, any Windows + artifact/metadata, oldest baselines, native trust/signing, SSH, publication, or an enabled tuple. +- Checklist items satisfied: no new completion box; the all-six SBOM/provenance/toolchain item + remains in progress. +- Follow-up: expose only a bounded path-tail diagnostic, rerun both native Windows architectures, + and correct the parser only after inspecting those exact paths. + +### E-M3-WINDOWS-LINKER-PATH-DIAGNOSTIC-LOCAL-001 — Bounded native path evidence + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `d1eb45d613f504d7363230420f14a0c8126fe125`; stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: local macOS 26.2 build 25C56, Darwin 25.2.0 arm64 on Apple Silicon; Node v26.0.0 and + pnpm 10.24.0 for pure path/provenance tests +- Remote and transport: none; artifact-only local contracts +- Exact evidence commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-*.test.mjs \ + src/main/ssh/ssh-relay-artifact-selector.test.ts + pnpm run typecheck + pnpm run lint + pnpm run check:max-lines-ratchet + pnpm exec oxfmt --check \ + config/scripts/ssh-relay-runtime-toolchain.mjs \ + config/scripts/ssh-relay-runtime-toolchain.test.mjs + git diff --check + ``` + +- Result: PASS. Twenty focused test files passed 99 tests. Typecheck passed all three TypeScript + projects. Full lint passed with only existing unrelated warnings; the independent max-lines, + focused formatting, and diff checks passed. +- Oracle proved: linker-path rejection includes at most the final 12 path segments and at most 512 + characters; truncation and suffix retention are purpose-tested. The strict identity parser and + exact linker SHA-256 requirement remain unchanged. +- Does not prove: either native path shape, that 12 segments are sufficient to identify the correct + bounded grammar, a Windows build/smoke/equality/upload, oldest Windows execution, native trust, + SSH, publication, or an enabled tuple. +- Checklist items satisfied: local diagnostic only; the all-six compiler/toolchain item remains + unchecked. +- Follow-up: push this exact commit and ledger head, inspect both bounded native path tails, and make + only the parser correction supported by that evidence. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -6795,12 +6888,14 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push exact MSVC-toolset linker identity commit `18d10da27` plus this evidence-ledger head and rerun -all six target-native cells. Require the expected bounded toolset identity and exact linker SHA-256, -exact SPDX ownership, archive-scoped document identity, commit-pinned builder identity, resolved -runner identity, complete bounded tool records with SHA-256, clean-build metadata equality, smoke, -upload, and regenerated Windows content identities before checking the remaining Milestone 3 -metadata/toolchain claims. Then proceed to oldest-baseline and native-trust proof. +Push bounded linker-path diagnostic commit `d1eb45d61` plus this evidence-ledger head and rerun all +six target-native cells. Inspect the bounded x64 and arm64 path tails, then make only the parser +correction supported by those native paths. Require the corrected exact head to retain the exact +linker SHA-256 and pass exact SPDX ownership, archive-scoped document identity, commit-pinned builder +identity, resolved runner identity, complete bounded tool records, clean-build metadata equality, +smoke, upload, regenerated Windows content identities, and direct artifact inspection before +checking the remaining Milestone 3 metadata/toolchain claims. Then proceed to oldest-baseline and +native-trust proof. 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 From 5f0c1417d5e28a0c01f1914c41d129814237e28e Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 18:33:47 -0400 Subject: [PATCH 071/267] build(ssh): select canonical MSVC linker Co-authored-by: Orca --- .../scripts/ssh-relay-runtime-toolchain.mjs | 39 +++++++++++++++++-- .../ssh-relay-runtime-toolchain.test.mjs | 22 ++++++++++- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/config/scripts/ssh-relay-runtime-toolchain.mjs b/config/scripts/ssh-relay-runtime-toolchain.mjs index 4d4cef54ca6..afee29e33a7 100644 --- a/config/scripts/ssh-relay-runtime-toolchain.mjs +++ b/config/scripts/ssh-relay-runtime-toolchain.mjs @@ -55,7 +55,7 @@ export function selectSshRelayRuntimeToolVersion({ stdout = '', stderr = '' }, p return version } -async function locateExecutable(command, { xcrun = false } = {}) { +async function locateExecutable(command, { windowsMsvcLinker = false, xcrun = false } = {}) { if (isAbsolute(command)) { return realpath(command) } @@ -65,10 +65,11 @@ async function locateExecutable(command, { xcrun = false } = {}) { ? ['where.exe', [command]] : ['which', [command]] const result = await capture(...locator) - const path = result.stdout + const paths = result.stdout .split(/\r?\n/) .map((line) => line.trim()) - .find(Boolean) + .filter(Boolean) + const path = windowsMsvcLinker ? selectSshRelayRuntimeWindowsMsvcLinker(paths) : paths[0] if (!path || !isAbsolute(path)) { throw new Error(`Runtime build tool executable could not be resolved: ${command}`) } @@ -84,7 +85,10 @@ async function executableRecord({ windowsMsvcToolsetVersion = false, xcrun = false }) { - const path = await locateExecutable(command, { xcrun }) + const path = await locateExecutable(command, { + windowsMsvcLinker: windowsMsvcToolsetVersion, + xcrun + }) const result = windowsMsvcToolsetVersion ? { stdout: sshRelayRuntimeWindowsMsvcToolsetVersion(path) } : await capture(versionCommand ?? path, versionArgs ?? args) @@ -207,6 +211,33 @@ export function sshRelayRuntimeWindowsMsvcToolsetVersion(path) { return `MSVC ${version}` } +export function selectSshRelayRuntimeWindowsMsvcLinker(paths) { + const candidates = [ + ...new Map(paths.map((path) => [win32.normalize(path).toLowerCase(), path])).values() + ] + const matches = candidates.filter((path) => { + try { + sshRelayRuntimeWindowsMsvcToolsetVersion(path) + return true + } catch { + return false + } + }) + if (matches.length !== 1) { + const diagnostic = + candidates + .slice(-4) + .map((path) => win32.normalize(path).split(win32.sep).slice(-8).join(win32.sep)) + .join(' | ') + .slice(-512) || '' + throw new Error( + `Runtime build did not resolve exactly one canonical MSVC linker: ${diagnostic}` + ) + } + // Why: Git for Windows also provides link.exe and is prepended for signature verification in CI. + return matches[0] +} + export function assertSshRelayRuntimeToolchain(toolchain, tuple) { const common = ['buildNode', 'bundledNode', 'compiler', 'buildSystem', 'python', 'archive'] const expected = tuple.startsWith('win32-') diff --git a/config/scripts/ssh-relay-runtime-toolchain.test.mjs b/config/scripts/ssh-relay-runtime-toolchain.test.mjs index 4f172b5dd24..a366104408c 100644 --- a/config/scripts/ssh-relay-runtime-toolchain.test.mjs +++ b/config/scripts/ssh-relay-runtime-toolchain.test.mjs @@ -4,6 +4,7 @@ import { assertSshRelayRuntimeToolchain, collectSshRelayRuntimeToolchain, selectSshRelayRuntimeToolVersion, + selectSshRelayRuntimeWindowsMsvcLinker, sshRelayRuntimeBuilderIdentity, sshRelayRuntimeRunnerIdentity, sshRelayRuntimeStripVersionProbe, @@ -52,6 +53,20 @@ describe('SSH relay runtime build provenance', () => { ).toThrow(/MSVC toolset path/) }) + it('selects exactly one canonical MSVC linker after Git for Windows PATH entries', () => { + const x64 = String.raw`C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\bin\HostX64\x64\link.exe` + const arm64 = String.raw`C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\bin\HostARM64\ARM64\link.exe` + const git = String.raw`C:\Program Files\Git\usr\bin\link.exe` + expect(selectSshRelayRuntimeWindowsMsvcLinker([git, x64])).toBe(x64) + expect(selectSshRelayRuntimeWindowsMsvcLinker([git, arm64])).toBe(arm64) + expect(() => selectSshRelayRuntimeWindowsMsvcLinker([git])).toThrow( + /exactly one canonical MSVC linker/ + ) + expect(() => selectSshRelayRuntimeWindowsMsvcLinker([x64, arm64])).toThrow( + /exactly one canonical MSVC linker/ + ) + }) + it('bounds failed version diagnostics', () => { expect(() => selectSshRelayRuntimeToolVersion({ stderr: `unexpected ${'x'.repeat(1_000)}` }, /version/i) @@ -59,10 +74,15 @@ describe('SSH relay runtime build provenance', () => { }) it('bounds rejected Windows linker path diagnostics', () => { - const path = String.raw`C:\${'unexpected\\'.repeat(100)}link.exe` + const path = `C:\\${'unexpected\\'.repeat(100)}link.exe` expect(() => sshRelayRuntimeWindowsMsvcToolsetVersion(path)).toThrow( /^Resolved Windows linker is not in a bounded MSVC toolset path: .{1,512}$/ ) + expect(() => + selectSshRelayRuntimeWindowsMsvcLinker( + Array.from({ length: 20 }, (_, index) => `C:\\${index}\\${'x'.repeat(600)}\\link.exe`) + ) + ).toThrow(/^Runtime build did not resolve exactly one canonical MSVC linker: .{1,512}$/) }) it('pins GitHub builder identity to the exact source commit', () => { From ff3ebf37e32e9dad5834e669438efd46a974a708 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 18:35:32 -0400 Subject: [PATCH 072/267] docs(ssh): record linker collision evidence Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 15 ++- ...github-release-implementation-checklist.md | 123 +++++++++++++++--- 2 files changed, 116 insertions(+), 22 deletions(-) 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 f715ded686a..1f1caeccecc 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 @@ -22,13 +22,14 @@ complete a box. - [ ] **WP2 — Finish target-native artifact metadata and provenance.** - Four POSIX GitHub runner cells pass build, smoke, equality, metadata upload, and direct payload - audit in [run 29372156145](https://github.com/stablyai/orca/actions/runs/29372156145). - - Windows x64/arm64 fail closed because the resolved `link.exe` path differs from the assumed MSVC - layout; neither cell downloads inputs or produces an artifact. - - Local commit `d1eb45d61` adds a bounded path-tail diagnostic and is green across 20 test files / - 99 tests plus static gates. - - Next gate: push the diagnostic, inspect both native path shapes, then make only the evidenced - parser correction and rerun all six jobs. + audit in [run 29372816457](https://github.com/stablyai/orca/actions/runs/29372816457). + - Windows x64/arm64 prove Git for Windows' `link.exe` shadows the configured MSVC linker; neither + cell downloads inputs or produces an artifact. + - Local commit `5f0c1417d` selects exactly one canonical MSVC candidate while keeping the strict + path grammar and exact linker hash. It is green across 20 test files / 100 tests plus static + gates. + - Next gate: push the correction, rerun all six native jobs, then directly inspect all six + artifacts before completing the metadata/provenance box. ## Work packages, in required order 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 4d3d5cd9497..813bf18fd96 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 SBOM/provenance closure — **In progress — 2026-07-14, Codex implementation owner**; exact-head run [29372156145](https://github.com/stablyai/orca/actions/runs/29372156145) and direct payload audit keep Linux x64/arm64 and macOS x64/arm64 green but both Windows cells fail closed before build/upload because their resolved linker paths do not match the assumed MSVC toolset layout (E-M3-METADATA-CI-RED-006); exact implementation commit `d1eb45d61` retains that strict parser and exact linker SHA-256 while adding only a 512-byte bounded path-tail diagnostic, and passes 20 files / 99 tests plus typecheck, full lint, formatting, max-lines, and diff gates (E-M3-WINDOWS-LINKER-PATH-DIAGNOSTIC-LOCAL-001); the next native run must expose both real path shapes before any parser correction, and the eventual exact head must pass all six cells plus direct artifact inspection before any remaining SBOM/provenance/toolchain box is checked; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      +Current phase: Milestone 3 / Work Package 2 SBOM/provenance closure — **In progress — 2026-07-14, Codex implementation owner**; exact-head run [29372816457](https://github.com/stablyai/orca/actions/runs/29372816457) and direct payload audit keep Linux x64/arm64 and macOS x64/arm64 green while both Windows cells independently prove Git for Windows' `C:\Program Files\Git\usr\bin\link.exe` shadows the configured MSVC linker and fail closed before input download/build/upload (E-M3-METADATA-CI-RED-007); exact implementation commit `5f0c1417d` selects exactly one strict canonical MSVC candidate from all resolved linker paths, rejects zero/ambiguous matches, retains the exact selected-binary SHA-256, and passes 20 files / 100 tests plus typecheck, full lint, formatting, max-lines, and diff gates (E-M3-WINDOWS-LINKER-SELECTION-LOCAL-001); the correction requires an exact-head all-six native run and direct inspection of all six artifacts before any remaining SBOM/provenance/toolchain box is checked; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      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) @@ -175,12 +175,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: push bounded linker-path diagnostic commit `d1eb45d61` plus its evidence - ledger head and rerun all six target-native cells. Inspect the bounded x64 and arm64 path tails, - then make only the parser correction supported by those native paths. The corrected exact head - must retain the exact linker SHA-256 and pass all metadata, smoke, equality, upload, and direct - payload audits before the all-six provenance item can close. Preserve the artifact-only boundary, - legacy/default path, and every other release gate. +- Next required action: push unique canonical MSVC linker-selection commit `5f0c1417d` plus its + evidence ledger head and rerun all six target-native cells. Require one strict MSVC match despite + the Git-for-Windows PATH collision, the exact selected-linker SHA-256, and all metadata, smoke, + equality, upload, and direct payload audits before the all-six provenance item can close. Preserve + the artifact-only boundary, legacy/default path, and every other release gate. ## Non-Negotiable Invariants @@ -6831,6 +6830,101 @@ bounded version line: `. The PE lookup returned successfully but the `Fil - Follow-up: push this exact commit and ledger head, inspect both bounded native path tails, and make only the parser correction supported by that evidence. +### E-M3-METADATA-CI-RED-007 — Git for Windows shadows the configured MSVC linker + +- Date: 2026-07-14 +- Commit SHA / PR: exact tested head `92f9b76101d4214c07291f6d9befa647fbc0dd86`; + stacked draft PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: [GitHub Actions run 29372816457](https://github.com/stablyai/orca/actions/runs/29372816457), + final conclusion `failure` after 8m11s. Native job IDs and wall durations: Linux x64 + `87219773628` / 2m44s, Linux arm64 `87219773618` / 3m33s, macOS x64 `87219773620` / + 8m06s, macOS arm64 `87219773629` / 2m59s, Windows x64 `87219773636` / 1m18s, and + Windows arm64 `87219773684` / 5m10s. +- Remote and transport: none; target-native artifact-only build workflow +- Exact evidence commands: + + ```sh + gh run view 29372816457 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh api repos/stablyai/orca/actions/runs/29372816457/artifacts + gh api repos/stablyai/orca/actions/jobs/87219773636/logs + gh api repos/stablyai/orca/actions/jobs/87219773684/logs + gh run download 29372816457 --repo stablyai/orca \ + --dir /tmp/orca-8450-metadata-red-29372816457 + # Repeat the exact fail-closed jq/shasum audit in E-M3-METADATA-CI-RED-003 with: + # evidence=/tmp/orca-8450-metadata-red-29372816457 + # commit=92f9b76101d4214c07291f6d9befa647fbc0dd86 + # run_id=29372816457 + ``` + +- Result: EXPECTED RED. All four POSIX cells passed contracts, two clean native builds, + archive/tree inspection, bundled runtime smoke, byte-for-byte comparison, and upload. Windows x64 + and arm64 each passed 18 contract files before the live toolchain test rejected + `C:\Program Files\Git\usr\bin\link.exe` as non-MSVC. Both stopped before input download, build, + smoke, comparison, or upload, so no Windows artifact exists. +- Downloaded-artifact audit: + + | Tuple | Artifact ID | Archive SHA-256 | Content ID prefix | Files | + | ----------------- | ------------ | ------------------------------------------------------------------ | ----------------- | ----- | + | linux-x64-glibc | `8326768093` | `c312a404515db43b7219ec7c0f5ff8c33a1424a32b3cf42754820a1599afcec0` | `960546cd…` | 34 | + | linux-arm64-glibc | `8326787062` | `5e31f1377be855fdddba08dab19af92360236fc9c4a08a3c6320162aa4dca4a0` | `aa3aa8ae…` | 34 | + | darwin-x64 | `8326884270` | `a3cacc1aaf16c659218987972a39ff367584da456d167906b13e293874297777` | `585ea603…` | 35 | + | darwin-arm64 | `8326773209` | `612e7328700bef7886df5caaef92c22cbbc38ad872ca40b17f0758b28e75bd92` | `40ff5d20…` | 35 | + + All archive/subject hashes, archive-scoped SPDX namespaces, one-owner-per-file counts, eight + dependency relationships, prohibited-content assertions, exact commit/run identities, runner + labels/images/architectures, and bounded tool version/hash records passed. + +- Oracle proved: appending Git's signature-tool directory through `GITHUB_PATH` shadows `link.exe` + identically on x64 and arm64; the first PATH match is not the configured compiler toolchain; both + jobs fail closed before consuming release inputs; four independent POSIX controls remain complete. +- Does not prove: that `where.exe` exposes exactly one later canonical MSVC candidate, a Windows + artifact/metadata record, oldest baselines, native trust/signing, SSH, publication, or an enabled + tuple. +- Checklist items satisfied: no new completion box; the all-six SBOM/provenance/toolchain item + remains in progress. +- Follow-up: select exactly one canonical MSVC linker from all resolved candidates, retain its exact + SHA-256, reject zero/ambiguous matches, and rerun all six native cells. + +### E-M3-WINDOWS-LINKER-SELECTION-LOCAL-001 — Unique canonical MSVC candidate + +- Date: 2026-07-14 +- Commit SHA / PR: exact implementation commit + `5f0c1417d5e28a0c01f1914c41d129814237e28e`; stacked draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: local macOS 26.2 build 25C56, Darwin 25.2.0 arm64 on Apple Silicon; Node v26.0.0 and + pnpm 10.24.0 for pure path/provenance tests +- Remote and transport: none; artifact-only local contracts +- Exact evidence commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-*.test.mjs \ + src/main/ssh/ssh-relay-artifact-selector.test.ts + pnpm run typecheck + pnpm run lint + pnpm run check:max-lines-ratchet + pnpm exec oxfmt --check \ + config/scripts/ssh-relay-runtime-toolchain.mjs \ + config/scripts/ssh-relay-runtime-toolchain.test.mjs + git diff --check + ``` + +- Result: PASS. Twenty focused test files passed 100 tests. Typecheck passed all three TypeScript + projects. Full lint passed with only existing unrelated warnings; the independent max-lines, + focused formatting, and diff checks passed. +- Oracle proved: a non-MSVC Git `link.exe` may precede either canonical x64 or arm64 MSVC path; + selection accepts exactly one case-insensitively deduplicated strict MSVC match, rejects zero or + multiple matches, and bounds rejection diagnostics. The strict absolute `MSVC//bin/...` + grammar and exact SHA-256 of the selected linker remain required. The corrected test also proves + the long-path fixtures actually interpolate and exercise the 512-byte bounds. +- Does not prove: the exact native candidate list, a Windows build/smoke/equality/upload, regenerated + Windows content IDs, oldest Windows execution, native trust, SSH, publication, or an enabled tuple. +- Checklist items satisfied: local correction only; the all-six compiler/toolchain item remains + unchecked. +- Follow-up: push this exact commit and ledger head, then require all six native jobs and direct + inspection of every uploaded artifact before closing any metadata/provenance claim. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -6888,14 +6982,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push bounded linker-path diagnostic commit `d1eb45d61` plus this evidence-ledger head and rerun all -six target-native cells. Inspect the bounded x64 and arm64 path tails, then make only the parser -correction supported by those native paths. Require the corrected exact head to retain the exact -linker SHA-256 and pass exact SPDX ownership, archive-scoped document identity, commit-pinned builder -identity, resolved runner identity, complete bounded tool records, clean-build metadata equality, -smoke, upload, regenerated Windows content identities, and direct artifact inspection before -checking the remaining Milestone 3 metadata/toolchain claims. Then proceed to oldest-baseline and -native-trust proof. +Push unique canonical MSVC linker-selection commit `5f0c1417d` plus this evidence-ledger head and +rerun all six target-native cells. Require exactly one strict MSVC candidate despite the +Git-for-Windows PATH collision, retain the exact selected-linker SHA-256, and pass exact SPDX +ownership, archive-scoped document identity, commit-pinned builder identity, resolved runner +identity, complete bounded tool records, clean-build metadata equality, smoke, upload, regenerated +Windows content identities, and direct artifact inspection before checking the remaining Milestone +3 metadata/toolchain claims. Then proceed to oldest-baseline and native-trust proof. 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 From 775f2cbcc7f66f6e728fa06ae2d6822edd50f7b0 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 18:48:44 -0400 Subject: [PATCH 073/267] docs(ssh): close native metadata gate Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 21 ++-- ...github-release-implementation-checklist.md | 105 +++++++++++++++--- 2 files changed, 97 insertions(+), 29 deletions(-) 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 1f1caeccecc..33201f518e3 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 @@ -20,16 +20,15 @@ complete a box. ## Active work -- [ ] **WP2 — Finish target-native artifact metadata and provenance.** - - Four POSIX GitHub runner cells pass build, smoke, equality, metadata upload, and direct payload - audit in [run 29372816457](https://github.com/stablyai/orca/actions/runs/29372816457). - - Windows x64/arm64 prove Git for Windows' `link.exe` shadows the configured MSVC linker; neither - cell downloads inputs or produces an artifact. - - Local commit `5f0c1417d` selects exactly one canonical MSVC candidate while keeping the strict - path grammar and exact linker hash. It is green across 20 test files / 100 tests plus static - gates. - - Next gate: push the correction, rerun all six native jobs, then directly inspect all six - artifacts before completing the metadata/provenance box. +- [ ] **WP2 — Prove oldest supported baselines and native trust.** + - All six target-native artifact, smoke, equality, SBOM, license, provenance, toolchain, and + prohibited-content cells pass in + [run 29373507297](https://github.com/stablyai/orca/actions/runs/29373507297); every downloaded + payload passed direct audit. + - No tuple is enabled: the artifacts still need execution on each declared oldest OS/libc/kernel + baseline and target-native signing/trust proof. + - Next gate: add the purpose-named baseline verification capability and GitHub-runner cells that + can qualify, while keeping unavailable snapshot/trust cells explicit and disabled. ## Work packages, in required order @@ -55,7 +54,7 @@ complete a box. - [x] Build and smoke-test Node, patched `node-pty`, and `@parcel/watcher` on GitHub runners for Linux, macOS, and Windows on x64 and arm64. - [x] Prove exact clean-build equality and exact runtime-tree closure on all six runner families. -- [ ] Complete the all-six SBOM, license, provenance, toolchain, and prohibited-content audit. +- [x] Complete the all-six SBOM, license, provenance, toolchain, and prohibited-content audit. - [ ] Prove each candidate on its oldest supported OS/libc/kernel baseline. - [ ] Sign macOS and Windows bytes and verify native trust on the target OS. - [ ] Merge draft PR [#8741](https://github.com/stablyai/orca/pull/8741). 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 813bf18fd96..1a4f99a9b8a 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
      Last updated: 2026-07-14
      -Current phase: Milestone 3 / Work Package 2 SBOM/provenance closure — **In progress — 2026-07-14, Codex implementation owner**; exact-head run [29372816457](https://github.com/stablyai/orca/actions/runs/29372816457) and direct payload audit keep Linux x64/arm64 and macOS x64/arm64 green while both Windows cells independently prove Git for Windows' `C:\Program Files\Git\usr\bin\link.exe` shadows the configured MSVC linker and fail closed before input download/build/upload (E-M3-METADATA-CI-RED-007); exact implementation commit `5f0c1417d` selects exactly one strict canonical MSVC candidate from all resolved linker paths, rejects zero/ambiguous matches, retains the exact selected-binary SHA-256, and passes 20 files / 100 tests plus typecheck, full lint, formatting, max-lines, and diff gates (E-M3-WINDOWS-LINKER-SELECTION-LOCAL-001); the correction requires an exact-head all-six native run and direct inspection of all six artifacts before any remaining SBOM/provenance/toolchain box is checked; oldest-baseline, native-trust, cross-family remote, and measured-baseline gates remain open; production/default behavior and every tuple state remain unchanged; no bundled-runtime path is enabled
      +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
      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) @@ -175,11 +175,12 @@ 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: push unique canonical MSVC linker-selection commit `5f0c1417d` plus its - evidence ledger head and rerun all six target-native cells. Require one strict MSVC match despite - the Git-for-Windows PATH collision, the exact selected-linker SHA-256, and all metadata, smoke, - equality, upload, and direct payload audits before the all-six provenance item can close. Preserve - the artifact-only boundary, legacy/default path, and every other release gate. +- 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. ## Non-Negotiable Invariants @@ -648,6 +649,14 @@ archive inspection, bundled-Node/native-module/PTY/watcher smoke, SBOM, and prov behind purpose-named scripts and CI artifacts. No release publication, desktop resolver, SSH transfer, install, fallback, rollout setting, or tuple enablement belongs in this package. +**Current active gate — 2026-07-14, Codex implementation owner:** exact-head run 29373507297 and +direct inspection of all six downloaded payloads close target-native metadata/provenance/toolchain +completeness under E-M3-METADATA-CI-001. The next artifact-only package must prove the declared +oldest glibc/libstdc++/kernel, macOS, and Windows floors and target-native trust. A newer hosted +runner, static symbol scan, or container may add evidence but cannot silently stand in for an +unavailable qualifying snapshot. Every tuple remains disabled and no production consumer is +connected. + **Active sub-gate — 2026-07-14, Codex implementation owner:** exact-head run 29345126283 proves all six target-native artifact/executable jobs, including normal Windows x64/arm64 settlement and unpublished upload. E-M3-REPRODUCIBILITY-LOCAL-001 proves the bounded comparator requires two @@ -798,8 +807,7 @@ Each runtime must contain only the executable closure required by the relay. - [x] Include exactly one compatible `@parcel/watcher@2.5.6` native optional package. (E-M3-RUNTIME-CLOSURE-CI-001) - [x] Include relay JavaScript, watcher child, required runtime JavaScript closure, licenses, SBOM, - provenance, and runtime metadata. (E-M3-RUNTIME-CLOSURE-CI-001; metadata completeness remains - a separate open gate) + provenance, and runtime metadata. (E-M3-RUNTIME-CLOSURE-CI-001, E-M3-METADATA-CI-001) - [x] Exclude package managers, development dependencies, compilers, sources, caches, build directories, and Orca-built debug symbols unless an approved diagnostics requirement needs them. Preserve verified official Node executable bytes without stripping or rewriting them, @@ -836,9 +844,10 @@ Rules: - [x] Every current candidate archive executes the exact bundled Node, loads both native dependencies, spawns a real PTY, performs input/resize/exit, and observes create/modify/rename/delete watcher events. (E-M3-REPRODUCIBILITY-CI-001) -- [ ] Every build records compiler/toolchain/container image digests and runner architecture. - **In progress — 2026-07-14, Codex implementation owner:** run 29368482959 failed closed on - both Windows architectures at the linker-version probe; no Windows build or upload followed. +- [x] Every build records compiler/toolchain/container image digests and runner architecture. + Current jobs use no build container; each hosted runner records its requested label, resolved + image/version, native architecture, exact tool versions, and executable/code SHA-256 values. + (E-M3-METADATA-CI-001) ## Milestone 4 — Make Relay Artifacts Prerequisites of Desktop Builds @@ -6925,6 +6934,67 @@ bounded version line: `. The PE lookup returned successfully but the `Fil - Follow-up: push this exact commit and ledger head, then require all six native jobs and direct inspection of every uploaded artifact before closing any metadata/provenance claim. +### E-M3-METADATA-CI-001 — All-six native metadata and provenance closure + +- Date: 2026-07-14 +- Commit SHA / PR: exact tested head `ff3ebf37e32e9dad5834e669438efd46a974a708`; + stacked draft PR [#8741](https://github.com/stablyai/orca/pull/8741) +- Runner: [GitHub Actions run 29373507297](https://github.com/stablyai/orca/actions/runs/29373507297), + final conclusion `success` after 9m13s. Native job IDs and wall durations: Linux x64 + `87221923438` / 2m55s, Linux arm64 `87221923415` / 3m06s, macOS x64 `87221923414` / + 7m02s, macOS arm64 `87221923432` / 3m29s, Windows x64 `87221923434` / 4m50s, and + Windows arm64 `87221923449` / 9m08s. +- Remote and transport: none; target-native artifact-only build workflow +- Exact evidence commands: + + ```sh + gh run view 29373507297 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh api repos/stablyai/orca/actions/runs/29373507297/artifacts + gh run download 29373507297 --repo stablyai/orca \ + --dir /tmp/orca-8450-metadata-green-29373507297 + # Repeat the exact fail-closed jq/shasum audit in E-M3-METADATA-CI-RED-003 with: + # evidence=/tmp/orca-8450-metadata-green-29373507297 + # commit=ff3ebf37e32e9dad5834e669438efd46a974a708 + # run_id=29373507297 + ``` + +- Result: PASS. All six native cells passed contract tests, authenticated exact Node inputs, two + independent clean builds, archive/tree inspection, bundled Node/native PTY/watcher smoke, + byte-for-byte runtime/archive/identity/SPDX/provenance comparison, and unpublished upload. Direct + inspection of all six downloaded payloads passed every archive/subject hash, archive-scoped SPDX + namespace, one-owner-per-file count, eight-dependency relationship, prohibited-content, exact + commit/run/builder/source identity, runner identity, and bounded tool version/hash assertion. +- Downloaded-artifact audit: + + | Tuple | Artifact ID | Archive SHA-256 | Content ID prefix | Files | Runner image | + | ----------------- | ------------ | ------------------------------------------------------------------ | ----------------- | ----- | ------------------------------ | + | linux-x64-glibc | `8327034796` | `177ad16222b08beb972bf66910bbafd32bf1e820deb007fef3304057cb6a01b8` | `960546cd…` | 34 | `ubuntu24` 20260705.232.1 | + | linux-arm64-glibc | `8327039534` | `9042214412ce73cd1febf94ff48a17de7bd979808e2226485fbbbf4b73fc093a` | `aa3aa8ae…` | 34 | `ubuntu24-arm64` 20260706.52.2 | + | darwin-x64 | `8327118454` | `449d96e0230f9f07437df571ccf4ab64bc124f9b398013e59c0d2ca1c4be0db8` | `585ea603…` | 35 | `macos15` 20260629.0276.1 | + | darwin-arm64 | `8327045964` | `cb593b9a30a82c743e4fffb201b57192c55af927bfce71a0cf9f6dc643dd20cb` | `40ff5d20…` | 35 | `macos15` 20260706.0213.1 | + | win32-x64 | `8327073419` | `b59aa25e59fb148b24dcc56d3c5ef6f1545a42673ba4e4f99a4b27fa1d0fb253` | `7ddad668…` | 42 | `win22` 20260706.237.1 | + | win32-arm64 | `8327157772` | `6b2fa8471a85c4d6872df34caf32acfdb7e9bf6c94cf83af06d5f19cecc1f33e` | `2955cec7…` | 42 | `win11-arm64` 20260706.102.1 | + + Windows x64 records `MSVC 14.44.35207` with exact linker SHA-256 + `ca11e6c45debd34bf652dfe984c5360a531a005ed78bf72852330c9c2590cf0d`; Windows arm64 records the + same bounded toolset identity with distinct exact linker SHA-256 + `f167f4e80fe6c38ba7099c22d83ef189e906b97b03d316b3cbfbac1eadc9fa6a`. No build job uses a + container; every job records the requested native runner label, resolved image/version, + architecture, environment, and exact tool executable or package-tree digests. + +- Oracle proved: the current six candidates have complete target-native build metadata, exact + clean-build identity, SPDX file/package ownership, license/closure contracts, runner identity, + bounded compiler/toolchain records, and direct payload consistency at the tested exact head. The + Git-for-Windows `link.exe` collision does not change the selected MSVC binary or its recorded hash. +- Does not prove: execution on declared oldest OS/libc/kernel floors, native signing/trust, + cross-family SSH transfer/install, release aggregation/publication, packaged desktop consumption, + fallback, rollout UI, performance, or an enabled tuple. +- Checklist items satisfied: the Milestone 3 compiler/toolchain/runner record item and the short + tracker's all-six SBOM/license/provenance/toolchain/prohibited-content item. +- Follow-up: implement and run purpose-named oldest-supported-baseline checks, then prove native + signing/trust before connecting any artifact consumer or enabling any tuple. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -6982,13 +7052,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push unique canonical MSVC linker-selection commit `5f0c1417d` plus this evidence-ledger head and -rerun all six target-native cells. Require exactly one strict MSVC candidate despite the -Git-for-Windows PATH collision, retain the exact selected-linker SHA-256, and pass exact SPDX -ownership, archive-scoped document identity, commit-pinned builder identity, resolved runner -identity, complete bounded tool records, clean-build metadata equality, smoke, upload, regenerated -Windows content identities, and direct artifact inspection before checking the remaining Milestone -3 metadata/toolchain claims. Then proceed to oldest-baseline and native-trust proof. +Implement purpose-named oldest-supported-baseline verification and artifact-only workflow cells. +Use GitHub-hosted native runners only where their OS/architecture actually satisfies a declared +floor; add static glibc/libstdc++/kernel eligibility oracles where execution on a newer runner is +insufficient, and do not claim an unavailable macOS/Windows floor from a newer hosted image. Record +the exact container image digest when a baseline container supplements native execution. Then prove +target-native signing/trust before connecting any artifact consumer or enabling any tuple. 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 From 0cb3f7510b55db50525dbc7b27b2086ef83b0c49 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 20:00:11 -0400 Subject: [PATCH 074/267] build(ssh): prove Linux runtime floor Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 270 +++++++++++++++++- config/scripts/build-ssh-relay-runtime.mjs | 5 + .../ssh-relay-linux-node-gyp-compiler.mjs | 20 ++ ...ssh-relay-linux-node-gyp-compiler.test.mjs | 57 ++++ config/scripts/ssh-relay-runtime-baseline.mjs | 258 +++++++++++++++++ .../ssh-relay-runtime-baseline.test.mjs | 149 ++++++++++ .../ssh-relay-runtime-compatibility.mjs | 38 +++ ...ssh-relay-runtime-linux-build-evidence.mjs | 161 +++++++++++ ...elay-runtime-linux-build-evidence.test.mjs | 79 +++++ .../scripts/ssh-relay-runtime-toolchain.mjs | 13 +- .../ssh-relay-runtime-toolchain.test.mjs | 11 + config/scripts/ssh-relay-runtime-tree.mjs | 42 +-- .../ssh-relay-runtime-workflow.test.mjs | 108 ++++++- ...-relay-runtime-linux-builder.Containerfile | 28 ++ 14 files changed, 1193 insertions(+), 46 deletions(-) create mode 100644 config/scripts/ssh-relay-linux-node-gyp-compiler.mjs create mode 100644 config/scripts/ssh-relay-linux-node-gyp-compiler.test.mjs create mode 100644 config/scripts/ssh-relay-runtime-baseline.mjs create mode 100644 config/scripts/ssh-relay-runtime-baseline.test.mjs create mode 100644 config/scripts/ssh-relay-runtime-compatibility.mjs create mode 100644 config/scripts/ssh-relay-runtime-linux-build-evidence.mjs create mode 100644 config/scripts/ssh-relay-runtime-linux-build-evidence.test.mjs create mode 100644 config/ssh-relay-runtime-linux-builder.Containerfile diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index b811e9c7998..feb83e420d3 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -5,6 +5,7 @@ on: paths: - '.github/workflows/ssh-relay-runtime-artifacts.yml' - 'config/patches/node-pty@1.1.0.patch' + - 'config/ssh-relay-runtime-linux-builder.Containerfile' - 'config/scripts/build-relay.mjs' - 'config/scripts/*ssh-relay*' - 'config/ssh-relay-node-release-keys/**' @@ -32,9 +33,11 @@ jobs: - runner: ubuntu-24.04 tuple: linux-x64-glibc node_archive: node-v24.18.0-linux-x64.tar.xz + container_image: docker.io/library/rockylinux@sha256:2d05a9266523bbf24f33ebc3a9832e4d5fd74b973c220f2204ca802286aa275d - runner: ubuntu-24.04-arm tuple: linux-arm64-glibc node_archive: node-v24.18.0-linux-arm64.tar.xz + container_image: docker.io/library/rockylinux@sha256:3c2d0ce12bf79fc5ff05e43b1000e30ff062dc89405525f3307cbff71661f1a0 - runner: macos-15-intel tuple: darwin-x64 node_archive: node-v24.18.0-darwin-x64.tar.xz @@ -107,6 +110,12 @@ jobs: set -euo pipefail node --check config/scripts/ssh-relay-runtime-reproducibility.mjs node --check config/scripts/ssh-relay-runtime-reproducibility.test.mjs + node --check config/scripts/ssh-relay-runtime-baseline.mjs + node --check config/scripts/ssh-relay-runtime-baseline.test.mjs + node --check config/scripts/ssh-relay-runtime-linux-build-evidence.mjs + node --check config/scripts/ssh-relay-runtime-linux-build-evidence.test.mjs + node --check config/scripts/ssh-relay-linux-node-gyp-compiler.mjs + node --check config/scripts/ssh-relay-linux-node-gyp-compiler.test.mjs node --check config/scripts/build-ssh-relay-runtime.mjs node --check config/scripts/ssh-relay-runtime-build.test.mjs node --check config/scripts/ssh-relay-runtime-closure.mjs @@ -119,12 +128,15 @@ jobs: 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 \ + config/scripts/ssh-relay-linux-node-gyp-compiler.test.mjs \ config/scripts/ssh-relay-node-pty-build.test.mjs \ config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs \ config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ config/scripts/ssh-relay-node-zip-inspection.test.mjs \ config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-baseline.test.mjs \ config/scripts/ssh-relay-runtime-build.test.mjs \ + config/scripts/ssh-relay-runtime-linux-build-evidence.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 \ @@ -150,17 +162,63 @@ jobs: done printf 'NODE_INPUTS=%s\n' "$inputs" >> "$GITHUB_ENV" + - name: Prepare digest-pinned Linux floor builder + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + image='${{ matrix.container_image }}' + builder="orca-ssh-relay-linux-builder:${GITHUB_RUN_ID}-${{ matrix.tuple }}" + docker pull "$image" + docker build --pull=false \ + --build-arg "BASE_IMAGE=$image" \ + --file config/ssh-relay-runtime-linux-builder.Containerfile \ + --tag "$builder" . + docker image inspect "$image" --format 'linux_floor_base={{index .RepoDigests 0}}' + docker image inspect "$builder" --format 'linux_floor_builder_id={{.Id}}' + printf 'ORCA_LINUX_BUILDER=%s\n' "$builder" >> "$GITHUB_ENV" + - name: Build twice, inspect, smoke, and compare exact runtime shell: bash run: | set -euo pipefail output_root="$RUNNER_TEMP/ssh-relay-runtime/${{ matrix.tuple }}" - mkdir -p "$output_root" - first_output="$output_root/first" - second_output="$output_root/second" work_directory="$RUNNER_TEMP/orca-ssh-relay-runtime-build-work" source_commit=$(git rev-parse HEAD) source_date_epoch=$(git show -s --format=%ct "$source_commit") + if [[ '${{ matrix.tuple }}' == linux-* ]]; then + # Why: package installation is isolated earlier; native compilation itself has no egress. + docker run --rm --network none --read-only --cap-drop all \ + --security-opt no-new-privileges --pids-limit 512 --memory 6g --cpus 4 \ + --tmpfs /tmp:rw,nosuid,size=1g \ + --mount "type=bind,src=$GITHUB_WORKSPACE,dst=$GITHUB_WORKSPACE" \ + --mount "type=bind,src=$RUNNER_TEMP,dst=$RUNNER_TEMP" \ + --workdir "$GITHUB_WORKSPACE" \ + --env "GITHUB_ACTIONS=$GITHUB_ACTIONS" \ + --env "GITHUB_REPOSITORY=$GITHUB_REPOSITORY" \ + --env "GITHUB_RUN_ID=$GITHUB_RUN_ID" \ + --env "GITHUB_WORKFLOW_REF=$GITHUB_WORKFLOW_REF" \ + --env "RUNNER_OS=$RUNNER_OS" \ + --env "RUNNER_ARCH=$RUNNER_ARCH" \ + --env "RUNNER_ENVIRONMENT=$RUNNER_ENVIRONMENT" \ + --env "ORCA_RUNTIME_REQUESTED_RUNNER=$ORCA_RUNTIME_REQUESTED_RUNNER" \ + --env 'ImageOS=rockylinux-8' \ + --env 'ImageVersion=${{ matrix.container_image }}' \ + --env 'HOME=/tmp' \ + "$ORCA_LINUX_BUILDER" \ + /usr/bin/node config/scripts/ssh-relay-runtime-linux-build-evidence.mjs \ + --tuple '${{ matrix.tuple }}' \ + --inputs-directory "$NODE_INPUTS" \ + --output-root "$output_root" \ + --work-directory "$work_directory" \ + --evidence-directory "$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}" \ + --source-date-epoch "$source_date_epoch" \ + --git-commit "$source_commit" + exit 0 + fi + mkdir -p "$output_root" + first_output="$output_root/first" + second_output="$output_root/second" for output in "$first_output" "$second_output"; do printf 'clean_build_output=%s\n' "$output" /usr/bin/time -p node config/scripts/build-ssh-relay-runtime.mjs \ @@ -286,6 +344,18 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime reproducibility script syntax check failed' } node --check config/scripts/ssh-relay-runtime-reproducibility.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime reproducibility test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-baseline.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime baseline script syntax check failed' } + node --check config/scripts/ssh-relay-runtime-baseline.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime baseline test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-linux-build-evidence.mjs + if ($LASTEXITCODE -ne 0) { throw 'Linux runtime evidence script syntax check failed' } + node --check config/scripts/ssh-relay-runtime-linux-build-evidence.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'Linux runtime evidence test syntax check failed' } + node --check config/scripts/ssh-relay-linux-node-gyp-compiler.mjs + if ($LASTEXITCODE -ne 0) { throw 'Linux node-gyp compiler script syntax check failed' } + node --check config/scripts/ssh-relay-linux-node-gyp-compiler.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'Linux node-gyp compiler test syntax check failed' } node --check config/scripts/ssh-relay-runtime-windows-pe-diagnostic.mjs if ($LASTEXITCODE -ne 0) { throw 'Windows PE diagnostic script syntax check failed' } node --check config/scripts/ssh-relay-runtime-windows-pe-diagnostic.test.mjs @@ -311,12 +381,15 @@ jobs: 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 ` + config/scripts/ssh-relay-linux-node-gyp-compiler.test.mjs ` config/scripts/ssh-relay-node-pty-build.test.mjs ` config/scripts/ssh-relay-node-pty-windows-build-determinism.test.mjs ` config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs ` config/scripts/ssh-relay-node-zip-inspection.test.mjs ` config/scripts/ssh-relay-runtime-artifact.test.mjs ` + config/scripts/ssh-relay-runtime-baseline.test.mjs ` config/scripts/ssh-relay-runtime-build.test.mjs ` + config/scripts/ssh-relay-runtime-linux-build-evidence.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 ` @@ -441,3 +514,194 @@ jobs: path: runtime-evidence/${{ matrix.tuple }}/ if-no-files-found: error retention-days: 7 + + verify-linux-runtime-baseline-userland: + name: Supplement ${{ matrix.tuple }} oldest userland (kernel gap remains) + needs: build-posix-runtime + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-24.04 + tuple: linux-x64-glibc + container_image: docker.io/library/rockylinux@sha256:2d05a9266523bbf24f33ebc3a9832e4d5fd74b973c220f2204ca802286aa275d + - runner: ubuntu-24.04-arm + tuple: linux-arm64-glibc + container_image: docker.io/library/rockylinux@sha256:3c2d0ce12bf79fc5ff05e43b1000e30ff062dc89405525f3307cbff71661f1a0 + runs-on: ${{ matrix.runner }} + timeout-minutes: 15 + env: + ORCA_RUNTIME_REQUESTED_RUNNER: ${{ matrix.runner }} + + steps: + - name: Checkout exact source revision + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Setup pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6 + with: + run_install: false + + - name: Setup Node 24.18.0 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 24.18.0 + cache: pnpm + + - name: Record supplemental baseline runner identity + shell: bash + run: | + set -euo pipefail + printf 'requested_runner=%s\n' "$ORCA_RUNTIME_REQUESTED_RUNNER" + printf 'resolved_image_os=%s\n' "${ImageOS:-unknown}" + printf 'resolved_image_version=%s\n' "${ImageVersion:-unknown}" + printf 'runner_arch=%s\n' "$RUNNER_ARCH" + uname -a + + - name: Install verification dependencies without native postinstall + shell: bash + run: | + set -euo pipefail + pnpm install --frozen-lockfile --ignore-scripts + git diff --exit-code package.json pnpm-lock.yaml + + - name: Download exact unpublished runtime evidence + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ssh-relay-runtime-${{ matrix.tuple }} + path: runtime-evidence/${{ matrix.tuple }} + + - name: Verify bytes before supplemental baseline execution + shell: bash + run: | + set -euo pipefail + evidence="$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}" + mapfile -d '' identities < <(find "$evidence" -maxdepth 1 -type f -name '*.identity.json' -print0) + mapfile -d '' archives < <(find "$evidence" -maxdepth 1 -type f -name '*.tar.xz' -print0) + if (( ${#identities[@]} != 1 || ${#archives[@]} != 1 )); then + printf 'runtime baseline evidence is ambiguous: identities=%s archives=%s\n' \ + "${#identities[@]}" "${#archives[@]}" >&2 + exit 1 + fi + identity=${identities[0]} + archive=${archives[0]} + runtime="$RUNNER_TEMP/baseline-runtime/${{ matrix.tuple }}" + mkdir -p "$runtime" + tar -xJf "$archive" -C "$runtime" + node config/scripts/verify-ssh-relay-runtime.mjs \ + --runtime-directory "$runtime" \ + --identity "$identity" \ + --archive "$archive" + printf 'BASELINE_RUNTIME=%s\n' "$runtime" >> "$GITHUB_ENV" + + - name: Prove oldest Linux userland and retain the kernel gap + shell: bash + run: | + set -euo pipefail + image='${{ matrix.container_image }}' + docker pull "$image" + docker image inspect "$image" --format 'container_image={{index .RepoDigests 0}}' + docker run --rm --network none --read-only --cap-drop all \ + --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 2 \ + --tmpfs /tmp:rw,nosuid,size=64m \ + --mount "type=bind,src=$GITHUB_WORKSPACE,dst=/workspace,readonly" \ + --mount "type=bind,src=$BASELINE_RUNTIME,dst=/runtime,readonly" \ + --workdir /workspace \ + "$image" \ + /runtime/bin/node config/scripts/ssh-relay-runtime-smoke-child.cjs /runtime + # Why: the container proves glibc/libstdc++ floors but shares the newer hosted kernel. + docker run --rm --network none --read-only --cap-drop all \ + --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 2 \ + --tmpfs /tmp:rw,nosuid,size=64m \ + --mount "type=bind,src=$GITHUB_WORKSPACE,dst=/workspace,readonly" \ + --mount "type=bind,src=$BASELINE_RUNTIME,dst=/runtime,readonly" \ + --workdir /workspace \ + "$image" \ + /runtime/bin/node config/scripts/ssh-relay-runtime-baseline.mjs \ + --tuple '${{ matrix.tuple }}' \ + --scope linux-userland \ + --runtime-directory /runtime + + verify-windows-runtime-baseline: + name: Verify ${{ matrix.tuple }} oldest Windows baseline + needs: build-windows-runtime + strategy: + fail-fast: false + matrix: + include: + - runner: windows-2022 + tuple: win32-x64 + - runner: windows-11-arm + tuple: win32-arm64 + runs-on: ${{ matrix.runner }} + timeout-minutes: 15 + env: + ORCA_RUNTIME_REQUESTED_RUNNER: ${{ matrix.runner }} + + steps: + - name: Checkout exact source revision + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Setup pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6 + with: + run_install: false + + - name: Setup Node 24.18.0 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 24.18.0 + cache: pnpm + + - name: Record oldest-baseline runner identity + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + "requested_runner=$env:ORCA_RUNTIME_REQUESTED_RUNNER" + "resolved_image_os=$env:ImageOS" + "resolved_image_version=$env:ImageVersion" + "runner_arch=$env:RUNNER_ARCH" + Get-ComputerInfo | Select-Object WindowsProductName, WindowsVersion, OsBuildNumber + + - name: Install verification dependencies without native postinstall + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + pnpm install --frozen-lockfile --ignore-scripts + if ($LASTEXITCODE -ne 0) { throw 'pnpm install failed' } + git diff --exit-code package.json pnpm-lock.yaml + if ($LASTEXITCODE -ne 0) { throw 'frozen install changed dependency files' } + + - name: Download exact unpublished runtime evidence + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ssh-relay-runtime-${{ matrix.tuple }} + path: runtime-evidence/${{ matrix.tuple }} + + - name: Verify bytes and execute on the declared Windows floor + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $evidence = Join-Path $env:GITHUB_WORKSPACE 'runtime-evidence/${{ matrix.tuple }}' + $identities = @(Get-ChildItem -LiteralPath $evidence -Filter '*.identity.json' -File) + $archives = @(Get-ChildItem -LiteralPath $evidence -Filter '*.zip' -File) + if ($identities.Count -ne 1 -or $archives.Count -ne 1) { + throw "runtime baseline evidence is ambiguous: identities=$($identities.Count) archives=$($archives.Count)" + } + $identity = $identities[0] + $archive = $archives[0] + $runtime = Join-Path $env:RUNNER_TEMP 'baseline-runtime/${{ matrix.tuple }}' + Expand-Archive -LiteralPath $archive.FullName -DestinationPath $runtime + node config/scripts/verify-ssh-relay-runtime.mjs ` + --runtime-directory $runtime ` + --identity $identity.FullName ` + --archive $archive.FullName + if ($LASTEXITCODE -ne 0) { throw 'runtime baseline verification failed' } + node config/scripts/ssh-relay-runtime-baseline.mjs ` + --tuple '${{ matrix.tuple }}' ` + --scope full + if ($LASTEXITCODE -ne 0) { throw 'runner does not prove the declared Windows floor' } diff --git a/config/scripts/build-ssh-relay-runtime.mjs b/config/scripts/build-ssh-relay-runtime.mjs index 46906f11591..416f2ac0f70 100644 --- a/config/scripts/build-ssh-relay-runtime.mjs +++ b/config/scripts/build-ssh-relay-runtime.mjs @@ -6,6 +6,7 @@ import { promisify } from 'node:util' import { buildPatchedSshRelayNodePty } from './ssh-relay-node-pty-build.mjs' import { extractVerifiedSshRelayNodeBuildInputs } from './ssh-relay-node-tar-inspection.mjs' import { extractVerifiedSshRelayNodeZipBuildInputs } from './ssh-relay-node-zip-inspection.mjs' +import { applySshRelayLinuxNodeGypCompilerFloor } from './ssh-relay-linux-node-gyp-compiler.mjs' import { createSshRelayRuntimeArchive, inspectSshRelayRuntimeArchive @@ -192,6 +193,10 @@ export async function buildSshRelayRuntime(options) { if (bundledVersion !== `v${release.nodeVersion}`) { throw new Error(`Bundled Node version mismatch: ${bundledVersion}`) } + await applySshRelayLinuxNodeGypCompilerFloor({ + nodeRoot: extracted.extractedRoot, + tuple: options.tuple + }) const nodePty = await buildPatchedSshRelayNodePty({ projectRoot, nodePath: extracted.nodePath, diff --git a/config/scripts/ssh-relay-linux-node-gyp-compiler.mjs b/config/scripts/ssh-relay-linux-node-gyp-compiler.mjs new file mode 100644 index 00000000000..f841e7b967b --- /dev/null +++ b/config/scripts/ssh-relay-linux-node-gyp-compiler.mjs @@ -0,0 +1,20 @@ +import { readFile, writeFile } from 'node:fs/promises' +import { join } from 'node:path' + +const NODE_CXX20_FLAG = "'-std=gnu++20'," +const GCC_8_CXX20_FLAG = "'-std=gnu++2a'," + +export async function applySshRelayLinuxNodeGypCompilerFloor({ nodeRoot, tuple }) { + if (!tuple.startsWith('linux-')) { + return { changed: false } + } + const commonGypiPath = join(nodeRoot, 'include', 'node', 'common.gypi') + const source = await readFile(commonGypiPath, 'utf8') + const occurrences = source.split(NODE_CXX20_FLAG).length - 1 + if (occurrences !== 1) { + throw new Error(`Expected exactly one Node Linux C++20 compiler flag, found ${occurrences}`) + } + // Why: GCC 8 names its C++20 draft mode gnu++2a; this preserves the oldest libstdc++ floor. + await writeFile(commonGypiPath, source.replace(NODE_CXX20_FLAG, GCC_8_CXX20_FLAG)) + return { changed: true, commonGypiPath, standard: 'gnu++2a' } +} diff --git a/config/scripts/ssh-relay-linux-node-gyp-compiler.test.mjs b/config/scripts/ssh-relay-linux-node-gyp-compiler.test.mjs new file mode 100644 index 00000000000..f598e20d81e --- /dev/null +++ b/config/scripts/ssh-relay-linux-node-gyp-compiler.test.mjs @@ -0,0 +1,57 @@ +import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { applySshRelayLinuxNodeGypCompilerFloor } from './ssh-relay-linux-node-gyp-compiler.mjs' + +const fixtureDirectories = [] + +async function fixture(source) { + const nodeRoot = await mkdtemp(join(tmpdir(), 'orca-relay-node-gyp-')) + fixtureDirectories.push(nodeRoot) + const includeDirectory = join(nodeRoot, 'include', 'node') + await mkdir(includeDirectory, { recursive: true }) + await writeFile(join(includeDirectory, 'common.gypi'), source) + return nodeRoot +} + +afterEach(async () => { + await Promise.all(fixtureDirectories.splice(0).map((path) => rm(path, { recursive: true }))) +}) + +describe('SSH relay Linux node-gyp compiler floor', () => { + it('uses the GCC 8 spelling for exactly one official Node C++20 flag', async () => { + const nodeRoot = await fixture("'cflags_cc': [ '-std=gnu++20', ],\n") + + await expect( + applySshRelayLinuxNodeGypCompilerFloor({ nodeRoot, tuple: 'linux-x64-glibc' }) + ).resolves.toMatchObject({ changed: true, standard: 'gnu++2a' }) + await expect(readFile(join(nodeRoot, 'include', 'node', 'common.gypi'), 'utf8')).resolves.toBe( + "'cflags_cc': [ '-std=gnu++2a', ],\n" + ) + }) + + it('does not alter non-Linux Node build inputs', async () => { + const nodeRoot = await fixture("'cflags_cc': [ '-std=gnu++20', ],\n") + + await expect( + applySshRelayLinuxNodeGypCompilerFloor({ nodeRoot, tuple: 'darwin-x64' }) + ).resolves.toEqual({ changed: false }) + await expect(readFile(join(nodeRoot, 'include', 'node', 'common.gypi'), 'utf8')).resolves.toBe( + "'cflags_cc': [ '-std=gnu++20', ],\n" + ) + }) + + it.each([ + ['', 0], + ["'-std=gnu++20', '-std=gnu++20',", 2] + ])('fails closed when the official flag count is not one', async (source, count) => { + const nodeRoot = await fixture(source) + + await expect( + applySshRelayLinuxNodeGypCompilerFloor({ nodeRoot, tuple: 'linux-arm64-glibc' }) + ).rejects.toThrow(`Expected exactly one Node Linux C++20 compiler flag, found ${count}`) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-baseline.mjs b/config/scripts/ssh-relay-runtime-baseline.mjs new file mode 100644 index 00000000000..a41b48254e9 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-baseline.mjs @@ -0,0 +1,258 @@ +import { execFile } from 'node:child_process' +import { readdir, realpath } from 'node:fs/promises' +import { release } from 'node:os' +import { basename, join, resolve } from 'node:path' +import { promisify } from 'node:util' + +import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' + +const execFileAsync = promisify(execFile) +const COMMAND_TIMEOUT_MS = 30_000 +const COMMAND_OUTPUT_BYTES = 1024 * 1024 +const MAX_RUNTIME_ENTRIES = 100 +const MAX_RUNTIME_DEPTH = 8 + +const BASELINES = Object.freeze({ + 'linux-x64-glibc': { + platform: 'linux', + architecture: 'x64', + glibc: sshRelayRuntimeCompatibility['linux-x64-glibc'].libc.minimumVersion, + libstdcxx: sshRelayRuntimeCompatibility['linux-x64-glibc'].libc.minimumLibstdcxxVersion, + kernel: sshRelayRuntimeCompatibility['linux-x64-glibc'].minimumKernelVersion + }, + 'linux-arm64-glibc': { + platform: 'linux', + architecture: 'arm64', + glibc: sshRelayRuntimeCompatibility['linux-arm64-glibc'].libc.minimumVersion, + libstdcxx: sshRelayRuntimeCompatibility['linux-arm64-glibc'].libc.minimumLibstdcxxVersion, + kernel: sshRelayRuntimeCompatibility['linux-arm64-glibc'].minimumKernelVersion + }, + 'darwin-x64': { + platform: 'darwin', + architecture: 'x64', + osVersion: sshRelayRuntimeCompatibility['darwin-x64'].minimumVersion + }, + 'darwin-arm64': { + platform: 'darwin', + architecture: 'arm64', + osVersion: sshRelayRuntimeCompatibility['darwin-arm64'].minimumVersion + }, + 'win32-x64': { + platform: 'win32', + architecture: 'x64', + // Why: the reviewed x64 floor permits either Windows 10 22H2 or Server 2022. + osBuilds: [sshRelayRuntimeCompatibility['win32-x64'].minimumBuild, 20348] + }, + 'win32-arm64': { + platform: 'win32', + architecture: 'arm64', + osBuilds: [sshRelayRuntimeCompatibility['win32-arm64'].minimumBuild] + } +}) + +function majorMinor(value) { + const match = /^(\d+)\.(\d+)(?:\.|$)/.exec(value ?? '') + return match ? `${Number(match[1])}.${Number(match[2])}` : null +} + +function windowsBuild(value) { + const match = /^\d+\.\d+\.(\d+)(?:\.|$)/.exec(value ?? '') + return match ? Number(match[1]) : null +} + +export function evaluateSshRelayRuntimeBaseline({ + tuple, + scope = 'full', + platform, + architecture, + osVersion, + kernelVersion, + glibcVersion, + libstdcxxVersion +}) { + const contract = BASELINES[tuple] + if (!contract) { + throw new Error(`Unknown SSH relay runtime baseline tuple: ${tuple}`) + } + if (scope !== 'full' && !(scope === 'linux-userland' && contract.platform === 'linux')) { + throw new Error(`Unsupported SSH relay runtime baseline scope: ${scope}`) + } + const checks = { + platform: platform === contract.platform, + architecture: architecture === contract.architecture + } + // Why: execution on a newer host does not prove the declared oldest floor can run these bytes. + if (contract.platform === 'linux') { + checks.glibc = majorMinor(glibcVersion) === contract.glibc + checks.libstdcxx = libstdcxxVersion === contract.libstdcxx + checks.kernel = majorMinor(kernelVersion) === contract.kernel + } else if (contract.platform === 'darwin') { + checks.osVersion = majorMinor(osVersion) === contract.osVersion + } else { + checks.osBuild = contract.osBuilds.includes(windowsBuild(osVersion)) + } + const requiredChecks = + scope === 'linux-userland' + ? ['platform', 'architecture', 'glibc', 'libstdcxx'] + : Object.keys(checks) + const residualGaps = Object.keys(checks).filter((name) => !requiredChecks.includes(name)) + return { + tuple, + scope, + qualified: requiredChecks.every((name) => checks[name]), + residualGaps, + contract, + observed: { + platform, + architecture, + osVersion: osVersion ?? null, + kernelVersion: kernelVersion ?? null, + glibcVersion: glibcVersion ?? null, + libstdcxxVersion: libstdcxxVersion ?? null + }, + checks + } +} + +export function parseSshRelayRuntimeLddLibstdcxxPaths(output) { + if (typeof output !== 'string' || Buffer.byteLength(output) > COMMAND_OUTPUT_BYTES) { + throw new Error('Runtime baseline ldd output is missing or oversized') + } + if (/=>\s+not found|version\s+[`'][^`']+['`]\s+not found/i.test(output)) { + // Why: an exact userland version is irrelevant when the staged native module cannot load. + throw new Error('Runtime baseline ldd output contains an unresolved native dependency') + } + return output + .split(/\r?\n/) + .map((line) => /libstdc\+\+\.so\.6\s+=>\s+(\/\S+)/.exec(line)?.[1]) + .filter(Boolean) +} + +export function parseSshRelayRuntimeLibstdcxxVersion(libraryPaths) { + if (!Array.isArray(libraryPaths) || libraryPaths.length === 0) { + throw new Error('Runtime baseline did not resolve a libstdc++ ABI library') + } + const versions = new Set( + libraryPaths.map((path) => /^libstdc\+\+\.so\.(\d+\.\d+\.\d+)$/.exec(basename(path))?.[1]) + ) + if (versions.has(undefined) || versions.size !== 1) { + throw new Error('Runtime baseline did not resolve one bounded libstdc++ ABI library') + } + return versions.values().next().value +} + +async function runtimeNativeModules(runtimeDirectory) { + const modules = [] + let entries = 0 + async function visit(directory, depth) { + if (depth > MAX_RUNTIME_DEPTH) { + throw new Error('Runtime baseline tree exceeds the depth bound') + } + for (const entry of await readdir(directory, { withFileTypes: true })) { + entries += 1 + if (entries > MAX_RUNTIME_ENTRIES) { + throw new Error('Runtime baseline tree exceeds the entry bound') + } + const path = join(directory, entry.name) + if (entry.isDirectory()) { + await visit(path, depth + 1) + } else if (entry.isFile() && entry.name.endsWith('.node')) { + modules.push(path) + } else if (!entry.isFile()) { + throw new Error('Runtime baseline tree contains a special entry') + } + } + } + await visit(runtimeDirectory, 0) + if (modules.length === 0) { + throw new Error('Runtime baseline tree contains no native modules') + } + return modules +} + +async function linuxLibstdcxxVersion(runtimeDirectory) { + const libraryPaths = new Set() + for (const modulePath of await runtimeNativeModules(runtimeDirectory)) { + const { stdout, stderr } = await execFileAsync('ldd', [modulePath], { + encoding: 'utf8', + maxBuffer: COMMAND_OUTPUT_BYTES, + timeout: COMMAND_TIMEOUT_MS + }) + for (const path of parseSshRelayRuntimeLddLibstdcxxPaths(`${stderr}\n${stdout}`)) { + libraryPaths.add(await realpath(path)) + } + } + return parseSshRelayRuntimeLibstdcxxVersion([...libraryPaths]) +} + +async function collectObservedBaseline({ tuple, runtimeDirectory }) { + const contract = BASELINES[tuple] + if (!contract) { + throw new Error(`Unknown SSH relay runtime baseline tuple: ${tuple}`) + } + const observed = { + platform: process.platform, + architecture: process.arch, + kernelVersion: release() + } + if (contract.platform === 'linux') { + if (!runtimeDirectory) { + throw new Error('--runtime-directory is required for a Linux baseline') + } + observed.glibcVersion = process.report.getReport().header.glibcVersionRuntime + observed.libstdcxxVersion = await linuxLibstdcxxVersion(runtimeDirectory) + } else if (contract.platform === 'darwin') { + const { stdout } = await execFileAsync('sw_vers', ['-productVersion'], { + encoding: 'utf8', + maxBuffer: COMMAND_OUTPUT_BYTES, + timeout: COMMAND_TIMEOUT_MS + }) + observed.osVersion = stdout.trim() + } else { + observed.osVersion = release() + } + return observed +} + +function parseArguments(argv) { + const result = { scope: 'full' } + for (let index = 0; index < argv.length; index += 2) { + const flag = argv[index] + const value = argv[index + 1] + if (!value || value.startsWith('--')) { + throw new Error(`${flag} requires a value`) + } + if (flag === '--tuple') { + result.tuple = value + } else if (flag === '--scope') { + result.scope = value + } else if (flag === '--runtime-directory') { + result.runtimeDirectory = resolve(value) + } else { + throw new Error(`Unknown argument: ${flag}`) + } + } + if (!result.tuple) { + throw new Error('--tuple is required') + } + return result +} + +async function main() { + const options = parseArguments(process.argv.slice(2)) + const result = evaluateSshRelayRuntimeBaseline({ + ...options, + ...(await collectObservedBaseline(options)) + }) + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`) + if (!result.qualified) { + process.exitCode = 1 + } +} + +if (process.argv[1] && resolve(process.argv[1]) === import.meta.filename) { + main().catch((error) => { + process.stderr.write(`SSH relay runtime baseline verification failed: ${error.message}\n`) + process.exitCode = 1 + }) +} diff --git a/config/scripts/ssh-relay-runtime-baseline.test.mjs b/config/scripts/ssh-relay-runtime-baseline.test.mjs new file mode 100644 index 00000000000..89e60ad3fcd --- /dev/null +++ b/config/scripts/ssh-relay-runtime-baseline.test.mjs @@ -0,0 +1,149 @@ +import { describe, expect, it } from 'vitest' + +import { + evaluateSshRelayRuntimeBaseline, + parseSshRelayRuntimeLibstdcxxVersion, + parseSshRelayRuntimeLddLibstdcxxPaths +} from './ssh-relay-runtime-baseline.mjs' + +describe('SSH relay runtime oldest-baseline evidence', () => { + it('qualifies only the exact Linux userland and kernel floors', () => { + const floor = { + tuple: 'linux-x64-glibc', + platform: 'linux', + architecture: 'x64', + glibcVersion: '2.28', + libstdcxxVersion: '6.0.25', + kernelVersion: '4.18.0-553.el8_10.x86_64' + } + expect(evaluateSshRelayRuntimeBaseline(floor)).toMatchObject({ + qualified: true, + checks: { glibc: true, libstdcxx: true, kernel: true } + }) + expect(evaluateSshRelayRuntimeBaseline({ ...floor, glibcVersion: '2.29' }).qualified).toBe( + false + ) + expect( + evaluateSshRelayRuntimeBaseline({ ...floor, libstdcxxVersion: '6.0.26' }).qualified + ).toBe(false) + expect(evaluateSshRelayRuntimeBaseline({ ...floor, kernelVersion: '5.15.0' }).qualified).toBe( + false + ) + }) + + it('records a newer-container kernel as an explicit Linux userland residual gap', () => { + const result = evaluateSshRelayRuntimeBaseline({ + tuple: 'linux-arm64-glibc', + scope: 'linux-userland', + platform: 'linux', + architecture: 'arm64', + glibcVersion: '2.28', + libstdcxxVersion: '6.0.25', + kernelVersion: '6.11.0' + }) + expect(result).toMatchObject({ + qualified: true, + residualGaps: ['kernel'], + checks: { kernel: false } + }) + }) + + it('requires macOS 13.5 on the tuple-native architecture', () => { + expect( + evaluateSshRelayRuntimeBaseline({ + tuple: 'darwin-arm64', + platform: 'darwin', + architecture: 'arm64', + osVersion: '13.5.2' + }).qualified + ).toBe(true) + expect( + evaluateSshRelayRuntimeBaseline({ + tuple: 'darwin-arm64', + platform: 'darwin', + architecture: 'arm64', + osVersion: '13.6.0' + }).qualified + ).toBe(false) + }) + + it('accepts only the declared Windows base-build floors', () => { + for (const osVersion of ['10.0.19045.0', '10.0.20348.0']) { + expect( + evaluateSshRelayRuntimeBaseline({ + tuple: 'win32-x64', + platform: 'win32', + architecture: 'x64', + osVersion + }).qualified + ).toBe(true) + } + expect( + evaluateSshRelayRuntimeBaseline({ + tuple: 'win32-arm64', + platform: 'win32', + architecture: 'arm64', + osVersion: '10.0.26100.1' + }).qualified + ).toBe(true) + expect( + evaluateSshRelayRuntimeBaseline({ + tuple: 'win32-x64', + platform: 'win32', + architecture: 'x64', + osVersion: '10.0.22621.0' + }).qualified + ).toBe(false) + }) + + it('rejects tuple, architecture, and scope substitutions', () => { + expect(() => + evaluateSshRelayRuntimeBaseline({ tuple: 'linux-x64-musl', scope: 'full' }) + ).toThrow(/unknown/i) + expect(() => + evaluateSshRelayRuntimeBaseline({ tuple: 'win32-x64', scope: 'linux-userland' }) + ).toThrow(/scope/i) + expect( + evaluateSshRelayRuntimeBaseline({ + tuple: 'win32-x64', + platform: 'win32', + architecture: 'arm64', + osVersion: '10.0.20348.0' + }).qualified + ).toBe(false) + }) + + it('extracts only resolved absolute libstdc++ paths from bounded ldd output', () => { + expect( + parseSshRelayRuntimeLddLibstdcxxPaths( + [ + 'linux-vdso.so.1 (0x0000ffff)', + 'libstdc++.so.6 => /lib64/libstdc++.so.6 (0x0000ffff)', + 'libm.so.6 => /lib64/libm.so.6 (0x0000ffff)' + ].join('\n') + ) + ).toEqual(['/lib64/libstdc++.so.6']) + expect(() => parseSshRelayRuntimeLddLibstdcxxPaths('x'.repeat(1024 * 1024 + 1))).toThrow( + /oversized/ + ) + for (const output of [ + 'libstdc++.so.6 => not found', + "/runtime/pty.node: /lib64/libc.so.6: version `GLIBC_2.34' not found" + ]) { + expect(() => parseSshRelayRuntimeLddLibstdcxxPaths(output)).toThrow(/unresolved/i) + } + }) + + it('retains the libstdc++ SONAME major and rejects ambiguous filenames', () => { + expect(parseSshRelayRuntimeLibstdcxxVersion(['/usr/lib64/libstdc++.so.6.0.25'])).toBe('6.0.25') + expect(() => + parseSshRelayRuntimeLibstdcxxVersion([ + '/usr/lib64/libstdc++.so.6.0.25', + '/opt/lib/libstdc++.so.6.0.26' + ]) + ).toThrow(/one bounded/i) + expect(() => parseSshRelayRuntimeLibstdcxxVersion(['/usr/lib64/libstdc++.so.6'])).toThrow( + /one bounded/i + ) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-compatibility.mjs b/config/scripts/ssh-relay-runtime-compatibility.mjs new file mode 100644 index 00000000000..2645a816a18 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-compatibility.mjs @@ -0,0 +1,38 @@ +export const sshRelayRuntimeCompatibility = Object.freeze({ + 'linux-x64-glibc': { + kind: 'linux', + minimumKernelVersion: '4.18', + libc: { + family: 'glibc', + minimumVersion: '2.28', + minimumLibstdcxxVersion: '6.0.25', + minimumGlibcxxVersion: '3.4.25' + } + }, + 'linux-arm64-glibc': { + kind: 'linux', + minimumKernelVersion: '4.18', + libc: { + family: 'glibc', + minimumVersion: '2.28', + minimumLibstdcxxVersion: '6.0.25', + minimumGlibcxxVersion: '3.4.25' + } + }, + 'darwin-x64': { kind: 'darwin', minimumVersion: '13.5' }, + 'darwin-arm64': { kind: 'darwin', minimumVersion: '13.5' }, + 'win32-x64': { + kind: 'win32', + minimumBuild: 19045, + minimumOpenSshVersion: '8.1p1', + minimumPowerShellVersion: '5.1', + minimumDotNetFrameworkRelease: 528040 + }, + 'win32-arm64': { + kind: 'win32', + minimumBuild: 26100, + minimumOpenSshVersion: '8.1p1', + minimumPowerShellVersion: '5.1', + minimumDotNetFrameworkRelease: 528040 + } +}) diff --git a/config/scripts/ssh-relay-runtime-linux-build-evidence.mjs b/config/scripts/ssh-relay-runtime-linux-build-evidence.mjs new file mode 100644 index 00000000000..5def791b8bd --- /dev/null +++ b/config/scripts/ssh-relay-runtime-linux-build-evidence.mjs @@ -0,0 +1,161 @@ +import { constants } from 'node:fs' +import { copyFile, mkdir } from 'node:fs/promises' +import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path' + +import { buildSshRelayRuntime } from './build-ssh-relay-runtime.mjs' +import { verifySshRelayRuntime } from './verify-ssh-relay-runtime.mjs' +import { verifySshRelayRuntimeReproducibility } from './ssh-relay-runtime-reproducibility.mjs' + +const scriptDirectory = import.meta.dirname +const defaultContractPath = resolve(scriptDirectory, '..', 'ssh-relay-node-release-v24.18.0.json') +const SUPPORTED_TUPLES = new Set(['linux-x64-glibc', 'linux-arm64-glibc']) + +function valueAfter(argv, index, flag) { + const value = argv[index + 1] + if (!value || value.startsWith('--')) { + throw new Error(`${flag} requires a value`) + } + return value +} + +export function parseSshRelayRuntimeLinuxBuildEvidenceArguments(argv) { + const result = { contractPath: defaultContractPath } + for (let index = 0; index < argv.length; index += 1) { + const flag = argv[index] + const value = valueAfter(argv, index, flag) + if (flag === '--tuple') { + result.tuple = value + } else if (flag === '--inputs-directory') { + result.inputsDirectory = resolve(value) + } else if (flag === '--output-root') { + result.outputRoot = resolve(value) + } else if (flag === '--work-directory') { + result.workDirectory = resolve(value) + } else if (flag === '--evidence-directory') { + result.evidenceDirectory = resolve(value) + } else if (flag === '--contract') { + result.contractPath = resolve(value) + } else if (flag === '--source-date-epoch') { + result.sourceDateEpoch = Number(value) + } else if (flag === '--git-commit') { + result.gitCommit = value + } else { + throw new Error(`Unknown Linux runtime evidence argument: ${flag}`) + } + index += 1 + } + for (const field of [ + 'tuple', + 'inputsDirectory', + 'outputRoot', + 'workDirectory', + 'evidenceDirectory', + 'sourceDateEpoch', + 'gitCommit' + ]) { + if (result[field] === undefined) { + throw new Error(`Missing required Linux runtime evidence argument: ${field}`) + } + } + if (!SUPPORTED_TUPLES.has(result.tuple)) { + throw new Error(`Unsupported Linux runtime evidence tuple: ${result.tuple}`) + } + if (!Number.isSafeInteger(result.sourceDateEpoch) || result.sourceDateEpoch < 0) { + throw new Error('--source-date-epoch must be a non-negative safe integer') + } + if (!/^[0-9a-f]{40}$/.test(result.gitCommit)) { + throw new Error('--git-commit must be a full lowercase SHA-1') + } + assertSshRelayRuntimeLinuxEvidenceDirectories(result) + return result +} + +function containsPath(parent, candidate) { + const path = relative(parent, candidate) + return path === '' || (path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path)) +} + +export function assertSshRelayRuntimeLinuxEvidenceDirectories({ + outputRoot, + workDirectory, + evidenceDirectory +}) { + const directories = [outputRoot, workDirectory, evidenceDirectory] + for (let left = 0; left < directories.length; left += 1) { + for (let right = left + 1; right < directories.length; right += 1) { + if ( + containsPath(directories[left], directories[right]) || + containsPath(directories[right], directories[left]) + ) { + throw new Error('Linux runtime evidence directories must be pairwise disjoint') + } + } + } +} + +async function createExclusiveDirectory(path) { + await mkdir(dirname(path), { recursive: true }) + await mkdir(path) +} + +async function buildAndVerify(options, label) { + const outputDirectory = join(options.outputRoot, label) + const build = await buildSshRelayRuntime({ + tuple: options.tuple, + inputsDirectory: options.inputsDirectory, + outputDirectory, + workDirectory: options.workDirectory, + contractPath: options.contractPath, + sourceDateEpoch: options.sourceDateEpoch, + gitCommit: options.gitCommit + }) + const verification = await verifySshRelayRuntime({ + runtimeDirectory: join(outputDirectory, 'runtime'), + identityPath: join(outputDirectory, build.metadata.identity.name), + archivePath: build.archive.path + }) + return { label, outputDirectory, build, verification } +} + +export async function buildSshRelayRuntimeLinuxEvidence(options) { + assertSshRelayRuntimeLinuxEvidenceDirectories(options) + await createExclusiveDirectory(options.outputRoot) + // Why: both native builds use the same canonical path but the builder removes it between runs. + const first = await buildAndVerify(options, 'first') + const second = await buildAndVerify(options, 'second') + const reproducibility = await verifySshRelayRuntimeReproducibility({ + tuple: options.tuple, + firstOutputDirectory: first.outputDirectory, + secondOutputDirectory: second.outputDirectory + }) + await createExclusiveDirectory(options.evidenceDirectory) + for (const asset of [ + first.build.archive, + first.build.metadata.identity, + first.build.metadata.sbom, + first.build.metadata.provenance + ]) { + const source = asset.path ?? join(first.outputDirectory, asset.name) + await copyFile(source, join(options.evidenceDirectory, asset.name), constants.COPYFILE_EXCL) + } + return { + tuple: options.tuple, + first: { build: first.build, verification: first.verification }, + second: { build: second.build, verification: second.verification }, + reproducibility + } +} + +async function main() { + const result = await buildSshRelayRuntimeLinuxEvidence( + parseSshRelayRuntimeLinuxBuildEvidenceArguments(process.argv.slice(2)) + ) + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`) +} + +if (process.argv[1] && resolve(process.argv[1]) === import.meta.filename) { + main().catch((error) => { + process.stderr.write(`SSH relay Linux runtime evidence build failed: ${error.stack ?? error}\n`) + process.exitCode = 1 + }) +} diff --git a/config/scripts/ssh-relay-runtime-linux-build-evidence.test.mjs b/config/scripts/ssh-relay-runtime-linux-build-evidence.test.mjs new file mode 100644 index 00000000000..c6abb42cb49 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-linux-build-evidence.test.mjs @@ -0,0 +1,79 @@ +import { describe, expect, it } from 'vitest' + +import { + assertSshRelayRuntimeLinuxEvidenceDirectories, + parseSshRelayRuntimeLinuxBuildEvidenceArguments +} from './ssh-relay-runtime-linux-build-evidence.mjs' + +const commit = 'a'.repeat(40) + +function argumentsFor(overrides = {}) { + const values = { + tuple: 'linux-x64-glibc', + inputsDirectory: '/tmp/inputs', + outputRoot: '/tmp/outputs', + workDirectory: '/tmp/work', + evidenceDirectory: '/tmp/evidence', + sourceDateEpoch: '123', + gitCommit: commit, + ...overrides + } + return [ + '--tuple', + values.tuple, + '--inputs-directory', + values.inputsDirectory, + '--output-root', + values.outputRoot, + '--work-directory', + values.workDirectory, + '--evidence-directory', + values.evidenceDirectory, + '--source-date-epoch', + values.sourceDateEpoch, + '--git-commit', + values.gitCommit + ] +} + +describe('SSH relay Linux runtime evidence build', () => { + it('accepts only the two declared native glibc tuples', () => { + for (const tuple of ['linux-x64-glibc', 'linux-arm64-glibc']) { + expect( + parseSshRelayRuntimeLinuxBuildEvidenceArguments(argumentsFor({ tuple })) + ).toMatchObject({ tuple, sourceDateEpoch: 123, gitCommit: commit }) + } + expect(() => + parseSshRelayRuntimeLinuxBuildEvidenceArguments(argumentsFor({ tuple: 'darwin-x64' })) + ).toThrow(/unsupported/i) + }) + + it('requires a full commit, safe epoch, and known flags', () => { + expect(() => + parseSshRelayRuntimeLinuxBuildEvidenceArguments(argumentsFor({ gitCommit: 'abc' })) + ).toThrow(/sha-1/i) + expect(() => + parseSshRelayRuntimeLinuxBuildEvidenceArguments(argumentsFor({ sourceDateEpoch: '-1' })) + ).toThrow(/epoch/i) + expect(() => + parseSshRelayRuntimeLinuxBuildEvidenceArguments([...argumentsFor(), '--unexpected', 'x']) + ).toThrow(/unknown/i) + }) + + it('keeps clean outputs, transient work, and uploaded evidence disjoint', () => { + expect(() => + assertSshRelayRuntimeLinuxEvidenceDirectories({ + outputRoot: '/tmp/build', + workDirectory: '/tmp/build/work', + evidenceDirectory: '/tmp/evidence' + }) + ).toThrow(/pairwise disjoint/i) + expect(() => + assertSshRelayRuntimeLinuxEvidenceDirectories({ + outputRoot: '/tmp/output', + workDirectory: '/tmp/work', + evidenceDirectory: '/tmp/evidence' + }) + ).not.toThrow() + }) +}) diff --git a/config/scripts/ssh-relay-runtime-toolchain.mjs b/config/scripts/ssh-relay-runtime-toolchain.mjs index afee29e33a7..4881c956843 100644 --- a/config/scripts/ssh-relay-runtime-toolchain.mjs +++ b/config/scripts/ssh-relay-runtime-toolchain.mjs @@ -55,6 +55,14 @@ export function selectSshRelayRuntimeToolVersion({ stdout = '', stderr = '' }, p return version } +export function sshRelayRuntimePythonCommand(platform, env = process.env) { + return platform === 'linux' && env.NODE_GYP_FORCE_PYTHON + ? env.NODE_GYP_FORCE_PYTHON + : platform === 'win32' + ? 'python.exe' + : 'python3' +} + async function locateExecutable(command, { windowsMsvcLinker = false, xcrun = false } = {}) { if (isAbsolute(command)) { return realpath(command) @@ -303,7 +311,10 @@ export async function collectSshRelayRuntimeToolchain(nodePath) { xcrun: process.platform === 'darwin' }), buildSystem: await executableRecord({ command: 'make', args: ['--version'] }), - python: await executableRecord({ command: 'python3', args: ['--version'] }), + python: await executableRecord({ + command: sshRelayRuntimePythonCommand(process.platform), + args: ['--version'] + }), archive: await executableRecord({ command: 'xz', args: ['--version'] }), strip: await executableRecord({ command: 'strip', diff --git a/config/scripts/ssh-relay-runtime-toolchain.test.mjs b/config/scripts/ssh-relay-runtime-toolchain.test.mjs index a366104408c..19d0291b1f9 100644 --- a/config/scripts/ssh-relay-runtime-toolchain.test.mjs +++ b/config/scripts/ssh-relay-runtime-toolchain.test.mjs @@ -7,6 +7,7 @@ import { selectSshRelayRuntimeWindowsMsvcLinker, sshRelayRuntimeBuilderIdentity, sshRelayRuntimeRunnerIdentity, + sshRelayRuntimePythonCommand, sshRelayRuntimeStripVersionProbe, sshRelayRuntimeWindowsMsvcToolsetVersion } from './ssh-relay-runtime-toolchain.mjs' @@ -150,6 +151,16 @@ describe('SSH relay runtime build provenance', () => { }) }) + it('records the exact Python forced into Linux node-gyp', () => { + expect( + sshRelayRuntimePythonCommand('linux', { + NODE_GYP_FORCE_PYTHON: '/usr/bin/python3.9' + }) + ).toBe('/usr/bin/python3.9') + expect(sshRelayRuntimePythonCommand('darwin', {})).toBe('python3') + expect(sshRelayRuntimePythonCommand('win32', {})).toBe('python.exe') + }) + it('records bounded native tool versions and SHA-256 executable or code digests', async () => { const toolchain = await collectSshRelayRuntimeToolchain(process.execPath) const tuple = process.platform === 'win32' ? 'win32-x64' : 'linux-x64-glibc' diff --git a/config/scripts/ssh-relay-runtime-tree.mjs b/config/scripts/ssh-relay-runtime-tree.mjs index 1316f81ef08..95595096098 100644 --- a/config/scripts/ssh-relay-runtime-tree.mjs +++ b/config/scripts/ssh-relay-runtime-tree.mjs @@ -9,6 +9,7 @@ import { sshRelayRuntimeWatcherPackage, verifySshRelayRuntimeClosure } from './ssh-relay-runtime-closure.mjs' +import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' const require = createRequire(import.meta.url) const MAX_FILES = 5_000 @@ -19,45 +20,6 @@ const MAX_PATH_DEPTH = 32 const PORTABLE_PATH = /^[A-Za-z0-9._@+/-]+$/ const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i -const COMPATIBILITY = Object.freeze({ - 'linux-x64-glibc': { - kind: 'linux', - minimumKernelVersion: '4.18', - libc: { - family: 'glibc', - minimumVersion: '2.28', - minimumLibstdcxxVersion: '6.0.25', - minimumGlibcxxVersion: '3.4.25' - } - }, - 'linux-arm64-glibc': { - kind: 'linux', - minimumKernelVersion: '4.18', - libc: { - family: 'glibc', - minimumVersion: '2.28', - minimumLibstdcxxVersion: '6.0.25', - minimumGlibcxxVersion: '3.4.25' - } - }, - 'darwin-x64': { kind: 'darwin', minimumVersion: '13.5' }, - 'darwin-arm64': { kind: 'darwin', minimumVersion: '13.5' }, - 'win32-x64': { - kind: 'win32', - minimumBuild: 19045, - minimumOpenSshVersion: '8.1p1', - minimumPowerShellVersion: '5.1', - minimumDotNetFrameworkRelease: 528040 - }, - 'win32-arm64': { - kind: 'win32', - minimumBuild: 26100, - minimumOpenSshVersion: '8.1p1', - minimumPowerShellVersion: '5.1', - minimumDotNetFrameworkRelease: 528040 - } -}) - function packageDirectory(name) { return dirname(require.resolve(`${name}/package.json`)) } @@ -304,7 +266,7 @@ export async function assembleSshRelayRuntimeTree({ runtimeRoot, nodeVersion }) { - const compatibility = COMPATIBILITY[tuple] + const compatibility = sshRelayRuntimeCompatibility[tuple] if (!compatibility) { throw new Error(`Runtime tree assembly is not implemented for ${tuple}`) } diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 91c351c51fb..d799830a257 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -7,6 +7,7 @@ const workflowUrl = new URL( '../../.github/workflows/ssh-relay-runtime-artifacts.yml', import.meta.url ) +const linuxBuilderUrl = new URL('../ssh-relay-runtime-linux-builder.Containerfile', import.meta.url) describe('SSH relay runtime artifact workflow', () => { it('uses exact native runner labels and SHA-pinned actions without publication authority', async () => { @@ -20,6 +21,12 @@ describe('SSH relay runtime artifact workflow', () => { ['macos-15-intel', 'darwin-x64'], ['macos-15', 'darwin-arm64'] ]) + expect( + posixJob.strategy.matrix.include.slice(0, 2).map((entry) => entry.container_image) + ).toEqual([ + 'docker.io/library/rockylinux@sha256:2d05a9266523bbf24f33ebc3a9832e4d5fd74b973c220f2204ca802286aa275d', + 'docker.io/library/rockylinux@sha256:3c2d0ce12bf79fc5ff05e43b1000e30ff062dc89405525f3307cbff71661f1a0' + ]) expect(windowsJob.strategy.matrix.include.map((entry) => [entry.runner, entry.tuple])).toEqual([ ['windows-2022', 'win32-x64'], ['windows-11-arm', 'win32-arm64'] @@ -85,7 +92,7 @@ describe('SSH relay runtime artifact workflow', () => { expect(source.match(/ssh-relay-node-pty-build\.test\.mjs/g)).toHaveLength(2) expect(source.match(/ssh-relay-node-pty-windows-build-determinism\.test\.mjs/g)).toHaveLength(2) expect(source.match(/ssh-relay-runtime-build\.test\.mjs/g)).toHaveLength(4) - expect(source.match(/--work-directory/g)).toHaveLength(2) + expect(source.match(/--work-directory/g)).toHaveLength(3) expect(source).toContain('work_directory="$RUNNER_TEMP/orca-ssh-relay-runtime-build-work"') expect(source).toContain( "$workDirectory = Join-Path $env:RUNNER_TEMP 'orca-ssh-relay-runtime-build-work'" @@ -108,7 +115,7 @@ describe('SSH relay runtime artifact workflow', () => { expect(run.indexOf('ssh-relay-runtime-reproducibility.mjs')).toBeGreaterThan( run.indexOf('verify-ssh-relay-runtime.mjs') ) - expect(run.indexOf('runtime-evidence/${{ matrix.tuple }}')).toBeGreaterThan( + expect(run.lastIndexOf('runtime-evidence/${{ matrix.tuple }}')).toBeGreaterThan( run.indexOf('ssh-relay-runtime-reproducibility.mjs') ) } @@ -130,4 +137,101 @@ describe('SSH relay runtime artifact workflow', () => { expect(steps[uploadIndex].with.path).toBe('runtime-evidence/${{ matrix.tuple }}/') expect(source).not.toMatch(/releases\/|gh release|contents:\s*write/i) }) + + it('builds Linux native modules in the pinned oldest userland with an offline build phase', async () => { + const source = await readFile(workflowUrl, 'utf8') + const workflow = parse(source) + const job = workflow.jobs['build-posix-runtime'] + const prepare = job.steps.find( + (step) => step.name === 'Prepare digest-pinned Linux floor builder' + ) + const build = job.steps.find( + (step) => step.name === 'Build twice, inspect, smoke, and compare exact runtime' + ) + const containerfile = await readFile(linuxBuilderUrl, 'utf8') + + expect(prepare.if).toBe("runner.os == 'Linux'") + expect(prepare.run).toContain('docker pull "$image"') + expect(prepare.run).toContain('--pull=false') + expect(prepare.run).toContain('config/ssh-relay-runtime-linux-builder.Containerfile') + expect(build.run).toContain("if [[ '${{ matrix.tuple }}' == linux-* ]]") + expect(build.run).toContain('--network none --read-only --cap-drop all') + expect(build.run).toContain('--security-opt no-new-privileges') + expect(build.run).toContain('ssh-relay-runtime-linux-build-evidence.mjs') + expect(build.run.indexOf('--network none')).toBeLessThan( + build.run.indexOf('ssh-relay-runtime-linux-build-evidence.mjs') + ) + expect(containerfile).toContain('ARG BASE_IMAGE=scratch\nFROM ${BASE_IMAGE}') + expect(containerfile).toContain("getconf GNU_LIBC_VERSION)\" = 'glibc 2.28'") + expect(containerfile).toContain('libstdc++.so.6.0.25') + expect(containerfile).toContain('dnf module enable -y -q nodejs:20') + expect(containerfile).toContain('Number(process.versions.node.split') + expect(containerfile).toContain('python39') + expect(containerfile).toContain('NODE_GYP_FORCE_PYTHON=/usr/bin/python3.9') + expect(containerfile).toContain(' which \\\n') + expect(source).not.toMatch(/releases\/|gh release|contents:\s*write/i) + }) + + it('separates qualifying Windows floors from supplemental Linux userland evidence', async () => { + const source = await readFile(workflowUrl, 'utf8') + const workflow = parse(source) + const linuxJob = workflow.jobs['verify-linux-runtime-baseline-userland'] + const windowsJob = workflow.jobs['verify-windows-runtime-baseline'] + + expect(linuxJob.needs).toBe('build-posix-runtime') + expect(windowsJob.needs).toBe('build-windows-runtime') + expect(linuxJob.strategy.matrix.include).toEqual([ + { + runner: 'ubuntu-24.04', + tuple: 'linux-x64-glibc', + container_image: + 'docker.io/library/rockylinux@sha256:2d05a9266523bbf24f33ebc3a9832e4d5fd74b973c220f2204ca802286aa275d' + }, + { + runner: 'ubuntu-24.04-arm', + tuple: 'linux-arm64-glibc', + container_image: + 'docker.io/library/rockylinux@sha256:3c2d0ce12bf79fc5ff05e43b1000e30ff062dc89405525f3307cbff71661f1a0' + } + ]) + expect(windowsJob.strategy.matrix.include).toEqual([ + { runner: 'windows-2022', tuple: 'win32-x64' }, + { runner: 'windows-11-arm', tuple: 'win32-arm64' } + ]) + for (const job of [linuxJob, windowsJob]) { + expect(job.env.ORCA_RUNTIME_REQUESTED_RUNNER).toBe('${{ matrix.runner }}') + expect(job['timeout-minutes']).toBe(15) + expect(job.steps[0].with.ref).toBe('${{ github.event.pull_request.head.sha || github.sha }}') + for (const step of job.steps.filter((candidate) => candidate.uses)) { + expect(step.uses).toMatch(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+@[0-9a-f]{40}$/) + } + } + + const linuxRun = linuxJob.steps.find( + (step) => step.name === 'Prove oldest Linux userland and retain the kernel gap' + ).run + expect(linuxRun).toContain('--scope linux-userland') + expect(linuxRun).toContain('--network none') + expect(linuxRun).toContain('--read-only --cap-drop all') + expect(linuxRun).toContain('--security-opt no-new-privileges') + expect(linuxRun).toContain('ssh-relay-runtime-smoke-child.cjs') + expect(linuxRun).not.toContain('--scope full') + + const linuxVerification = linuxJob.steps.find( + (step) => step.name === 'Verify bytes before supplemental baseline execution' + ).run + expect(linuxVerification).toContain('${#identities[@]} != 1') + expect(linuxVerification).toContain('${#archives[@]} != 1') + + const windowsRun = windowsJob.steps.find( + (step) => step.name === 'Verify bytes and execute on the declared Windows floor' + ).run + expect(windowsRun.indexOf('verify-ssh-relay-runtime.mjs')).toBeLessThan( + windowsRun.indexOf('ssh-relay-runtime-baseline.mjs') + ) + expect(windowsRun).toContain('--scope full') + expect(windowsRun).toContain('$identities.Count -ne 1') + expect(windowsRun).toContain('$archives.Count -ne 1') + expect(source).not.toMatch(/releases\/|gh release|contents:\s*write/i) + }) }) diff --git a/config/ssh-relay-runtime-linux-builder.Containerfile b/config/ssh-relay-runtime-linux-builder.Containerfile new file mode 100644 index 00000000000..bb4dbe41768 --- /dev/null +++ b/config/ssh-relay-runtime-linux-builder.Containerfile @@ -0,0 +1,28 @@ +ARG BASE_IMAGE=scratch +FROM ${BASE_IMAGE} + +# Why: Rocky 8's default Python 3.6 cannot parse node-gyp 12; Python 3.9 keeps the glibc floor. +ENV NODE_GYP_FORCE_PYTHON=/usr/bin/python3.9 + +RUN set -eu; \ + dnf module enable -y -q nodejs:20; \ + dnf install -y -q \ + binutils \ + ca-certificates \ + curl \ + findutils \ + gcc-c++ \ + git \ + gnupg2 \ + make \ + nodejs \ + python39 \ + time \ + which \ + xz; \ + test "$(getconf GNU_LIBC_VERSION)" = 'glibc 2.28'; \ + test "$(basename "$(readlink -f /usr/lib64/libstdc++.so.6)")" = 'libstdc++.so.6.0.25'; \ + node -e "if (Number(process.versions.node.split('.')[0]) !== 20) process.exit(1)"; \ + python3.9 -c "import sys; raise SystemExit(sys.version_info[:2] != (3, 9))"; \ + dnf clean all; \ + rm -rf /var/cache/dnf From 3973e68d772008a20c2664ad90708f6d0e00b403 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 20:01:11 -0400 Subject: [PATCH 075/267] docs(ssh): record Linux floor evidence Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 11 +- ...github-release-implementation-checklist.md | 321 +++++++++++++++++- ...6-07-14-ssh-relay-github-release-plan.html | 25 +- 3 files changed, 343 insertions(+), 14 deletions(-) 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 33201f518e3..e3298df4ece 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 @@ -21,14 +21,19 @@ complete a box. ## Active work - [ ] **WP2 — Prove oldest supported baselines and native trust.** + - Session checkpoint: a local x64 Rocky 8 build now passes two complete offline builds, exact + equality, archive/tree verification, PTY/watcher smoke, and the glibc 2.28/libstdc++ 6.0.25 + userland verifier. This is supplemental local emulation evidence, not a qualifying native cell. + - Local focused tests pass 100/100; typecheck, lint, max-lines, formatting, and diff checks pass. + - Artifact-only implementation commit: `0cb3f7510` (`build(ssh): prove Linux runtime floor`). - All six target-native artifact, smoke, equality, SBOM, license, provenance, toolchain, and prohibited-content cells pass in [run 29373507297](https://github.com/stablyai/orca/actions/runs/29373507297); every downloaded payload passed direct audit. - No tuple is enabled: the artifacts still need execution on each declared oldest OS/libc/kernel baseline and target-native signing/trust proof. - - Next gate: add the purpose-named baseline verification capability and GitHub-runner cells that - can qualify, while keeping unavailable snapshot/trust cells explicit and disabled. + - Next gate: run the corrected builder on native GitHub x64/arm64 runners. Kernel 4.18, macOS + 13.5, and native signing/trust remain explicit open cells. ## Work packages, in required order @@ -55,6 +60,8 @@ complete a box. Linux, macOS, and Windows on x64 and arm64. - [x] Prove exact clean-build equality and exact runtime-tree closure on all six runner families. - [x] Complete the all-six SBOM, license, provenance, toolchain, and prohibited-content audit. +- [ ] Rebuild both Linux artifacts in the digest-pinned glibc 2.28/libstdc++ 6.0.25 userland on + native x64/arm64 runners; smoke and compare them there. - [ ] Prove each candidate on its oldest supported OS/libc/kernel baseline. - [ ] Sign macOS and Windows bytes and verify native trust on the target OS. - [ ] Merge draft PR [#8741](https://github.com/stablyai/orca/pull/8741). 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 1a4f99a9b8a..41063b6a9e2 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 @@ -9,6 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps 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** — artifact-only implementation commit `0cb3f7510` packages the correction after E-M3-LINUX-BASELINE-LOCAL-GREEN-001 proved the Linux x64 builder through two complete offline Rocky 8 builds, exact output equality, archive/tree verification, bundled PTY/watcher smoke, and the glibc 2.28/libstdc++ 6.0.25 userland verifier; this local Docker Desktop x64-emulation cell is supplemental only, so exact-head native GitHub x64/arm64 repetitions, exact kernel 4.18, macOS 13.5, and native-trust cells remain open; no tuple is enabled or published.
      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) @@ -657,6 +658,15 @@ runner, static symbol scan, or container may add evidence but cannot silently st unavailable qualifying snapshot. Every tuple remains disabled and no production consumer is connected. +**Baseline correction — 2026-07-14, Codex implementation owner:** +E-M3-LINUX-BASELINE-LOCAL-RED-001 proves the existing Linux x64 candidate cannot load its patched +`node-pty` on Rocky 8.9/glibc 2.28 because the Ubuntu 24.04 producer emitted references to +`GLIBC_2.32` and `GLIBC_2.34`. This exposes a producer flaw, not permission to weaken the declared +floor. The Linux jobs must compile and smoke native modules inside the digest-pinned oldest +glibc/libstdc++ userland on native x64/arm64 runners, repeat exact clean-build equality and metadata +gates for the new bytes, and retain the exact kernel 4.18 cell as an explicit residual gap. A newer +host build followed by container execution is not qualifying evidence. + **Active sub-gate — 2026-07-14, Codex implementation owner:** exact-head run 29345126283 proves all six target-native artifact/executable jobs, including normal Windows x64/arm64 settlement and unpublished upload. E-M3-REPRODUCIBILITY-LOCAL-001 proves the bounded comparator requires two @@ -825,16 +835,16 @@ Each runtime must contain only the executable closure required by the relay. ### Per-tuple build and executable proof -| Runtime tuple | Build/provenance | Bundled Node | `node-pty` load + real PTY | Watcher events | Oldest baseline | Native trust | Evidence | -| ----------------- | ---------------- | ------------ | -------------------------- | -------------- | --------------- | ------------ | ----------------------------------- | -| linux-x64-glibc | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-CI-001 | -| linux-arm64-glibc | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-RUNTIME-LOCAL-001; E-M3-CI-001 | -| linux-x64-musl | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | -| linux-arm64-musl | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | -| darwin-x64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-CI-001 | -| darwin-arm64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-CI-001 | -| win32-x64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-WINDOWS-CI-001 | -| win32-arm64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-WINDOWS-CI-001 | +| Runtime tuple | Build/provenance | Bundled Node | `node-pty` load + real PTY | Watcher events | Oldest baseline | Native trust | Evidence | +| ----------------- | ---------------- | ------------ | -------------------------- | -------------- | --------------- | ------------ | ---------------------------------------------------------- | +| linux-x64-glibc | [ ] | [x] | [ ] | [ ] | [ ] | [ ] | E-M3-CI-001; E-M3-LINUX-BASELINE-LOCAL-RED-001 | +| linux-arm64-glibc | [ ] | [x] | [ ] | [ ] | [ ] | [ ] | E-M3-RUNTIME-LOCAL-001; E-M3-CI-001; floor rebuild pending | +| linux-x64-musl | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | +| linux-arm64-musl | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | +| darwin-x64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-CI-001 | +| darwin-arm64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-CI-001 | +| win32-x64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-WINDOWS-CI-001 | +| win32-arm64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-WINDOWS-CI-001 | Rules: @@ -6995,6 +7005,297 @@ bounded version line: `. The PE lookup returned successfully but the `Fil - Follow-up: implement and run purpose-named oldest-supported-baseline checks, then prove native signing/trust before connecting any artifact consumer or enabling any tuple. +### E-M3-LINUX-BASELINE-LOCAL-RED-001 — Ubuntu-built Linux candidate fails the glibc 2.28 floor + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source artifact: exact-head Actions run + [29373507297](https://github.com/stablyai/orca/actions/runs/29373507297), unpublished + `linux-x64-glibc` artifact `8327034796`, archive SHA-256 + `177ad16222b08beb972bf66910bbafd32bf1e820deb007fef3304057cb6a01b8`, content ID + `960546cd96c67fcf9bb0a61e96ecdbecbffd9104d3a495578f8bb19dd810649a`. +- Runner/environment: local macOS arm64 controller, Docker 29.2.1, native artifact architecture + executed through Docker Desktop's bounded `linux/amd64` VM; container + `docker.io/library/rockylinux@sha256:2d05a9266523bbf24f33ebc3a9832e4d5fd74b973c220f2204ca802286aa275d` + (Rocky Linux 8.9, glibc 2.28, libstdc++.so.6.0.25). This is local red evidence only; native + GitHub x64/arm64 repetition remains required. +- Remote and transport: no SSH remote; read-only bind-mounted unpublished runtime, container + networking disabled, all capabilities dropped, `no-new-privileges`, 128 PIDs, 1 GiB memory, two + CPUs, and a 64 MiB `/tmp` tmpfs. +- Commands: + + ```bash + tar -xJf orca-ssh-relay-runtime-v1-linux-x64-glibc-960546cd….tar.xz -C "$runtime" + docker run --rm --platform linux/amd64 --network none --read-only --cap-drop all \ + --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 2 \ + --tmpfs /tmp:rw,nosuid,size=64m \ + --mount "type=bind,src=$PWD,dst=/workspace,readonly" \ + --mount "type=bind,src=$runtime,dst=/runtime,readonly" \ + --workdir /workspace "$image" \ + /runtime/bin/node config/scripts/ssh-relay-runtime-smoke-child.cjs /runtime + docker run --rm --platform linux/amd64 --network none --read-only --cap-drop all \ + --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 2 \ + --tmpfs /tmp:rw,nosuid,size=64m \ + --mount "type=bind,src=$runtime,dst=/runtime,readonly" "$image" \ + /runtime/bin/node -e "require('/runtime/node_modules/node-pty/build/Release/pty.node')" + ``` + +- Result: expected RED. The complete smoke stops before PTY/watcher evidence because patched + `node-pty` cannot load. Direct loading reports `/lib64/libc.so.6: version 'GLIBC_2.34' not found`; + `ldd` additionally reports `GLIBC_2.32` missing. The file exists and is the identity-authenticated + x64 ELF, so this is an ABI-floor mismatch rather than a missing archive entry. +- Verifier defect exposed: the first baseline CLI run also rejected with `Runtime baseline did not +resolve one bounded libstdc++ ABI library`; inspection found its filename parser accidentally + removed the SONAME `6` from `libstdc++.so.6.0.25`. This is + E-M3-BASELINE-VERIFIER-LOCAL-RED-001 and requires a focused parser test/correction before CI. +- Oracle proved: Ubuntu 24.04 producer smoke and target-native architecture alone do not prove the + declared Linux ABI floor; current Linux candidate bytes are ineligible. +- Does not prove: native x64 timing, Linux arm64 behavior, kernel 4.18, SSH transfer/install, native + trust, or any enabled tuple. +- Follow-up: build both Linux tuples in the digest-pinned Rocky 8.9 userland on native runners, + correct the bounded libstdc++ filename parser, repeat smoke/baseline/equality/metadata gates, and + keep kernel 4.18 explicitly open. + +### E-M3-LINUX-BUILDER-PYTHON-LOCAL-RED-001 — Rocky 8 default Python is too old for node-gyp 12 + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: uncommitted local WP2 correction at Git HEAD + `775f2cbcc7f66f6e728fa06ae2d6822edd50f7b0`; no artifact was emitted or published. +- Runner/environment: local macOS arm64 controller, Docker 29.2.1, `linux/amd64` Docker Desktop + emulation; builder base + `docker.io/library/rockylinux@sha256:2d05a9266523bbf24f33ebc3a9832e4d5fd74b973c220f2204ca802286aa275d` + (Rocky Linux 8.9, glibc 2.28, libstdc++.so.6.0.25), derived local builder image ID + `sha256:1f06eda13b33be5d30aa1b256895516b322d329753f9743614a5c8ebf1286291`. +- Remote and transport: no SSH remote; workflow-equivalent local container with networking + disabled, read-only root, all capabilities dropped, `no-new-privileges`, 512 PIDs, 6 GiB memory, + four CPUs, and a 1 GiB `/tmp` tmpfs. +- Command: + + ```bash + docker run --rm --platform linux/amd64 --network none --read-only --cap-drop all \ + --security-opt no-new-privileges --pids-limit 512 --memory 6g --cpus 4 \ + --tmpfs /tmp:rw,nosuid,size=1g \ + /usr/bin/node config/scripts/ssh-relay-runtime-linux-build-evidence.mjs \ + --tuple linux-x64-glibc --inputs-directory /evidence/inputs \ + --output-root /evidence/native-output --work-directory /evidence/native-work \ + --evidence-directory /evidence/verified-output \ + --source-date-epoch 1784069324 \ + --git-commit 775f2cbcc7f66f6e728fa06ae2d6822edd50f7b0 + ``` + +- Result: expected RED. The evidence driver stopped during the first `node-pty` native build before + creating or copying any candidate. node-gyp 12.3.0 selected `/usr/bin/python3` version 3.6.8 and + failed parsing its own `if flags := ...` syntax. The container capability probe then proved + distro package `python39-3.9.25-2.module+el8.10.0+40046+11e46e10` installs as + `/usr/bin/python3.9` without changing the glibc/libstdc++ floor. +- Correction: the digest-pinned prepared builder installs `python39`, asserts Python 3.9, and sets + `NODE_GYP_FORCE_PYTHON=/usr/bin/python3.9`; the focused workflow contract passes 4/4. Package + installation remains in the networked preparation phase, while both native builds remain + network-disabled. +- Oracle proved: the earlier builder definition could not build the artifact and failed before + output; a modern build controller is compatible with the old runtime ABI userland when installed + before the isolated compilation phase. +- Does not prove: a complete x64 build, x64 reproducibility/smoke/baseline, native arm64, kernel + 4.18, SSH transfer/install, signing/trust, or any enabled tuple. +- Follow-up: repeat the full two-build x64 evidence command with the corrected builder, run the + purpose-named Linux userland verifier over its verified output, then repeat on native GitHub x64 + and arm64 runners. + +### E-M3-LINUX-BUILDER-GCC-LOCAL-RED-001 — Rocky GCC 8 rejects Node v24's final C++20 spelling + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: uncommitted local WP2 correction at Git HEAD + `775f2cbcc7f66f6e728fa06ae2d6822edd50f7b0`; no artifact was emitted or published. +- Runner/environment: the same local macOS arm64/Docker `linux/amd64` emulation and digest-pinned + Rocky 8.9 base as E-M3-LINUX-BUILDER-PYTHON-LOCAL-RED-001; corrected builder selected Python + 3.9.25 and retained glibc 2.28/libstdc++.so.6.0.25. +- Command: the E-M3-LINUX-BUILDER-PYTHON-LOCAL-RED-001 two-build evidence command, repeated with + `NODE_GYP_FORCE_PYTHON=/usr/bin/python3.9` in the corrected builder. +- Result: expected RED after 36.42 seconds, 148,564 KiB peak RSS. node-gyp 12.3.0 successfully + selected Python 3.9.25, then GCC 8.5.0 rejected Node v24's generated `-std=gnu++20` argument and + suggested its supported draft spelling, `-std=gnu++2a`. The evidence driver removed the failed + first output and copied no candidate. +- Correction: the Linux-only build now replaces exactly one `'-std=gnu++20',` entry with + `'-std=gnu++2a',` in the already signature/hash-verified extracted Node `common.gypi`; zero or + multiple matches fail closed and non-Linux inputs are unchanged. The extracted headers are build + inputs only, so bundled official Node bytes remain unchanged. Four focused compatibility tests + cover replace, non-Linux preservation, and both rejected counts. +- Oracle proved: the oldest declared GCC/libstdc++ toolchain cannot consume the unmodified Node v24 + header spelling; silently selecting a newer compiler could narrow the libstdc++ floor. +- Does not prove: successful native compilation with the corrected flag, runtime smoke, exact + equality, x64/arm64 native runners, kernel 4.18, SSH transfer/install, signing/trust, or an enabled + tuple. + +### E-M3-LINUX-BUILDER-TOOLCHAIN-LOCAL-RED-001 — Minimal builder cannot record its compiler + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source and environment: the same uncommitted source, base digest, local controller, and bounded + offline container as E-M3-LINUX-BUILDER-GCC-LOCAL-RED-001. +- Command: the same two-build evidence command after the exact Linux C++ flag correction. +- Result: expected RED after 1 minute 40.14 seconds, 694,188 KiB peak RSS. The first native module + compiled and the runtime reached metadata collection, but the minimal Rocky image lacked the + `which` locator used by the fail-closed toolchain collector. It therefore refused to claim a + compiler identity and removed the incomplete output. The collector would also have probed + generic `python3` instead of the exact forced node-gyp interpreter. +- Correction: the prepared builder explicitly installs distro-signed `which`, and Linux provenance + resolves and hashes `NODE_GYP_FORCE_PYTHON` (`/usr/bin/python3.9`) rather than a generic alias. + The focused toolchain/workflow suite passes 15/15 and asserts both contracts. +- Oracle proved: artifact compilation alone is insufficient; missing or inaccurate toolchain + identity fails the build before any candidate is copied. +- Does not prove: a complete first build, a second equal build, runtime smoke, baseline execution, + native runner behavior, kernel 4.18, or any enabled tuple. + +### E-M3-LINUX-BASELINE-LOCAL-GREEN-001 — Corrected x64 bytes pass the oldest Linux userland + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: uncommitted local WP2 correction at Git HEAD + `775f2cbcc7f66f6e728fa06ae2d6822edd50f7b0`; the artifact-affecting correction was subsequently + committed as `0cb3f7510`, while later fail-closed artifact-selection, `ldd` stderr, and test-cleanup + hardening did not change built bytes. This remains unpublished local evidence only. +- Runner/environment: local macOS arm64 controller, Docker 29.2.1, Docker Desktop bounded + `linux/amd64` emulation. Build base and separate baseline image: + `docker.io/library/rockylinux@sha256:2d05a9266523bbf24f33ebc3a9832e4d5fd74b973c220f2204ca802286aa275d`; + corrected builder image + `sha256:9c32993d7a91557657593ae8258568a71f9832b0ce7c8824a1229b939ef49968`. + Compilation used glibc 2.28, libstdc++.so.6.0.25, GCC 8.5.0, Python 3.9.25, GNU Make 4.2.1, + GNU strip 2.30, distro Node 20.20.2 as controller, and verified bundled Node v24.18.0. +- Build command: + + ```bash + repo=$PWD + root=/private/tmp/orca-8450-linux-floor-build-v6 + docker run --rm --platform linux/amd64 --network none --read-only --cap-drop all \ + --security-opt no-new-privileges --pids-limit 512 --memory 6g --cpus 4 \ + --tmpfs /tmp:rw,nosuid,size=1g \ + --mount type=bind,src=/private/tmp/orca-8450-linux-floor-build/workspace,dst=/workspace \ + --mount type=bind,src=$repo/node_modules,dst=/workspace/node_modules,readonly \ + --mount type=bind,src=$repo/config/scripts,dst=/workspace/config/scripts,readonly \ + --mount type=bind,src=/private/tmp/orca-8450-linux-floor-build/inputs,dst=/inputs,readonly \ + --mount type=bind,src=$root,dst=/evidence \ + --workdir /workspace orca-ssh-relay-linux-builder:local-x64-python39 \ + /usr/bin/time -v /usr/bin/node \ + config/scripts/ssh-relay-runtime-linux-build-evidence.mjs \ + --tuple linux-x64-glibc --inputs-directory /inputs \ + --output-root /evidence/output --work-directory /evidence/work \ + --evidence-directory /evidence/verified --source-date-epoch 1784069324 \ + --git-commit 775f2cbcc7f66f6e728fa06ae2d6822edd50f7b0 + ``` + +- Build result: PASS in 6 minutes 48.16 seconds with 694,196 KiB peak RSS. Both complete builds + produced content ID `fc63ca342a5990f460ec6d72262a8542173dab20ce03c9b9cfb755b1c6057e6d` + and byte-identical 29,268,776-byte archive SHA-256 + `8f9095d1017fda387d66762ca6ccdd10a05d0138ff1b4ca6835d4b2a6eb7be83`. + Reproducibility compared 54 entries, 38 files, and 154,157,598 bytes. First/second build durations + were 256,687.12/127,672.46 ms; verifier durations were 9,541.30/5,525.82 ms. Both smokes reported + Node v24.18.0, modules ABI 137, PTY exit 23 after resize to 101x37, the required five watcher + events, 377.22/378.81 ms smoke duration, and 58,380,288/58,052,608 RSS bytes. +- Verified metadata SHA-256 values: identity + `edc57658e5d245b4652f4418a70f0b8a519d287ab570ceac582ba6ead0221715`, SPDX + `a7b58dc14ea671c60d5111eb9ade8141e502a4562161ed540b4de75f9703346b`, and provenance + `c19086b3fb511cca04a31311d6677df58f47d0f10d91e30b180b88b6775b839d`. +- Separate boundary/baseline commands: + + ```bash + repo=$PWD + root=/private/tmp/orca-8450-linux-floor-build-v6 + runtime=$root/baseline-runtime + archive=orca-ssh-relay-runtime-v1-linux-x64-glibc-fc63ca342a5990f460ec6d72262a8542173dab20ce03c9b9cfb755b1c6057e6d.tar.xz + identity=orca-ssh-relay-runtime-linux-x64-glibc.identity.json + mkdir $runtime + tar -xJf $root/verified/$archive -C $runtime + docker run --rm --platform linux/amd64 --network none --read-only --cap-drop all \ + --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 2 \ + --tmpfs /tmp:rw,nosuid,size=64m \ + --mount type=bind,src=$repo/config/scripts,dst=/workspace/config/scripts,readonly \ + --mount type=bind,src=$repo/node_modules,dst=/workspace/node_modules,readonly \ + --mount type=bind,src=$root/verified,dst=/evidence,readonly \ + --mount type=bind,src=$runtime,dst=/runtime,readonly \ + --workdir /workspace orca-ssh-relay-linux-builder:local-x64-python39 \ + /runtime/bin/node config/scripts/verify-ssh-relay-runtime.mjs \ + --runtime-directory /runtime --identity /evidence/$identity --archive /evidence/$archive + image=docker.io/library/rockylinux@sha256:2d05a9266523bbf24f33ebc3a9832e4d5fd74b973c220f2204ca802286aa275d + docker run --rm --platform linux/amd64 --network none --read-only --cap-drop all \ + --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 2 \ + --tmpfs /tmp:rw,nosuid,size=64m \ + --mount type=bind,src=$repo/config/scripts,dst=/workspace/config/scripts,readonly \ + --mount type=bind,src=$runtime,dst=/runtime,readonly --workdir /workspace $image \ + /runtime/bin/node config/scripts/ssh-relay-runtime-smoke-child.cjs /runtime + docker run --rm --platform linux/amd64 --network none --read-only --cap-drop all \ + --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 2 \ + --tmpfs /tmp:rw,nosuid,size=64m \ + --mount type=bind,src=$repo/config/scripts,dst=/workspace/config/scripts,readonly \ + --mount type=bind,src=$runtime,dst=/runtime,readonly --workdir /workspace $image \ + /runtime/bin/node config/scripts/ssh-relay-runtime-baseline.mjs \ + --tuple linux-x64-glibc --scope linux-userland --runtime-directory /runtime + ``` + +- Boundary/baseline result: PASS. Fresh archive/tree verification matched 49 entries, 34 files, + 124,846,430 expanded bytes, and the exact content ID; smoke took 280.64 ms at 58,208,256 RSS + bytes. The separate unmodified Rocky base smoke took 438.55 ms at 58,908,672 RSS bytes. The + baseline evaluator returned `qualified: true` for platform x64, glibc 2.28, and libstdc++ 6.0.25, + with explicit residual gap `kernel`: observed shared Docker kernel 6.12.72-linuxkit versus required + 4.18. +- Oracle proved: corrected x64 artifacts can be built twice without egress in the oldest declared + Linux userland, remain byte-identical, pass archive/tree/Node/PTY/watcher checks, and execute in an + unmodified glibc 2.28/libstdc++ 6.0.25 userland. +- Does not prove: a target-native x64 runner, native arm64, exact kernel 4.18, SSH transfer/install, + native trust, release aggregation, packaged desktop use, fallback, performance against legacy, or + an enabled tuple. +- Follow-up: push the exact correction to draft PR #8741, require the native x64 and arm64 build and + supplemental userland cells to pass, audit the downloaded artifacts, and retain the kernel 4.18 + and native-trust blockers. + +### E-M3-LINUX-BUILDER-LOCAL-VALIDATION-001 — Corrected WP2 source gates pass locally + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: the same uncommitted WP2 source and Git HEAD as + E-M3-LINUX-BASELINE-LOCAL-GREEN-001. +- Commands: + + ```bash + 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 + pnpm exec oxfmt --check \ + .github/workflows/ssh-relay-runtime-artifacts.yml \ + config/ssh-relay-runtime-linux-builder.Containerfile \ + config/scripts/build-ssh-relay-runtime.mjs \ + config/scripts/ssh-relay-linux-node-gyp-compiler.mjs \ + config/scripts/ssh-relay-linux-node-gyp-compiler.test.mjs \ + config/scripts/ssh-relay-runtime-baseline.mjs \ + config/scripts/ssh-relay-runtime-baseline.test.mjs \ + config/scripts/ssh-relay-runtime-compatibility.mjs \ + config/scripts/ssh-relay-runtime-linux-build-evidence.mjs \ + config/scripts/ssh-relay-runtime-linux-build-evidence.test.mjs \ + config/scripts/ssh-relay-runtime-toolchain.mjs \ + config/scripts/ssh-relay-runtime-toolchain.test.mjs \ + config/scripts/ssh-relay-runtime-tree.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html + git diff --check + ``` + +- Result: PASS. The expanded focused suite passed 22 files/100 tests. Typecheck passed. Lint passed + with only existing unrelated repository warnings; its reliability, max-lines, bundled-skill, + localization-catalog, and localization-coverage sub-gates all passed. The direct max-lines ratchet + passed with 355 grandfathered suppressions and no new bypass. Focused formatting and + `git diff --check` passed. +- Runner detail: local macOS arm64 shell used Node v26.0.0 and pnpm 10.24.0, which emitted the + expected package-engine warning because repository CI requires Node 24. Draft-PR jobs install + exact Node 24.18.0 and remain the qualifying contract cells. +- Does not prove: GitHub workflow parsing/execution, native x64/arm64 builds, oldest kernel 4.18, + native signing/trust, SSH transfer/install, or any enabled tuple. +- Follow-up: commit this evidence update, push both reviewable commits, and audit every draft-PR job + and downloaded artifact before closing any runner-backed cell. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 64eb43b7ab2..865b79ccee7 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1137,6 +1137,25 @@

      Build for the runtime, architecture, and libc together.

      Rosetta SSH cell. These are selector eligibility constraints, not tuple enablement; every candidate still needs the complete two-layer evidence.

      +

      + Implementation evidence on 2026-07-14 rejected Linux native modules built directly on + Ubuntu 24.04: the resulting pty.node required GLIBC_2.32 and + GLIBC_2.34 and could not load in the declared glibc 2.28 userland. Linux + native modules must therefore be compiled and smoked in the digest-pinned oldest + glibc/libstdc++ userland on the tuple-native architecture. A later baseline-container + test cannot repair bytes built against a newer ABI, and that userland proof still + leaves the exact kernel 4.18 cell open. +

      +

      + The prepared Rocky 8 builder pins Python 3.9 because node-gyp 12 cannot run under the + distribution's default Python 3.6. GCC 8 uses the draft spelling + gnu++2a for the C++20 mode requested by Node v24's build headers, so the + Linux-only build step must replace exactly one -std=gnu++20 occurrence in + the already verified, extracted common.gypi and fail closed on any other + count. This build-header compatibility step never changes bundled Node bytes, does not + apply to macOS or Windows, and avoids introducing a newer libstdc++ dependency; + complete native-module smoke and ABI inspection remain mandatory proof. +

      Windows target-native native-module builds use three immutable inputs authenticated by that same signed checksum document: the tuple's official Node ZIP, the exact-version @@ -1777,8 +1796,10 @@

      Resolve these before default-on.

      Linux baseline mismatch

      - Build against the oldest supported glibc and libstdc++ baseline; run on that exact - baseline before promotion. + Build native modules in the oldest supported glibc and libstdc++ userland on the + tuple-native architecture; run the complete runtime there, then separately run on + the exact oldest kernel before promotion. Reject newer-userland producer output even + when it smokes successfully on the producer runner.

      From 53082dd1f920f41c4865980cf9a7686eb7bb82f9 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 20:07:13 -0400 Subject: [PATCH 076/267] test(ssh): normalize Windows workflow fixtures Co-authored-by: Orca --- .../ssh-relay-runtime-workflow.test.mjs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index d799830a257..481f53960ec 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -9,7 +9,15 @@ const workflowUrl = new URL( ) const linuxBuilderUrl = new URL('../ssh-relay-runtime-linux-builder.Containerfile', import.meta.url) +function normalizeCheckoutNewlines(source) { + return source.replaceAll('\r\n', '\n') +} + describe('SSH relay runtime artifact workflow', () => { + it('normalizes Windows checkout newlines for text contracts', () => { + expect(normalizeCheckoutNewlines('first\r\nsecond\r\n')).toBe('first\nsecond\n') + }) + it('uses exact native runner labels and SHA-pinned actions without publication authority', async () => { const workflow = parse(await readFile(workflowUrl, 'utf8')) const posixJob = workflow.jobs['build-posix-runtime'] @@ -149,6 +157,8 @@ describe('SSH relay runtime artifact workflow', () => { (step) => step.name === 'Build twice, inspect, smoke, and compare exact runtime' ) const containerfile = await readFile(linuxBuilderUrl, 'utf8') + // Why: Git may materialize CRLF on Windows, but the Containerfile contract is newline-agnostic. + const normalizedContainerfile = normalizeCheckoutNewlines(containerfile) expect(prepare.if).toBe("runner.os == 'Linux'") expect(prepare.run).toContain('docker pull "$image"') @@ -161,14 +171,14 @@ describe('SSH relay runtime artifact workflow', () => { expect(build.run.indexOf('--network none')).toBeLessThan( build.run.indexOf('ssh-relay-runtime-linux-build-evidence.mjs') ) - expect(containerfile).toContain('ARG BASE_IMAGE=scratch\nFROM ${BASE_IMAGE}') - expect(containerfile).toContain("getconf GNU_LIBC_VERSION)\" = 'glibc 2.28'") - expect(containerfile).toContain('libstdc++.so.6.0.25') - expect(containerfile).toContain('dnf module enable -y -q nodejs:20') - expect(containerfile).toContain('Number(process.versions.node.split') - expect(containerfile).toContain('python39') - expect(containerfile).toContain('NODE_GYP_FORCE_PYTHON=/usr/bin/python3.9') - expect(containerfile).toContain(' which \\\n') + expect(normalizedContainerfile).toContain('ARG BASE_IMAGE=scratch\nFROM ${BASE_IMAGE}') + expect(normalizedContainerfile).toContain("getconf GNU_LIBC_VERSION)\" = 'glibc 2.28'") + expect(normalizedContainerfile).toContain('libstdc++.so.6.0.25') + expect(normalizedContainerfile).toContain('dnf module enable -y -q nodejs:20') + expect(normalizedContainerfile).toContain('Number(process.versions.node.split') + expect(normalizedContainerfile).toContain('python39') + expect(normalizedContainerfile).toContain('NODE_GYP_FORCE_PYTHON=/usr/bin/python3.9') + expect(normalizedContainerfile).toContain(' which \\\n') expect(source).not.toMatch(/releases\/|gh release|contents:\s*write/i) }) From 24153775b2327001e581c341301dfc41bb9cb61f Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 20:07:34 -0400 Subject: [PATCH 077/267] docs(ssh): record Windows newline gate Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 3 +++ ...github-release-implementation-checklist.md | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+) 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 e3298df4ece..5802b11735a 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 @@ -26,6 +26,9 @@ complete a box. userland verifier. This is supplemental local emulation evidence, not a qualifying native cell. - Local focused tests pass 100/100; typecheck, lint, max-lines, formatting, and diff checks pass. - Artifact-only implementation commit: `0cb3f7510` (`build(ssh): prove Linux runtime floor`). + - Exact-head CI run `29377854121` stopped in the Windows x64 contract test because Git supplied + CRLF Containerfile text to an LF-only assertion. Portability correction `53082dd1f` passes 5/5 + focused and 101/101 aggregate tests; no Windows x64 artifact was built in the red job. - All six target-native artifact, smoke, equality, SBOM, license, provenance, toolchain, and prohibited-content cells pass in [run 29373507297](https://github.com/stablyai/orca/actions/runs/29373507297); every downloaded 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 41063b6a9e2..f2e63848a1c 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 @@ -7296,6 +7296,31 @@ resolve one bounded libstdc++ ABI library`; inspection found its filename parser - Follow-up: commit this evidence update, push both reviewable commits, and audit every draft-PR job and downloaded artifact before closing any runner-backed cell. +### E-M3-WORKFLOW-WINDOWS-CRLF-CI-RED-001 — Containerfile contract was newline-sensitive + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source/run: exact draft-PR head `3973e68d772008a20c2664ad90708f6d0e00b403`, Actions run + [29377854121](https://github.com/stablyai/orca/actions/runs/29377854121), Windows x64 job + [87234935616](https://github.com/stablyai/orca/actions/runs/29377854121/job/87234935616). +- Runner/environment: GitHub-hosted `windows-2022` x64; exact image/tool identities remain in the + job log. The failure occurred in the purpose-named runtime artifact contract-test step before Node + inputs, native compilation, verification, or upload. +- Command: the workflow's exact Node 24.18.0 PowerShell contract suite, including + `config/scripts/ssh-relay-runtime-workflow.test.mjs`. +- Result: expected RED. Git's Windows checkout supplied the Containerfile with CRLF line endings; + the test compared `ARG BASE_IMAGE=scratch\nFROM ${BASE_IMAGE}` literally and failed at line 164. + No Windows x64 candidate was built or uploaded. Linux contract cells passed the same assertion + with LF, proving the implementation text was present but the oracle was not checkout-portable. +- Correction: normalize CRLF to LF only within the test's in-memory Containerfile text before + asserting exact content. Commit `53082dd1f` adds the explicit CRLF fixture; the purpose-named + workflow suite passes 5/5 and the expanded artifact suite passes 22 files/101 tests. Production + workflow/Containerfile bytes and runtime behavior remain unchanged. +- Does not prove: the corrected Windows contract, any candidate from this job, the complete run, + baseline execution, signing/trust, SSH behavior, or an enabled tuple. +- Follow-up: commit this evidence, push both commits, and audit the replacement exact-head run from + the beginning. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 05b3a4b18a54aa06ba5e29eb3edf8bb4e86b4d0e Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 20:29:40 -0400 Subject: [PATCH 078/267] build(ssh): preserve Linux runner ownership Co-authored-by: Orca --- .github/workflows/ssh-relay-runtime-artifacts.yml | 3 ++- config/scripts/ssh-relay-runtime-workflow.test.mjs | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index feb83e420d3..b004dd4f76c 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -189,8 +189,9 @@ jobs: if [[ '${{ matrix.tuple }}' == linux-* ]]; then # Why: package installation is isolated earlier; native compilation itself has no egress. docker run --rm --network none --read-only --cap-drop all \ + --user "$(id -u):$(id -g)" \ --security-opt no-new-privileges --pids-limit 512 --memory 6g --cpus 4 \ - --tmpfs /tmp:rw,nosuid,size=1g \ + --tmpfs /tmp:rw,nosuid,size=1g,mode=1777 \ --mount "type=bind,src=$GITHUB_WORKSPACE,dst=$GITHUB_WORKSPACE" \ --mount "type=bind,src=$RUNNER_TEMP,dst=$RUNNER_TEMP" \ --workdir "$GITHUB_WORKSPACE" \ diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 481f53960ec..7f583a961c1 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -166,7 +166,9 @@ describe('SSH relay runtime artifact workflow', () => { expect(prepare.run).toContain('config/ssh-relay-runtime-linux-builder.Containerfile') expect(build.run).toContain("if [[ '${{ matrix.tuple }}' == linux-* ]]") expect(build.run).toContain('--network none --read-only --cap-drop all') + expect(build.run).toContain('--user "$(id -u):$(id -g)"') expect(build.run).toContain('--security-opt no-new-privileges') + expect(build.run).toContain('--tmpfs /tmp:rw,nosuid,size=1g,mode=1777') expect(build.run).toContain('ssh-relay-runtime-linux-build-evidence.mjs') expect(build.run.indexOf('--network none')).toBeLessThan( build.run.indexOf('ssh-relay-runtime-linux-build-evidence.mjs') From cc4c45c8981fd87b5681e8c0be7b3b06f2cfab22 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 20:30:04 -0400 Subject: [PATCH 079/267] docs(ssh): record native floor blockers Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 28 ++--- ...github-release-implementation-checklist.md | 108 ++++++++++++++++-- ...6-07-14-ssh-relay-github-release-plan.html | 8 ++ 3 files changed, 121 insertions(+), 23 deletions(-) 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 5802b11735a..3a1d28538da 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 @@ -21,22 +21,18 @@ complete a box. ## Active work - [ ] **WP2 — Prove oldest supported baselines and native trust.** - - Session checkpoint: a local x64 Rocky 8 build now passes two complete offline builds, exact - equality, archive/tree verification, PTY/watcher smoke, and the glibc 2.28/libstdc++ 6.0.25 - userland verifier. This is supplemental local emulation evidence, not a qualifying native cell. - - Local focused tests pass 100/100; typecheck, lint, max-lines, formatting, and diff checks pass. - - Artifact-only implementation commit: `0cb3f7510` (`build(ssh): prove Linux runtime floor`). - - Exact-head CI run `29377854121` stopped in the Windows x64 contract test because Git supplied - CRLF Containerfile text to an LF-only assertion. Portability correction `53082dd1f` passes 5/5 - focused and 101/101 aggregate tests; no Windows x64 artifact was built in the red job. - - All six target-native artifact, smoke, equality, SBOM, license, provenance, toolchain, and - prohibited-content cells pass in - [run 29373507297](https://github.com/stablyai/orca/actions/runs/29373507297); every downloaded - payload passed direct audit. - - No tuple is enabled: the artifacts still need execution on each declared oldest OS/libc/kernel - baseline and target-native signing/trust proof. - - Next gate: run the corrected builder on native GitHub x64/arm64 runners. Kernel 4.18, macOS - 13.5, and native signing/trust remain explicit open cells. + - Proven: all-six artifact metadata gate; local Rocky 8 x64 two-build equality, smoke, and glibc + 2.28/libstdc++ 6.0.25 execution; Windows checkout-newline contract. + - Active red: exact-head run + [29378160419](https://github.com/stablyai/orca/actions/runs/29378160419) prepares both native Linux + builders, but both builds cannot create exclusive output through the runner-temp bind mount. + - Active correction locally passes two complete offline x64 builds, exact equality, archive/tree + verification, and bundled Node/PTY/watcher smoke as the host UID/GID with a mode-1777 tmpfs. + - Next proof: both native Linux builds, supplemental userland jobs, and direct artifact audit. + - Windows x64 passes its declared oldest-floor job. The hosted arm64 runner is build 26200, not the + required build 26100, so its otherwise successful artifact/runtime smoke does not close that cell. + - Still open: Windows arm64 build 26100, kernel 4.18, macOS 13.5, native signing/trust, and every SSH + transfer/runtime cell. ## Work packages, in required order 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 f2e63848a1c..554c8e7b00d 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps 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** — artifact-only implementation commit `0cb3f7510` packages the correction after E-M3-LINUX-BASELINE-LOCAL-GREEN-001 proved the Linux x64 builder through two complete offline Rocky 8 builds, exact output equality, archive/tree verification, bundled PTY/watcher smoke, and the glibc 2.28/libstdc++ 6.0.25 userland verifier; this local Docker Desktop x64-emulation cell is supplemental only, so exact-head native GitHub x64/arm64 repetitions, exact kernel 4.18, macOS 13.5, and native-trust cells remain open; no tuple is enabled or published.
      +Session checkpoint: **In progress — 2026-07-14, Codex implementation owner** — exact-head run [29378160419](https://github.com/stablyai/orca/actions/runs/29378160419) prepares the digest-pinned Linux builders on both native GitHub architectures, but both Linux jobs fail because the root container cannot create exclusive output through the runner-owned temporary-directory bind mount (E-M3-LINUX-RUNNER-MOUNT-PERMISSION-CI-RED-001); the bounded correction runs the offline container as the runner UID/GID with an explicit mode-1777 tmpfs while preserving every isolation/resource bound and passes a complete local two-build/equality/archive/tree/PTY/watcher proof (E-M3-LINUX-RUNNER-MOUNT-PERMISSION-LOCAL-GREEN-001); the same run proves Windows x64 on its declared floor, while native Windows arm64 artifacts and smoke pass but hosted build 26200 cannot prove required build 26100 (E-M3-WINDOWS-ARM64-BASELINE-CI-RED-001); no tuple is enabled or published.
      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) @@ -7321,6 +7321,100 @@ resolve one bounded libstdc++ ABI library`; inspection found its filename parser - Follow-up: commit this evidence, push both commits, and audit the replacement exact-head run from the beginning. +### E-M3-LINUX-RUNNER-MOUNT-PERMISSION-CI-RED-001 — Root container cannot stage into runner temp + +- Date: 2026-07-14 (job timestamps 2026-07-15 UTC) +- Owner: Codex implementation owner +- Source/run: exact draft-PR head `24153775b2327001e581c341301dfc41bb9cb61f`, Actions run + [29378160419](https://github.com/stablyai/orca/actions/runs/29378160419), Linux arm64 job + [87235847644](https://github.com/stablyai/orca/actions/runs/29378160419/job/87235847644). Linux x64 job + [87235847575](https://github.com/stablyai/orca/actions/runs/29378160419/job/87235847575) fails with + the same exception in the first second of the same build step. +- Runner/environment: GitHub-hosted native `ubuntu-24.04` x64 and `ubuntu-24.04-arm`; requested runner + and architecture were recorded by the workflow. Digest-pinned Rocky arm64 base + `sha256:3c2d0ce12bf79fc5ff05e43b1000e30ff062dc89405525f3307cbff71661f1a0`; produced builder image + `sha256:6384da2f38157d93856beb31f6ba45dffeec8e7cc139cd4468cdf5df2fa5619c`. Digest-pinned Rocky x64 + base `sha256:2d05a9266523bbf24f33ebc3a9832e4d5fd74b973c220f2204ca802286aa275d`; produced builder image + `sha256:4adbb55dff700b55b8cf1c20e4be6099869e3bd43236688bf462116b92da40dc`. +- Command: the workflow's offline `docker run --rm --network none --read-only --cap-drop all ...` + invocation of `ssh-relay-runtime-linux-build-evidence.mjs`, with the workspace and + `/home/runner/work/_temp` bind-mounted at identical container paths. +- Result: expected RED. Builder preparation passed after 3 minutes 25 seconds. The evidence driver + then failed immediately with `EACCES: permission denied, mkdir +'/home/runner/work/_temp/ssh-relay-runtime'`; no build, archive, smoke, comparison, upload, or + dependent baseline job ran. The container defaulted to root while the hosted runner bind mount did + not grant root write access under this isolation configuration. +- Plan correction: run the offline build container as `$(id -u):$(id -g)` and make `/tmp` an explicit + mode-1777 tmpfs. Retain read-only root, no network, dropped capabilities, no-new-privileges, and the + existing process/memory/CPU bounds. Do not broaden permissions on `RUNNER_TEMP` or leave root-owned + host output. +- Does not prove: the correction, any Linux runtime bytes from this run, Linux userland/kernel + baselines, native trust, SSH behavior, or an enabled tuple. +- Follow-up: add a workflow contract for UID/GID and tmpfs mode, validate the exact invocation + locally, then repeat both native Linux cells and audit their downloaded artifacts. + +### E-M3-LINUX-RUNNER-MOUNT-PERMISSION-LOCAL-GREEN-001 — Non-root build preserves the full gate + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: implementation commit `05b3a4b18` (`build(ssh): preserve Linux runner ownership`), based + on `24153775b2327001e581c341301dfc41bb9cb61f`; runtime-building scripts are unchanged from the base. +- Runner/environment: local macOS arm64 Docker Desktop running the existing amd64 Rocky 8 builder + `sha256:9c32993d7a91557657593ae8258568a71f9832b0ce7c8824a1229b939ef49968` under emulation. This is + supplemental permission/functional evidence, not a qualifying native architecture cell. +- Commands: the purpose-named workflow suite, plus a clean disposable workspace invocation of + `ssh-relay-runtime-linux-build-evidence.mjs` using `--network none --read-only --cap-drop all`, + `--user "$(id -u):$(id -g)"`, `--security-opt no-new-privileges`, the existing process/memory/CPU + bounds, and `--tmpfs /tmp:rw,nosuid,size=1g,mode=1777`. A separate bounded mount probe recorded the + runtime UID/GID, tmpfs mode, and host ownership of created output. +- Static commands: `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`; focused `pnpm exec oxfmt --check` over the five changed files; and `git +diff --check`. +- Result: PASS. Workflow contract 5/5. The probe observed UID 501, GID 20, `/tmp` mode 1777, and output + owned by local user `jinwoohong:wheel`. The complete driver passed in 4 minutes 47.48 seconds with + 694,172 KiB peak RSS. Both builds produced content ID + `fc63ca342a5990f460ec6d72262a8542173dab20ce03c9b9cfb755b1c6057e6d`, identical 29,270,716-byte + archive SHA-256 `94b2d5c1e28835aab05fc84578298199c300d7d5d0cd4e76057a3f44451356c3`, 49 archive entries, + 34 files, and 124,846,430 expanded bytes. +- Runtime result: both bundled-Node checks reported v24.18.0/modules ABI 137; both PTY checks resized + to 101x37 and exited 23; both watcher checks observed the required five create/update/delete events. + First/second build durations were 138,118.53/130,370.80 ms; verification durations were + 6,235.84/5,041.08 ms; smoke durations were 431.46/348.38 ms at 58,347,520/58,040,320 RSS bytes. +- Static result: PASS. The expanded artifact suite passed 22 files/101 tests. Typecheck, full lint, + reliability metadata, max-lines (355 grandfathered suppressions and no new bypass), bundled-skill, + localization catalog/coverage, focused formatting, and `git diff --check` passed. Lint emitted only + the existing unrelated repository warnings. Local Node v26.0.0 emitted the expected engine warning; + draft-PR jobs install exact Node 24.18.0. +- Does not prove: native x64 or arm64 hosted-runner behavior, exact Linux kernel 4.18, SSH + transfer/install, native trust, release aggregation, or an enabled tuple. +- Follow-up: run the exact correction on both native GitHub labels and audit every resulting artifact + and supplemental userland job before closing either cell. + +### E-M3-WINDOWS-ARM64-BASELINE-CI-RED-001 — Hosted build 26200 cannot prove build 26100 + +- Date: 2026-07-14 (job timestamps 2026-07-15 UTC) +- Owner: Codex implementation owner +- Source/run: exact draft-PR head `24153775b2327001e581c341301dfc41bb9cb61f`, Actions run + [29378160419](https://github.com/stablyai/orca/actions/runs/29378160419), Windows arm64 artifact job + [87235847556](https://github.com/stablyai/orca/actions/runs/29378160419/job/87235847556), and baseline + job [87237242684](https://github.com/stablyai/orca/actions/runs/29378160419/job/87237242684). Windows x64 + baseline control [87237242686](https://github.com/stablyai/orca/actions/runs/29378160419/job/87237242686) + passed. +- Runner/environment: GitHub-hosted native `windows-11-arm`. The baseline evaluator observed native + arm64 Windows `10.0.26200`; the declared oldest arm64 contract is exactly Windows 11 24H2 build 26100. Downloaded artifact ID `8328797474`, 33,084,426 bytes, Actions transport digest + `2453ddd8eefd9b371be296bebe0bcec15fdbaa245dca3a89b1eb2c668450cd23`. +- Result: expected RED for oldest-floor qualification. The native artifact built twice, verified, + compared, uploaded, downloaded, re-verified, and completed bundled Node/PTY/watcher smoke; smoke + settled to only the parent stdio pipes after the two-second observation window and used 48,525,312 + RSS bytes. The baseline evaluator returned platform and architecture true, `osBuild: false`, and + `qualified: false`, so the job correctly failed closed rather than presenting a newer hosted image + as proof of build 26100. Windows x64 passed the analogous declared-floor job. +- Does not prove: Windows arm64 build 26100 compatibility, native signature/trust, SSH behavior, or an + enabled tuple. A successful artifact/smoke run on build 26200 is deliberately insufficient. +- Follow-up: provision or select a native arm64 build-26100 snapshot/runner before closing this cell; + keep the tuple disabled and do not weaken the exact oldest-floor oracle. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -7378,12 +7472,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Implement purpose-named oldest-supported-baseline verification and artifact-only workflow cells. -Use GitHub-hosted native runners only where their OS/architecture actually satisfies a declared -floor; add static glibc/libstdc++/kernel eligibility oracles where execution on a newer runner is -insufficient, and do not claim an unavailable macOS/Windows floor from a newer hosted image. Record -the exact container image digest when a baseline container supplements native execution. Then prove -target-native signing/trust before connecting any artifact consumer or enabling any tuple. +Correct the native Linux runner bind-mount boundary without weakening isolation: execute the offline +builder as the runner UID/GID, use an explicit mode-1777 tmpfs, and retain the read-only root, +no-network, capability, privilege, process, memory, and CPU restrictions. Prove that correction on +both native GitHub architectures, then run and audit the supplemental oldest-userland cells. Keep the +exact kernel 4.18, unavailable macOS/Windows floor, and target-native signing/trust cells open before +connecting any artifact consumer or enabling any tuple. 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 diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 865b79ccee7..793d985b54c 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1156,6 +1156,14 @@

      Build for the runtime, architecture, and libc together.

      apply to macOS or Windows, and avoids introducing a newer libstdc++ dependency; complete native-module smoke and ABI inspection remain mandatory proof.

      +

      + The offline Linux build container runs as the native GitHub runner's numeric user and + group so exclusive staging written through bind mounts remains runner-owned on both + hosted architectures. Its writable /tmp is an explicitly mode-1777 tmpfs; + the root filesystem remains read-only and the existing no-network, capability-drop, + no-new-privileges, process, memory, and CPU bounds remain mandatory. This avoids both + root-owned host output and broad permission changes to runner temporary directories. +

      Windows target-native native-module builds use three immutable inputs authenticated by that same signed checksum document: the tuple's official Node ZIP, the exact-version From c6a7277cc6f56d453662ebd373e9e8e4e278f4ac Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 20:49:54 -0400 Subject: [PATCH 080/267] docs(ssh): record native Linux floor proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 21 ++- ...github-release-implementation-checklist.md | 152 +++++++++++++++--- 2 files changed, 140 insertions(+), 33 deletions(-) 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 3a1d28538da..5863d6e2397 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 @@ -21,18 +21,15 @@ complete a box. ## Active work - [ ] **WP2 — Prove oldest supported baselines and native trust.** - - Proven: all-six artifact metadata gate; local Rocky 8 x64 two-build equality, smoke, and glibc - 2.28/libstdc++ 6.0.25 execution; Windows checkout-newline contract. - - Active red: exact-head run - [29378160419](https://github.com/stablyai/orca/actions/runs/29378160419) prepares both native Linux - builders, but both builds cannot create exclusive output through the runner-temp bind mount. - - Active correction locally passes two complete offline x64 builds, exact equality, archive/tree - verification, and bundled Node/PTY/watcher smoke as the host UID/GID with a mode-1777 tmpfs. - - Next proof: both native Linux builds, supplemental userland jobs, and direct artifact audit. + - Proven: all-six target-native build/equality/smoke/metadata gates and direct payload audit. + - Proven: exact-head run + [29379227209](https://github.com/stablyai/orca/actions/runs/29379227209) builds both Linux tuples in + digest-pinned Rocky 8 on native runners; both downloaded artifacts pass glibc 2.28/libstdc++ + 6.0.25 execution, bundled Node, PTY, and watcher smoke. - Windows x64 passes its declared oldest-floor job. The hosted arm64 runner is build 26200, not the required build 26100, so its otherwise successful artifact/runtime smoke does not close that cell. - - Still open: Windows arm64 build 26100, kernel 4.18, macOS 13.5, native signing/trust, and every SSH - transfer/runtime cell. + - Next 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. ## Work packages, in required order @@ -59,8 +56,8 @@ complete a box. Linux, macOS, and Windows on x64 and arm64. - [x] Prove exact clean-build equality and exact runtime-tree closure on all six runner families. - [x] Complete the all-six SBOM, license, provenance, toolchain, and prohibited-content audit. -- [ ] Rebuild both Linux artifacts in the digest-pinned glibc 2.28/libstdc++ 6.0.25 userland on - native x64/arm64 runners; smoke and compare them there. +- [x] Rebuild both Linux artifacts in the digest-pinned glibc 2.28/libstdc++ 6.0.25 userland on + native x64/arm64 runners; smoke and compare them there. (`E-M3-LINUX-NATIVE-USERLAND-CI-001`) - [ ] Prove each candidate on its oldest supported OS/libc/kernel baseline. - [ ] Sign macOS and Windows bytes and verify native trust on the target OS. - [ ] Merge draft PR [#8741](https://github.com/stablyai/orca/pull/8741). 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 554c8e7b00d..c61afba921d 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps 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** — exact-head run [29378160419](https://github.com/stablyai/orca/actions/runs/29378160419) prepares the digest-pinned Linux builders on both native GitHub architectures, but both Linux jobs fail because the root container cannot create exclusive output through the runner-owned temporary-directory bind mount (E-M3-LINUX-RUNNER-MOUNT-PERMISSION-CI-RED-001); the bounded correction runs the offline container as the runner UID/GID with an explicit mode-1777 tmpfs while preserving every isolation/resource bound and passes a complete local two-build/equality/archive/tree/PTY/watcher proof (E-M3-LINUX-RUNNER-MOUNT-PERMISSION-LOCAL-GREEN-001); the same run proves Windows x64 on its declared floor, while native Windows arm64 artifacts and smoke pass but hosted build 26200 cannot prove required build 26100 (E-M3-WINDOWS-ARM64-BASELINE-CI-RED-001); no tuple is enabled or published.
      +Session checkpoint: **In progress — 2026-07-14, Codex implementation owner** — exact-head run [29379227209](https://github.com/stablyai/orca/actions/runs/29379227209) proves the non-root correction and both digest-pinned Linux builders on native x64/arm64 through two clean builds, equality, archive/tree inspection, bundled Node/PTY/watcher smoke, upload, download, direct metadata/closure audit, and supplemental glibc 2.28/libstdc++ 6.0.25 execution (E-M3-LINUX-NATIVE-USERLAND-CI-001); Windows x64 qualifies on exact Server 2022 build 20348 (E-M3-WINDOWS-X64-BASELINE-CI-001), while native Windows arm64 artifacts and smoke pass but hosted build 26200 again fails closed against required build 26100 (E-M3-WINDOWS-ARM64-BASELINE-CI-RED-001); exact kernel 4.18, macOS 13.5, Windows arm64 build 26100, and native signing/trust remain open; no tuple is enabled or published.
      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) @@ -650,10 +650,11 @@ archive inspection, bundled-Node/native-module/PTY/watcher smoke, SBOM, and prov behind purpose-named scripts and CI artifacts. No release publication, desktop resolver, SSH transfer, install, fallback, rollout setting, or tuple enablement belongs in this package. -**Current active gate — 2026-07-14, Codex implementation owner:** exact-head run 29373507297 and -direct inspection of all six downloaded payloads close target-native metadata/provenance/toolchain -completeness under E-M3-METADATA-CI-001. The next artifact-only package must prove the declared -oldest glibc/libstdc++/kernel, macOS, and Windows floors and target-native trust. A newer hosted +**Current active gate — 2026-07-14, Codex implementation owner:** exact-head run 29379227209 and +direct inspection of all six downloaded payloads close the native Linux producer plus glibc +2.28/libstdc++ 6.0.25 userland gates under E-M3-LINUX-NATIVE-USERLAND-CI-001 and the Windows x64 +Server 2022 build-20348 gate under E-M3-WINDOWS-X64-BASELINE-CI-001. Exact Linux kernel 4.18, +macOS 13.5, Windows arm64 build 26100, and target-native signing/trust remain open. A newer hosted runner, static symbol scan, or container may add evidence but cannot silently stand in for an unavailable qualifying snapshot. Every tuple remains disabled and no production consumer is connected. @@ -835,16 +836,16 @@ Each runtime must contain only the executable closure required by the relay. ### Per-tuple build and executable proof -| Runtime tuple | Build/provenance | Bundled Node | `node-pty` load + real PTY | Watcher events | Oldest baseline | Native trust | Evidence | -| ----------------- | ---------------- | ------------ | -------------------------- | -------------- | --------------- | ------------ | ---------------------------------------------------------- | -| linux-x64-glibc | [ ] | [x] | [ ] | [ ] | [ ] | [ ] | E-M3-CI-001; E-M3-LINUX-BASELINE-LOCAL-RED-001 | -| linux-arm64-glibc | [ ] | [x] | [ ] | [ ] | [ ] | [ ] | E-M3-RUNTIME-LOCAL-001; E-M3-CI-001; floor rebuild pending | -| linux-x64-musl | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | -| linux-arm64-musl | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | -| darwin-x64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-CI-001 | -| darwin-arm64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-CI-001 | -| win32-x64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-WINDOWS-CI-001 | -| win32-arm64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-WINDOWS-CI-001 | +| Runtime tuple | Build/provenance | Bundled Node | `node-pty` load + real PTY | Watcher events | Oldest baseline | Native trust | Evidence | +| ----------------- | ---------------- | ------------ | -------------------------- | -------------- | --------------- | ------------ | --------------------------------------------------- | +| linux-x64-glibc | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-LINUX-NATIVE-USERLAND-CI-001; kernel 4.18 open | +| linux-arm64-glibc | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-LINUX-NATIVE-USERLAND-CI-001; kernel 4.18 open | +| linux-x64-musl | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | +| linux-arm64-musl | [ ] | [ ] | [ ] | [ ] | [ ] | [ ] | — | +| darwin-x64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-CI-001 | +| darwin-arm64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-CI-001 | +| win32-x64 | [x] | [x] | [x] | [x] | [x] | [ ] | E-M3-WINDOWS-X64-BASELINE-CI-001 | +| win32-arm64 | [x] | [x] | [x] | [x] | [ ] | [ ] | E-M3-WINDOWS-ARM64-BASELINE-CI-RED-001 | Rules: @@ -7415,6 +7416,117 @@ diff --check`. - Follow-up: provision or select a native arm64 build-26100 snapshot/runner before closing this cell; keep the tuple disabled and do not weaken the exact oldest-floor oracle. +- Repeat evidence: exact-head run + [29379227209](https://github.com/stablyai/orca/actions/runs/29379227209), artifact job + [87239051368](https://github.com/stablyai/orca/actions/runs/29379227209/job/87239051368), and baseline + job [87240257136](https://github.com/stablyai/orca/actions/runs/29379227209/job/87240257136) reproduce + the same bounded result at `cc4c45c8981fd87b5681e8c0be7b3b06f2cfab22`: native build, equality, + upload, download, verification, PTY/watcher smoke, and 48,361,472-byte RSS pass; observed build + 26200 returns `osBuild: false` and the job fails closed. Artifact ID `8329167066`, 33,084,426 bytes, + Actions digest `4956ad608ede84372a393248dc90c03b13bd5e28345d8c3940df0419b9fdb192`; + archive SHA-256 `87a9c749f6aced6d2c63a3f2b80033dfa3ecc7524c2f232baaa199340c562cf1`. + +### E-M3-LINUX-NATIVE-USERLAND-CI-001 — Native Linux producers pass the oldest userland + +- Date: 2026-07-14 (job timestamps 2026-07-15 UTC) +- Owner: Codex implementation owner +- Source/run: exact draft-PR head `cc4c45c8981fd87b5681e8c0be7b3b06f2cfab22`, containing + implementation commit `05b3a4b18`; Actions run + [29379227209](https://github.com/stablyai/orca/actions/runs/29379227209). Linux x64 artifact job + [87239051400](https://github.com/stablyai/orca/actions/runs/29379227209/job/87239051400), Linux arm64 + artifact job [87239051434](https://github.com/stablyai/orca/actions/runs/29379227209/job/87239051434), + x64 userland job [87240706426](https://github.com/stablyai/orca/actions/runs/29379227209/job/87240706426), + and arm64 userland job + [87240706430](https://github.com/stablyai/orca/actions/runs/29379227209/job/87240706430) all pass. +- Aggregate status: the artifact workflow concludes `failure` only because the separately required + Windows arm64 build-26100 floor fails closed on hosted build 26200. Exact-head PR Checks + [29379227222](https://github.com/stablyai/orca/actions/runs/29379227222) and Golden E2E Experiment + [29379227283](https://github.com/stablyai/orca/actions/runs/29379227283) both pass with no failed jobs. +- Runners/builders: GitHub-hosted native `ubuntu-24.04` / `ubuntu24` `20260705.232.1` x64 and + `ubuntu-24.04-arm` / `ubuntu24-arm64` `20260706.52.2` arm64. Digest-pinned Rocky bases are + `2d05a926...aa275d` x64 and `3c2d0ce1...61f1a0` arm64; produced builder IDs are + `sha256:4e260f8acb74ac4800e31af90160aac0c9eed00c5fc4e8cb1aa83d0f1b1a8f4b` and + `sha256:a2f683970a2fcef419610f77bdc0af332d991dc0cf19fa4f239771dfe5b06a94`. +- Exact evidence commands: + + ```sh + gh run view 29379227209 --repo stablyai/orca \ + --json headSha,status,conclusion,createdAt,updatedAt,url,jobs + gh run view 29379227209 --repo stablyai/orca --log | \ + rg 'requested_runner=|resolved_image_|runner_arch=|linux_floor_|"contentId"|"qualified"|"residualGaps"' + gh api repos/stablyai/orca/actions/runs/29379227209/artifacts --paginate + gh run download 29379227209 --repo stablyai/orca \ + --dir /private/tmp/orca-8450-run-29379227209-artifacts + # Repeat the fail-closed jq/shasum audit from E-M3-METADATA-CI-RED-003 with: + # evidence=/private/tmp/orca-8450-run-29379227209-artifacts + # commit=cc4c45c8981fd87b5681e8c0be7b3b06f2cfab22 + # run_id=29379227209 + # Extract each exact archive, then repeat the closure verifier command from + # E-M3-RUNTIME-CLOSURE-LOCAL-001 against all six extracted directories. + ``` + +- Result: PASS. Both offline, non-root builders preserve the read-only/no-network/capability/resource + envelope; build twice; inspect archive/tree; execute bundled Node v24.18.0, native PTY + input/resize/exit, and five watcher events twice; compare runtime/archive/identity/SPDX/provenance + exactly; and upload. Artifact job wall times were 6m49s x64 and 11m57s arm64, including builder + preparation. Clean x64 builds took 67,398.98/64,050.97 ms with 55,918,592/55,955,456-byte smoke + RSS; clean arm64 builds took 75,230.87/74,661.31 ms with 52,195,328/52,207,616-byte smoke RSS. +- Supplemental userland result: PASS after download and fresh byte/tree verification. Both execute in + glibc 2.28/libstdc++ 6.0.25 Rocky userland and report `qualified: true`; x64 PTY/watcher smoke takes + 161.43 ms at 55,611,392 RSS bytes and arm64 takes 161.85 ms at 51,658,752 RSS bytes. Both retain the + explicit kernel residual: observed shared runner kernel `6.17.0-1018-azure`, not required 4.18. +- Downloaded artifacts/direct audit: exact cardinality, archive/identity/provenance/SPDX hashes, + archive-scoped namespace, one owner per file, eight dependency relationships, exact + commit/run/builder/runner/toolchain identities, prohibited-content rules, and extracted-tree + closure all pass for all six run artifacts. Linux x64 artifact `8329137098`, 29,290,715 bytes, + Actions digest `4859448ad465600836700884449b56b65cc65b13e71588813a5341c7eee2ef3b`, + content ID `fc63ca342a5990f460ec6d72262a8542173dab20ce03c9b9cfb755b1c6057e6d`, archive SHA-256 + `c771c13d23fb7384ad0b018cb4a91e04066d53d07b021fa2ff4e0ae55f701467`. Linux arm64 artifact + `8329223012`, 28,216,772 bytes, Actions digest + `f3a834de03714c4bd702d933261cc8c3c8f3ea6472b97d74737904d5b7461a06`, content ID + `96f07f62af9b35304bb8ca0870ca4d8095e059bfa61dd1bc57e81b20f3fbca67`, archive SHA-256 + `ea52c7fadbc100e055e489d14ddf01daca5f90b1f83cdff93c1f3d728cfcaa34`. +- Six-artifact audit controls: + + | Tuple | Artifact ID | Actions digest | Archive SHA-256 | Content ID prefix | Files | + | ----------------- | ------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ----------------- | ----- | + | linux-x64-glibc | `8329137098` | `4859448ad465600836700884449b56b65cc65b13e71588813a5341c7eee2ef3b` | `c771c13d23fb7384ad0b018cb4a91e04066d53d07b021fa2ff4e0ae55f701467` | `fc63ca34…` | 34 | + | linux-arm64-glibc | `8329223012` | `f3a834de03714c4bd702d933261cc8c3c8f3ea6472b97d74737904d5b7461a06` | `ea52c7fadbc100e055e489d14ddf01daca5f90b1f83cdff93c1f3d728cfcaa34` | `96f07f62…` | 34 | + | darwin-x64 | `8329119914` | `d86a6deac1c5f6b38a1824197b19de69c5f5df6bc7081266a636e86cfae34a87` | `470b69a5317d212cab99cf1dc12841b09e0984c70cfbc90c3f46be040a263374` | `585ea603…` | 35 | + | darwin-arm64 | `8329097437` | `d2200b01c983afbc2b7ca0d08d36f9e7962932113c67ab604c1072488b6ebb26` | `54a2fb49a0bc63c04622525bada57ccfa4cf7884e09bfb88c4c87f30a363616d` | `40ff5d20…` | 35 | + | win32-x64 | `8329104979` | `3168b4b29c635502ceb5320fc1e96a19a394b2a36ea5f8083c8502a81a385063` | `e8c1165df331602027f548228f8b09791d27aa353d829c575b9b6dbf86833670` | `7ddad668…` | 42 | + | win32-arm64 | `8329167066` | `4956ad608ede84372a393248dc90c03b13bd5e28345d8c3940df0419b9fdb192` | `87a9c749f6aced6d2c63a3f2b80033dfa3ecc7524c2f232baaa199340c562cf1` | `2955cec7…` | 42 | + +- Oracle proved: the exact native Linux producer bytes are compatible with the declared oldest + glibc/libstdc++ userland on both architectures; the original Ubuntu-linked ABI defect and the + runner bind-mount defect are closed without weakening isolation. +- Does not prove: exact kernel 4.18, SSH transfer/install, native trust, release aggregation, + packaged desktop use, fallback/performance, or an enabled tuple. +- Follow-up: retain both tuples disabled until kernel 4.18, native trust, and the remaining required + live SSH evidence pass. + +### E-M3-WINDOWS-X64-BASELINE-CI-001 — Windows x64 passes the declared Server floor + +- Date: 2026-07-14 (job timestamps 2026-07-15 UTC) +- Owner: Codex implementation owner +- Source/run: exact head `cc4c45c8981fd87b5681e8c0be7b3b06f2cfab22`, Actions run + [29379227209](https://github.com/stablyai/orca/actions/runs/29379227209), artifact job + [87239051373](https://github.com/stablyai/orca/actions/runs/29379227209/job/87239051373), and baseline + job [87240257128](https://github.com/stablyai/orca/actions/runs/29379227209/job/87240257128). +- Runner/result: GitHub-hosted native `windows-2022` / `win22` `20260706.237.1` x64 observed exact + build `10.0.20348`; archive/tree verification and bundled Node/PTY/watcher smoke pass, then the full + baseline evaluator returns `qualified: true` with no residual gaps. Smoke took 5,382.17 ms at + 50,561,024 RSS bytes; the baseline job wall time was 1m29s. +- Artifact/audit: artifact `8329104979`, 37,034,298 bytes, Actions digest + `3168b4b29c635502ceb5320fc1e96a19a394b2a36ea5f8083c8502a81a385063`; content ID + `7ddad668780ce5b2592d86afcebf9d897172bdf07c618ae238b5d51eebfe1596`; archive SHA-256 + `e8c1165df331602027f548228f8b09791d27aa353d829c575b9b6dbf86833670`. The independent all-six + metadata and extracted-closure audit passes. +- Does not prove: native signature/trust, Windows arm64 build 26100, SSH transfer/install, release + aggregation, fallback/performance, or an enabled tuple. +- Follow-up: keep the tuple disabled until native trust and the remaining required live SSH evidence + pass. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -7472,12 +7584,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Correct the native Linux runner bind-mount boundary without weakening isolation: execute the offline -builder as the runner UID/GID, use an explicit mode-1777 tmpfs, and retain the read-only root, -no-network, capability, privilege, process, memory, and CPU restrictions. Prove that correction on -both native GitHub architectures, then run and audit the supplemental oldest-userland cells. Keep the -exact kernel 4.18, unavailable macOS/Windows floor, and target-native signing/trust cells open before -connecting any artifact consumer or enabling any tuple. +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. 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 From 9bdae7f5bd7d5df838ab8c59af9346fc2282443c Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 21:02:44 -0400 Subject: [PATCH 081/267] build(ssh): define native signing plan Co-authored-by: Orca --- .../ssh-relay-runtime-native-signing-plan.mjs | 153 ++++++++++++++++++ ...relay-runtime-native-signing-plan.test.mjs | 153 ++++++++++++++++++ 2 files changed, 306 insertions(+) create mode 100644 config/scripts/ssh-relay-runtime-native-signing-plan.mjs create mode 100644 config/scripts/ssh-relay-runtime-native-signing-plan.test.mjs diff --git a/config/scripts/ssh-relay-runtime-native-signing-plan.mjs b/config/scripts/ssh-relay-runtime-native-signing-plan.mjs new file mode 100644 index 00000000000..4395daf4f93 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-native-signing-plan.mjs @@ -0,0 +1,153 @@ +import { readFile, stat } from 'node:fs/promises' +import { resolve } from 'node:path' +import { pathToFileURL } from 'node:url' + +import { assertSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' + +const MAX_IDENTITY_BYTES = 4 * 1024 * 1024 +const NATIVE_ROLES = new Set(['node', 'node-pty-native', 'parcel-watcher-native', 'native-runtime']) + +const PLATFORM_CONTRACTS = Object.freeze({ + linux: { + policy: 'linux-hash-only-v1', + nodePath: 'bin/node', + verificationCount: 3, + signingAction: null, + signingCount: 0 + }, + darwin: { + policy: 'apple-developer-id-v1', + nodePath: 'bin/node', + verificationCount: 4, + signingAction: 'developer-id-required', + signingCount: 3 + }, + win32: { + policy: 'signpath-authenticode-v1', + nodePath: 'bin/node.exe', + verificationCount: 6, + signingAction: 'signpath-if-unsigned', + signingCount: 5 + } +}) + +function platformForTuple(tupleId) { + if (tupleId.startsWith('linux-')) { + return 'linux' + } + if (tupleId.startsWith('darwin-')) { + return 'darwin' + } + if (tupleId.startsWith('win32-')) { + return 'win32' + } + throw new Error(`Runtime signing plan does not support tuple: ${tupleId}`) +} + +function assertNativeExtension(platform, entry) { + if (entry.role === 'node') { + return + } + const accepted = + platform === 'linux' + ? entry.path.endsWith('.node') + : platform === 'darwin' + ? entry.path.endsWith('.node') || + entry.path.endsWith('.dylib') || + entry.path.endsWith('/spawn-helper') + : entry.path.endsWith('.node') || entry.path.endsWith('.dll') || entry.path.endsWith('.exe') + if (!accepted) { + throw new Error(`Runtime signing plan rejects native file extension: ${entry.path}`) + } +} + +function signingFile(entry, action) { + return { path: entry.path, role: entry.role, sourceSha256: entry.sha256, action } +} + +function verificationFile(entry) { + return { path: entry.path, role: entry.role, sourceSha256: entry.sha256 } +} + +export function buildSshRelayRuntimeNativeSigningPlan(identity) { + if (!identity || typeof identity !== 'object' || Array.isArray(identity)) { + throw new Error('Runtime signing plan requires an identity object') + } + assertSshRelayRuntimeClosureEntries(identity) + const platform = platformForTuple(identity.tupleId) + if (identity.os !== platform) { + throw new Error(`Runtime signing plan tuple and OS disagree: ${identity.tupleId}`) + } + const contract = PLATFORM_CONTRACTS[platform] + const nativeFiles = identity.entries + .filter((entry) => entry.type === 'file' && NATIVE_ROLES.has(entry.role)) + // Why: signing input order must match the portable byte ordering used by runtime identity. + .sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0)) + const nodeFiles = nativeFiles.filter((entry) => entry.role === 'node') + if (nodeFiles.length !== 1 || nodeFiles[0].path !== contract.nodePath) { + // Why: the signed official Node input is immutable and must never enter Orca's signing flow. + throw new Error(`Runtime signing plan requires exact immutable Node path: ${contract.nodePath}`) + } + for (const entry of nativeFiles) { + if (entry.mode !== 0o755 || typeof entry.sha256 !== 'string') { + throw new Error(`Runtime signing plan requires executable authenticated bytes: ${entry.path}`) + } + assertNativeExtension(platform, entry) + } + if (nativeFiles.length !== contract.verificationCount) { + throw new Error(`Runtime signing plan has unexpected native file count for ${identity.tupleId}`) + } + const signingCandidates = contract.signingAction + ? nativeFiles + .filter((entry) => entry.role !== 'node') + .map((entry) => signingFile(entry, contract.signingAction)) + : [] + if (signingCandidates.length !== contract.signingCount) { + throw new Error( + `Runtime signing plan has unexpected signing target count for ${identity.tupleId}` + ) + } + return { + tupleId: identity.tupleId, + platform, + policy: contract.policy, + immutableVendorFiles: [signingFile(nodeFiles[0], 'preserve-exact-bytes')], + signingCandidates, + verificationFiles: nativeFiles.map(verificationFile) + } +} + +export function parseSshRelayRuntimeNativeSigningPlanArguments(argv) { + if (argv.length !== 2 || argv[0] !== '--identity' || !argv[1]) { + throw new Error( + 'Usage: node config/scripts/ssh-relay-runtime-native-signing-plan.mjs --identity ' + ) + } + return { identityPath: resolve(argv[1]) } +} + +export async function readSshRelayRuntimeNativeSigningIdentity(identityPath) { + const metadata = await stat(identityPath) + if (!metadata.isFile() || metadata.size > MAX_IDENTITY_BYTES) { + throw new Error('Runtime signing identity must be one bounded regular file') + } + const source = await readFile(identityPath, 'utf8') + try { + return JSON.parse(source) + } catch (error) { + throw new Error(`Runtime signing identity is not valid JSON: ${error.message}`) + } +} + +async function main() { + const { identityPath } = parseSshRelayRuntimeNativeSigningPlanArguments(process.argv.slice(2)) + const identity = await readSshRelayRuntimeNativeSigningIdentity(identityPath) + console.log(JSON.stringify(buildSshRelayRuntimeNativeSigningPlan(identity), null, 2)) +} + +if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) { + main().catch((error) => { + console.error(`SSH relay runtime native signing plan failed: ${error}`) + process.exitCode = 1 + }) +} diff --git a/config/scripts/ssh-relay-runtime-native-signing-plan.test.mjs b/config/scripts/ssh-relay-runtime-native-signing-plan.test.mjs new file mode 100644 index 00000000000..9bf10c66197 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-native-signing-plan.test.mjs @@ -0,0 +1,153 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { describe, expect, it } from 'vitest' + +import { expectedSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { + buildSshRelayRuntimeNativeSigningPlan, + parseSshRelayRuntimeNativeSigningPlanArguments, + readSshRelayRuntimeNativeSigningIdentity +} from './ssh-relay-runtime-native-signing-plan.mjs' + +const DIGEST = `sha256:${'a'.repeat(64)}` + +function identityFor(tupleId) { + const os = tupleId.startsWith('linux-') + ? 'linux' + : tupleId.startsWith('darwin-') + ? 'darwin' + : 'win32' + return { + tupleId, + os, + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries: expectedSshRelayRuntimeClosureEntries(tupleId).map((entry) => + entry.type === 'file' ? { ...entry, size: 1, sha256: DIGEST } : entry + ) + } +} + +function paths(entries) { + return entries.map((entry) => entry.path) +} + +describe('SSH relay runtime native signing plan', () => { + it('keeps official Linux Node immutable and selects no signing targets', () => { + for (const tupleId of ['linux-x64-glibc', 'linux-arm64-glibc']) { + const plan = buildSshRelayRuntimeNativeSigningPlan(identityFor(tupleId)) + expect(plan.policy).toBe('linux-hash-only-v1') + expect(paths(plan.immutableVendorFiles)).toEqual(['bin/node']) + expect(plan.signingCandidates).toEqual([]) + expect(paths(plan.verificationFiles)).toEqual([ + 'bin/node', + `node_modules/@parcel/watcher-linux-${tupleId.includes('arm64') ? 'arm64' : 'x64'}-glibc/watcher.node`, + 'node_modules/node-pty/build/Release/pty.node' + ]) + } + }) + + it('selects every non-Node macOS native file for Developer ID signing', () => { + for (const tupleId of ['darwin-x64', 'darwin-arm64']) { + const architecture = tupleId.endsWith('arm64') ? 'arm64' : 'x64' + const plan = buildSshRelayRuntimeNativeSigningPlan(identityFor(tupleId)) + expect(plan.policy).toBe('apple-developer-id-v1') + expect(paths(plan.immutableVendorFiles)).toEqual(['bin/node']) + expect(paths(plan.signingCandidates)).toEqual([ + `node_modules/@parcel/watcher-darwin-${architecture}/watcher.node`, + 'node_modules/node-pty/build/Release/pty.node', + 'node_modules/node-pty/build/Release/spawn-helper' + ]) + expect( + plan.signingCandidates.every((entry) => entry.action === 'developer-id-required') + ).toBe(true) + expect(paths(plan.verificationFiles)).toEqual(['bin/node', ...paths(plan.signingCandidates)]) + } + }) + + it('selects every non-Node Windows PE as a SignPath candidate', () => { + for (const tupleId of ['win32-x64', 'win32-arm64']) { + const architecture = tupleId.endsWith('arm64') ? 'arm64' : 'x64' + const plan = buildSshRelayRuntimeNativeSigningPlan(identityFor(tupleId)) + expect(plan.policy).toBe('signpath-authenticode-v1') + expect(paths(plan.immutableVendorFiles)).toEqual(['bin/node.exe']) + expect(paths(plan.signingCandidates)).toEqual([ + `node_modules/@parcel/watcher-win32-${architecture}/watcher.node`, + 'node_modules/node-pty/build/Release/conpty.node', + 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe', + 'node_modules/node-pty/build/Release/conpty/conpty.dll', + 'node_modules/node-pty/build/Release/conpty_console_list.node' + ]) + expect(plan.signingCandidates.every((entry) => entry.action === 'signpath-if-unsigned')).toBe( + true + ) + expect(paths(plan.verificationFiles)).toEqual([ + 'bin/node.exe', + ...paths(plan.signingCandidates) + ]) + } + }) + + it('fails closed on tuple, role, path, and native-count drift', () => { + const wrongOs = identityFor('darwin-arm64') + wrongOs.os = 'linux' + expect(() => buildSshRelayRuntimeNativeSigningPlan(wrongOs)).toThrow(/tuple and OS/i) + + const wrongRole = identityFor('darwin-arm64') + wrongRole.entries.find((entry) => entry.path.endsWith('/watcher.node')).role = + 'runtime-javascript' + expect(() => buildSshRelayRuntimeNativeSigningPlan(wrongRole)).toThrow(/unexpected role/i) + + const wrongPath = identityFor('win32-x64') + wrongPath.entries.find((entry) => entry.path.endsWith('/conpty.node')).path += '.txt' + expect(() => buildSshRelayRuntimeNativeSigningPlan(wrongPath)).toThrow(/missing required file/i) + + const duplicate = identityFor('linux-x64-glibc') + duplicate.entries.push( + structuredClone(duplicate.entries.find((entry) => entry.role === 'node')) + ) + expect(() => buildSshRelayRuntimeNativeSigningPlan(duplicate)).toThrow(/duplicate file/i) + }) + + it('accepts only one purpose-named identity argument', () => { + expect( + parseSshRelayRuntimeNativeSigningPlanArguments(['--identity', './identity.json']) + ).toEqual({ + identityPath: expect.stringMatching(/identity[.]json$/) + }) + expect(() => parseSshRelayRuntimeNativeSigningPlanArguments([])).toThrow(/usage/i) + expect(() => + parseSshRelayRuntimeNativeSigningPlanArguments(['--identity', 'a', '--identity', 'b']) + ).toThrow(/usage/i) + }) + + it('reads only one bounded regular JSON identity file', async () => { + const directory = await mkdtemp(join(tmpdir(), 'ssh-relay-native-signing-plan-')) + try { + const validPath = join(directory, 'valid.json') + await writeFile(validPath, JSON.stringify(identityFor('linux-x64-glibc'))) + await expect(readSshRelayRuntimeNativeSigningIdentity(validPath)).resolves.toMatchObject({ + tupleId: 'linux-x64-glibc' + }) + + const malformedPath = join(directory, 'malformed.json') + await writeFile(malformedPath, '{') + await expect(readSshRelayRuntimeNativeSigningIdentity(malformedPath)).rejects.toThrow( + /valid JSON/i + ) + + const oversizedPath = join(directory, 'oversized.json') + await writeFile(oversizedPath, Buffer.alloc(4 * 1024 * 1024 + 1)) + await expect(readSshRelayRuntimeNativeSigningIdentity(oversizedPath)).rejects.toThrow( + /bounded/i + ) + await expect(readSshRelayRuntimeNativeSigningIdentity(directory)).rejects.toThrow( + /regular file/i + ) + } finally { + await rm(directory, { recursive: true, force: true }) + } + }) +}) From 8ac54159c6f9d95858358f11746117c5480440cf Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 21:03:23 -0400 Subject: [PATCH 082/267] docs(ssh): record native signing plan proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 3 +- ...github-release-implementation-checklist.md | 109 +++++++++++++++++- 2 files changed, 110 insertions(+), 2 deletions(-) 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 5863d6e2397..4f3003d92e3 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 @@ -28,7 +28,8 @@ complete a box. 6.0.25 execution, bundled Node, PTY, and watcher smoke. - Windows x64 passes its declared oldest-floor job. The hosted arm64 runner is build 26200, not the required build 26100, so its otherwise successful artifact/runtime smoke does not close that cell. - - Next proof: kernel 4.18, macOS 13.5, Windows arm64 build 26100, and native signing/trust. + - Active: native-signing plan commit `9bdae7f5b` passes locally; exact-head CI is next. + - 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. ## Work packages, in required order 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 c61afba921d..ed69d9932e4 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps 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** — exact-head run [29379227209](https://github.com/stablyai/orca/actions/runs/29379227209) proves the non-root correction and both digest-pinned Linux builders on native x64/arm64 through two clean builds, equality, archive/tree inspection, bundled Node/PTY/watcher smoke, upload, download, direct metadata/closure audit, and supplemental glibc 2.28/libstdc++ 6.0.25 execution (E-M3-LINUX-NATIVE-USERLAND-CI-001); Windows x64 qualifies on exact Server 2022 build 20348 (E-M3-WINDOWS-X64-BASELINE-CI-001), while native Windows arm64 artifacts and smoke pass but hosted build 26200 again fails closed against required build 26100 (E-M3-WINDOWS-ARM64-BASELINE-CI-RED-001); exact kernel 4.18, macOS 13.5, Windows arm64 build 26100, and native signing/trust remain open; no tuple is enabled or published.
      +Session checkpoint: **In progress — 2026-07-14, Codex implementation owner** — implementation commit `9bdae7f5b` defines the credential-free native-signing plan contract and is locally green against hostile fixtures and all six downloaded exact-run identities under E-M3-NATIVE-SIGNING-PLAN-LOCAL-001; it preserves official Node bytes, selects the exact non-Node native signing set, enumerates every native verification file, and rejects closure/path/role/count/extension drift before credentials or signing side effects. Exact-head CI and every credentialed native-signing/trust cell remain open; no tuple is enabled or published.
      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) @@ -659,6 +659,17 @@ runner, static symbol scan, or container may add evidence but cannot silently st unavailable qualifying snapshot. Every tuple remains disabled and no production consumer is connected. +**Active native-trust contract slice — 2026-07-14, Codex implementation owner:** before any +credentialed Apple or SignPath job, derive one exact signing plan from the builder-enforced runtime +identity. The plan must keep the official Node executable immutable, enumerate every native file +that requires platform verification, select no Linux signing targets, select every non-Node macOS +native file for Developer ID signing, and select every non-Node Windows PE file as a SignPath +candidate whose already-valid upstream signature may be preserved. Unknown roles/extensions, +duplicates, missing expected files, or identity/closure drift fail before credentials or signing +side effects. This package is contract/test-only and cannot sign, publish, aggregate, or enable a +tuple. The local contract and all-six actual-identity proof pass under +E-M3-NATIVE-SIGNING-PLAN-LOCAL-001; exact-head CI is the next gate. + **Baseline correction — 2026-07-14, Codex implementation owner:** E-M3-LINUX-BASELINE-LOCAL-RED-001 proves the existing Linux x64 candidate cannot load its patched `node-pty` on Rocky 8.9/glibc 2.28 because the Ubuntu 24.04 producer emitted references to @@ -7527,6 +7538,102 @@ diff --check`. - Follow-up: keep the tuple disabled until native trust and the remaining required live SSH evidence pass. +### E-M3-NATIVE-SIGNING-PLAN-SORT-LOCAL-RED-001 — Locale collation changes Windows signing order + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: local macOS arm64 shell at Git HEAD + `c6a7277cc6f56d453662ebd373e9e8e4e278f4ac`; no runtime or signing credentials were used. +- Command: + + ```sh + node - <<'NODE' + const paths = [ + 'node_modules/node-pty/build/Release/conpty.node', + 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe', + 'node_modules/node-pty/build/Release/conpty/conpty.dll', + 'node_modules/node-pty/build/Release/conpty_console_list.node' + ] + console.log(JSON.stringify({ + locale: [...paths].sort((left, right) => left.localeCompare(right)), + portable: [...paths].sort((left, right) => left < right ? -1 : left > right ? 1 : 0) + }, null, 2)) + NODE + ``` + +- Result: expected RED control. Locale collation ordered `conpty_console_list.node`, `conpty.node`, + `conpty/conpty.dll`, then `conpty/OpenConsole.exe`; portable byte ordering produced + `conpty.node`, `conpty/OpenConsole.exe`, `conpty/conpty.dll`, then + `conpty_console_list.node`, matching `canonicalSshRelayRuntimeIdentityBytes`. +- Correction/oracle: the signing plan uses explicit `<`/`>` path comparison, and the Windows x64 + and arm64 expected-path assertions lock that order. Locale-sensitive `localeCompare` is not used. +- Does not prove: target-native execution, any signature, native trust, release aggregation, SSH + behavior, or an enabled tuple. + +### E-M3-NATIVE-SIGNING-PLAN-LOCAL-001 — Exact credential-free signing plans pass for all six identities + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: implementation commit `9bdae7f5bd7d5df838ab8c59af9346fc2282443c`, based on + `c6a7277cc6f56d453662ebd373e9e8e4e278f4ac`; downloaded identities are from exact-head Actions run + [29379227209](https://github.com/stablyai/orca/actions/runs/29379227209) and were already audited + under E-M3-LINUX-NATIVE-USERLAND-CI-001. +- Runner/remote/network: local macOS arm64 shell, Node v26.0.0 and pnpm 10.24.0; no remote, signing + service, credential, publication, or network access was used by the plan command. The source + artifacts were produced on the six native GitHub runner families recorded by the prior evidence. +- Commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-native-signing-plan.test.mjs + 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 + pnpm exec oxfmt --check \ + config/scripts/ssh-relay-runtime-native-signing-plan.mjs \ + config/scripts/ssh-relay-runtime-native-signing-plan.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 + for identity in /private/tmp/orca-8450-run-29379227209-artifacts/ssh-relay-runtime-*/*.identity.json; do + node config/scripts/ssh-relay-runtime-native-signing-plan.mjs --identity "$identity" + done + ``` + +- Static result: PASS. The purpose-named contract passed 1 file/6 tests in 944 ms; the aggregate SSH + relay artifact suite passed 23 files/107 tests in 6.91 seconds. Typecheck, full lint, reliability + gates, max-lines (355 grandfathered suppressions and no new bypass), bundled-skill verification, + localization catalog/coverage, focused formatting, and `git diff --check` passed. Lint emitted only + existing unrelated warnings; the local Node 26 shell emitted the expected Node-24 engine warning. +- Actual-identity result: PASS. Every plan retained one official Node executable as + `preserve-exact-bytes`, copied the exact identity digest into `sourceSha256`, and produced this + closed cardinality: + + | Tuple | Policy | Immutable vendor files | Signing candidates | Verification files | + | ----------------- | -------------------------- | ---------------------- | ------------------ | ------------------ | + | linux-x64-glibc | `linux-hash-only-v1` | 1 | 0 | 3 | + | linux-arm64-glibc | `linux-hash-only-v1` | 1 | 0 | 3 | + | darwin-x64 | `apple-developer-id-v1` | 1 | 3 | 4 | + | darwin-arm64 | `apple-developer-id-v1` | 1 | 3 | 4 | + | win32-x64 | `signpath-authenticode-v1` | 1 | 5 | 6 | + | win32-arm64 | `signpath-authenticode-v1` | 1 | 5 | 6 | + +- Fail-closed coverage: wrong tuple/OS, role/path/count/extension drift, duplicates, missing exact + Node, non-executable native entries, malformed invocation, malformed JSON, directories, and input + larger than 4 MiB reject. Every native file is present in `verificationFiles`; Linux selects no + signing target, macOS requires Developer ID for all three non-Node native files, and Windows sends + all five non-Node PE files through the preserve-valid-upstream-signature SignPath policy. +- Oracle proved: one bounded, deterministic, credential-free contract derives the complete platform + signing/verification plan from the builder-enforced closure before signing side effects, while + keeping official Node outside Orca signing. +- Does not prove: Apple or SignPath credential wiring, returned signed-byte hashes, Gatekeeper, + quarantine, notarized containing-app provenance, Authenticode signer policy, Defender, WDAC, exact + oldest-OS snapshots, release aggregation, SSH transfer/install, packaged desktop use, fallback, + performance, or an enabled tuple. +- Follow-up: commit this evidence separately, require exact-head CI, then make native signing staging + consume this plan without weakening any remaining credential/snapshot gate. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 9c0357235867cb2ff1ab8cbeed4901e6f013cf1d Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 21:21:29 -0400 Subject: [PATCH 083/267] ci(ssh): run native signing plan contracts Co-authored-by: Orca --- .github/workflows/ssh-relay-runtime-artifacts.yml | 8 ++++++++ config/scripts/ssh-relay-runtime-workflow.test.mjs | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index b004dd4f76c..3ae99261ad2 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -125,6 +125,8 @@ jobs: node --check config/scripts/ssh-relay-runtime-provenance.test.mjs node --check config/scripts/ssh-relay-runtime-toolchain.mjs node --check config/scripts/ssh-relay-runtime-toolchain.test.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-plan.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-plan.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 \ @@ -137,6 +139,7 @@ jobs: config/scripts/ssh-relay-runtime-baseline.test.mjs \ config/scripts/ssh-relay-runtime-build.test.mjs \ config/scripts/ssh-relay-runtime-linux-build-evidence.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-plan.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 \ @@ -379,6 +382,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime toolchain script syntax check failed' } node --check config/scripts/ssh-relay-runtime-toolchain.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime toolchain test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-native-signing-plan.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime native signing plan script syntax check failed' } + node --check config/scripts/ssh-relay-runtime-native-signing-plan.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime native signing plan 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 ` @@ -391,6 +398,7 @@ jobs: config/scripts/ssh-relay-runtime-baseline.test.mjs ` config/scripts/ssh-relay-runtime-build.test.mjs ` config/scripts/ssh-relay-runtime-linux-build-evidence.test.mjs ` + config/scripts/ssh-relay-runtime-native-signing-plan.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-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 7f583a961c1..8578c1b7368 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -81,6 +81,10 @@ describe('SSH relay runtime artifact workflow', () => { expect(source.match(/ssh-relay-runtime-sbom\.test\.mjs/g)).toHaveLength(4) expect(source.match(/ssh-relay-runtime-provenance\.test\.mjs/g)).toHaveLength(4) expect(source.match(/ssh-relay-runtime-toolchain\.test\.mjs/g)).toHaveLength(4) + expect(source.match(/ssh-relay-runtime-native-signing-plan\.test\.mjs/g)).toHaveLength(4) + expect( + source.match(/node --check config\/scripts\/ssh-relay-runtime-native-signing-plan\.mjs/g) + ).toHaveLength(2) expect( source.match(/node --check config\/scripts\/ssh-relay-runtime-closure\.mjs/g) ).toHaveLength(2) From 0f589ea299e9363a3988a9bc92b721f23c1ce1f4 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 21:22:53 -0400 Subject: [PATCH 084/267] docs(ssh): record native contract CI gap Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 3 +- ...github-release-implementation-checklist.md | 88 ++++++++++++++++++- 2 files changed, 88 insertions(+), 3 deletions(-) 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 4f3003d92e3..33cb807e904 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 @@ -28,7 +28,8 @@ complete a box. 6.0.25 execution, bundled Node, PTY, and watcher smoke. - Windows x64 passes its declared oldest-floor job. The hosted arm64 runner is build 26200, not the required build 26100, so its otherwise successful artifact/runtime smoke does not close that cell. - - Active: native-signing plan commit `9bdae7f5b` passes locally; exact-head CI is next. + - Active: native-signing plan commit `9bdae7f5b` and CI-wiring correction `9c0357235` pass locally; + replacement exact-head CI is next. - 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. 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 ed69d9932e4..5f9b87e7cd8 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps 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** — implementation commit `9bdae7f5b` defines the credential-free native-signing plan contract and is locally green against hostile fixtures and all six downloaded exact-run identities under E-M3-NATIVE-SIGNING-PLAN-LOCAL-001; it preserves official Node bytes, selects the exact non-Node native signing set, enumerates every native verification file, and rejects closure/path/role/count/extension drift before credentials or signing side effects. Exact-head CI and every credentialed native-signing/trust cell remain open; no tuple is enabled or published.
      +Session checkpoint: **In progress — 2026-07-14, Codex implementation owner** — implementation commit `9bdae7f5b` defines the locally green credential-free native-signing plan contract, but exact-head run [29380684866](https://github.com/stablyai/orca/actions/runs/29380684866) exposed that the native workflow's explicit command omitted its new 6-test suite (E-M3-NATIVE-SIGNING-PLAN-CI-RED-001). Correction commit `9c0357235` explicitly syntax-checks and executes it in both native job families and passes local static gates under E-M3-NATIVE-SIGNING-PLAN-WORKFLOW-LOCAL-001; a replacement exact head remains required. Every credentialed native-signing/trust cell is open and no tuple is enabled or published.
      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) @@ -668,7 +668,10 @@ candidate whose already-valid upstream signature may be preserved. Unknown roles duplicates, missing expected files, or identity/closure drift fail before credentials or signing side effects. This package is contract/test-only and cannot sign, publish, aggregate, or enable a tuple. The local contract and all-six actual-identity proof pass under -E-M3-NATIVE-SIGNING-PLAN-LOCAL-001; exact-head CI is the next gate. +E-M3-NATIVE-SIGNING-PLAN-LOCAL-001. E-M3-NATIVE-SIGNING-PLAN-CI-RED-001 proves the first exact-head +workflow omitted the new suite despite otherwise healthy builds; explicitly wiring it into both +native job families is implemented at `9c0357235` and locally green under +E-M3-NATIVE-SIGNING-PLAN-WORKFLOW-LOCAL-001; passing a replacement exact head is the active gate. **Baseline correction — 2026-07-14, Codex implementation owner:** E-M3-LINUX-BASELINE-LOCAL-RED-001 proves the existing Linux x64 candidate cannot load its patched @@ -7634,6 +7637,87 @@ diff --check`. - Follow-up: commit this evidence separately, require exact-head CI, then make native signing staging consume this plan without weakening any remaining credential/snapshot gate. +### E-M3-NATIVE-SIGNING-PLAN-CI-RED-001 — First exact-head workflow omitted the new suite + +- Date: 2026-07-14 (run timestamps 2026-07-15 UTC) +- Owner: Codex implementation owner +- Source/run: exact draft-PR head `8ac54159c6f9d95858358f11746117c5480440cf`, containing + implementation commit `9bdae7f5bd7d5df838ab8c59af9346fc2282443c`; Actions run + [29380684866](https://github.com/stablyai/orca/actions/runs/29380684866). +- Commands: + + ```sh + gh run view 29380684866 --repo stablyai/orca \ + --json headSha,status,conclusion,createdAt,updatedAt,url,jobs + gh run view 29380684866 --repo stablyai/orca --job 87243480614 --log | \ + rg 'Run runtime artifact contract tests|Test Files|Tests|Duration|24.18.0' + gh run view 29380684866 --repo stablyai/orca --job 87243480598 --log | \ + rg 'Run runtime artifact contract tests|Test Files|Tests|Duration' + ``` + +- Result: expected RED evidence for the new contract's CI gate. All six native build jobs and their + existing contract steps passed; both Linux userland supplements, Windows x64 build-20348 floor, + exact-head PR Checks + [29380684876](https://github.com/stablyai/orca/actions/runs/29380684876), and Golden E2E + [29380684872](https://github.com/stablyai/orca/actions/runs/29380684872) also passed. The artifact + workflow concluded failure only because Windows arm64 correctly rejected hosted build 26200 + against required build 26100. +- Missing oracle: inspection of the exact command/log showed the POSIX job ran 21 files/97 tests and + the Windows job ran 22 files/98 passed plus 3 skipped; neither explicit list named + `ssh-relay-runtime-native-signing-plan.test.mjs`, and neither syntax-checked its source. Healthy + builds cannot substitute for a test that did not run, so this exact head does not qualify the new + package. +- Native build job controls: Linux x64/arm64, macOS x64/arm64, and Windows x64/arm64 jobs all passed + at 3m43s/9m57s, 7m30s/2m41s, and 5m27s/8m7s respectively. PR Checks passed in 14m57s; Golden Linux + and macOS passed in 4m37s/5m25s. These are regression controls only, not the omitted contract proof. +- Does not prove: the new suite on Node 24/native runners, signing credentials, returned signed bytes, + native trust, release aggregation, SSH behavior, or an enabled tuple. +- Correction: explicitly syntax-check the source/test and name the suite in both POSIX and Windows + native workflow commands; lock all four test references and both source syntax checks in the + workflow contract before repeating the exact head. + +### E-M3-NATIVE-SIGNING-PLAN-WORKFLOW-LOCAL-001 — Both native job families now execute the suite + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: correction commit `9c0357235867cb2ff1ab8cbeed4901e6f013cf1d`, based on + `8ac54159c6f9d95858358f11746117c5480440cf`. +- Runner: local macOS arm64 shell, Node v26.0.0 and pnpm 10.24.0; no remote, network, credential, + artifact, or signing service was used. +- Commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-plan.test.mjs + pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-*.test.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-plan.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-plan.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-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 workflow plus signing-plan suites passed 2 files/11 tests in 1.53 seconds; the + aggregate SSH relay artifact suite passed 23 files/107 tests in 4.64 seconds. Both files passed + direct syntax checks. Typecheck, full lint, reliability gates, max-lines (355 grandfathered + suppressions and no new bypass), bundled-skill verification, localization catalog/coverage, + focused formatting, and `git diff --check` passed with only existing unrelated lint warnings and + the expected local Node-24 engine warning. +- Workflow oracle: POSIX and Windows each syntax-check the plan source and test, and each exact Vitest + command names the purpose-built suite. The workflow test requires four test-path occurrences and + two source syntax-check occurrences, preventing one native family from silently dropping it. +- Does not prove: GitHub workflow parsing/execution, Node 24, any native runner, native signing/trust, + returned signed bytes, release aggregation, SSH behavior, or an enabled tuple. +- Follow-up: push the correction plus this evidence separately and require the replacement exact-head + POSIX and Windows logs to contain the 6-test suite before accepting CI proof. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 30496bacf11a7e727ca6bf97c23ececd89c351ad Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 21:38:39 -0400 Subject: [PATCH 085/267] docs(ssh): record native signing plan CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 6 ++- ...github-release-implementation-checklist.md | 54 ++++++++++++++++++- 2 files changed, 56 insertions(+), 4 deletions(-) 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 33cb807e904..a8ed842ce85 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 @@ -28,8 +28,10 @@ complete a box. 6.0.25 execution, bundled Node, PTY, and watcher smoke. - Windows x64 passes its declared oldest-floor job. The hosted arm64 runner is build 26200, not the required build 26100, so its otherwise successful artifact/runtime smoke does not close that cell. - - Active: native-signing plan commit `9bdae7f5b` and CI-wiring correction `9c0357235` pass locally; - replacement exact-head CI is next. + - Proven: native-signing plan commit `9bdae7f5b` and CI correction `9c0357235` pass locally and on + all six native jobs in exact-head run + [29381495240](https://github.com/stablyai/orca/actions/runs/29381495240). + - Active: add credential-free exact signing-stage and returned-byte verification contracts. - 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. 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 5f9b87e7cd8..da9a3ba8926 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps 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** — implementation commit `9bdae7f5b` defines the locally green credential-free native-signing plan contract, but exact-head run [29380684866](https://github.com/stablyai/orca/actions/runs/29380684866) exposed that the native workflow's explicit command omitted its new 6-test suite (E-M3-NATIVE-SIGNING-PLAN-CI-RED-001). Correction commit `9c0357235` explicitly syntax-checks and executes it in both native job families and passes local static gates under E-M3-NATIVE-SIGNING-PLAN-WORKFLOW-LOCAL-001; a replacement exact head remains required. Every credentialed native-signing/trust cell is open and no tuple is enabled or published.
      +Session checkpoint: **In progress — 2026-07-14, Codex implementation owner** — implementation commit `9bdae7f5b` and native-CI correction `9c0357235` now pass the full local and replacement exact-head gate under E-M3-NATIVE-SIGNING-PLAN-CI-001: all six native jobs syntax-check and execute the 6-test plan suite, all six artifact builds pass, PR Checks and Golden E2E pass, and only the unchanged Windows arm64 build-26100 floor rejects hosted build 26200. Credentialed native signing, returned-byte verification, and target trust remain open; no tuple is enabled or published.
      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) @@ -671,7 +671,9 @@ tuple. The local contract and all-six actual-identity proof pass under E-M3-NATIVE-SIGNING-PLAN-LOCAL-001. E-M3-NATIVE-SIGNING-PLAN-CI-RED-001 proves the first exact-head workflow omitted the new suite despite otherwise healthy builds; explicitly wiring it into both native job families is implemented at `9c0357235` and locally green under -E-M3-NATIVE-SIGNING-PLAN-WORKFLOW-LOCAL-001; passing a replacement exact head is the active gate. +E-M3-NATIVE-SIGNING-PLAN-WORKFLOW-LOCAL-001. Replacement exact-head proof passes under +E-M3-NATIVE-SIGNING-PLAN-CI-001; the next safe package is credential-free exact signing-stage and +returned-byte verification, without credentials or publication. **Baseline correction — 2026-07-14, Codex implementation owner:** E-M3-LINUX-BASELINE-LOCAL-RED-001 proves the existing Linux x64 candidate cannot load its patched @@ -7718,6 +7720,54 @@ diff --check`. - Follow-up: push the correction plus this evidence separately and require the replacement exact-head POSIX and Windows logs to contain the 6-test suite before accepting CI proof. +### E-M3-NATIVE-SIGNING-PLAN-CI-001 — Replacement exact head executes the suite on all six native jobs + +- Date: 2026-07-14 (run timestamps 2026-07-15 UTC) +- Owner: Codex implementation owner +- Source/run: exact draft-PR head `0f589ea299e9363a3988a9bc92b721f23c1ce1f4`, containing plan + implementation `9bdae7f5bd7d5df838ab8c59af9346fc2282443c` and CI correction + `9c0357235867cb2ff1ab8cbeed4901e6f013cf1d`; Actions run + [29381495240](https://github.com/stablyai/orca/actions/runs/29381495240). +- Commands: + + ```sh + gh run view 29381495240 --repo stablyai/orca \ + --json headSha,status,conclusion,createdAt,updatedAt,url,jobs + gh run view 29381495240 --repo stablyai/orca --job 87245905347 --log | \ + rg 'native-signing-plan|Test Files|Tests|Duration' + gh run view 29381495240 --repo stablyai/orca --job 87245905412 --log | \ + rg 'native-signing-plan|Test Files|Tests|Duration' + gh run view 29381494930 --repo stablyai/orca \ + --json headSha,status,conclusion,createdAt,updatedAt,url,jobs + gh run view 29381494940 --repo stablyai/orca \ + --json headSha,status,conclusion,createdAt,updatedAt,url,jobs + ``` + +- Contract result: PASS on all six native jobs. Job-step metadata reports the purpose-named contract + step successful for Linux x64/arm64, macOS x64/arm64, and Windows x64/arm64. The macOS arm64 log + directly records both syntax checks, the named suite passing 6/6 in 29 ms, and the aggregate + 22 files/103 tests in 6.11 seconds. The Windows x64 log records both syntax checks, the suite + passing 6/6 in 37 ms, and 23 files/104 passed plus 3 platform skips out of 107 in 5.63 seconds. +- Build/regression controls: all six native artifact jobs passed. Linux x64/arm64 took 3m50s/8m53s; + macOS x64/arm64 took 7m26s/3m36s; Windows x64/arm64 took 4m49s/8m33s. Both Linux oldest-userland + supplements passed in 43s/54s, and Windows x64 passed exact build 20348 in 1m25s. +- Expected aggregate status: the artifact workflow concludes failure only because the Windows arm64 + floor job again rejects hosted build 26200 against required build 26100 in 3m23s. This preserves + E-M3-WINDOWS-ARM64-BASELINE-CI-RED-001 and is not a signing-plan regression. +- Other exact-head controls: PR Checks + [29381494930](https://github.com/stablyai/orca/actions/runs/29381494930) passed in 14m16s. Golden E2E + [29381494940](https://github.com/stablyai/orca/actions/runs/29381494940) passed Linux/macOS in + 4m35s/5m56s. +- Oracle proved: the credential-free plan contract parses and executes under exact Node 24.18.0 in + both shell families on every native runner, while all existing artifact, smoke, equality, + userland, packaging, and E2E controls retain their prior outcomes. +- Does not prove: Apple/SignPath credentials, signing requests, approval behavior, returned signed + bytes, final post-signing hashes, Gatekeeper/quarantine/notarized-app provenance, + Authenticode/Defender/WDAC policy, exact missing oldest snapshots, release aggregation, SSH + transfer/install, packaged desktop use, fallback/performance, or an enabled tuple. +- Follow-up: keep every tuple disabled; implement a credential-free exact signing-stage/returned-byte + contract that consumes the proven plan before any protected Apple or SignPath job is connected. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From c847c4a11d60ee37d161564ca3685955ce3c2d6d Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 21:51:45 -0400 Subject: [PATCH 086/267] build(ssh): validate native signing payloads Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 16 + ...h-relay-runtime-native-signing-payload.mjs | 240 ++++++++++++ ...ay-runtime-native-signing-payload.test.mjs | 360 ++++++++++++++++++ ...relay-runtime-native-signing-selection.mjs | 160 ++++++++ ...-runtime-native-signing-selection.test.mjs | 167 ++++++++ .../ssh-relay-runtime-workflow.test.mjs | 8 + 6 files changed, 951 insertions(+) create mode 100644 config/scripts/ssh-relay-runtime-native-signing-payload.mjs create mode 100644 config/scripts/ssh-relay-runtime-native-signing-payload.test.mjs create mode 100644 config/scripts/ssh-relay-runtime-native-signing-selection.mjs create mode 100644 config/scripts/ssh-relay-runtime-native-signing-selection.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 3ae99261ad2..ccb0c6cd780 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -127,6 +127,10 @@ jobs: node --check config/scripts/ssh-relay-runtime-toolchain.test.mjs node --check config/scripts/ssh-relay-runtime-native-signing-plan.mjs node --check config/scripts/ssh-relay-runtime-native-signing-plan.test.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-selection.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-selection.test.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-payload.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-payload.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 \ @@ -140,6 +144,8 @@ jobs: config/scripts/ssh-relay-runtime-build.test.mjs \ config/scripts/ssh-relay-runtime-linux-build-evidence.test.mjs \ config/scripts/ssh-relay-runtime-native-signing-plan.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-selection.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-payload.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 \ @@ -386,6 +392,14 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime native signing plan script syntax check failed' } node --check config/scripts/ssh-relay-runtime-native-signing-plan.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime native signing plan test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-native-signing-selection.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime native signing selection script syntax check failed' } + node --check config/scripts/ssh-relay-runtime-native-signing-selection.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime native signing selection test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-native-signing-payload.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime native signing payload script syntax check failed' } + node --check config/scripts/ssh-relay-runtime-native-signing-payload.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime native signing payload 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 ` @@ -399,6 +413,8 @@ jobs: config/scripts/ssh-relay-runtime-build.test.mjs ` config/scripts/ssh-relay-runtime-linux-build-evidence.test.mjs ` config/scripts/ssh-relay-runtime-native-signing-plan.test.mjs ` + config/scripts/ssh-relay-runtime-native-signing-selection.test.mjs ` + config/scripts/ssh-relay-runtime-native-signing-payload.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-native-signing-payload.mjs b/config/scripts/ssh-relay-runtime-native-signing-payload.mjs new file mode 100644 index 00000000000..25b49e9a3be --- /dev/null +++ b/config/scripts/ssh-relay-runtime-native-signing-payload.mjs @@ -0,0 +1,240 @@ +import { createHash } from 'node:crypto' +import { constants, createReadStream } from 'node:fs' +import { copyFile, lstat, mkdir, readdir, realpath, rm } from 'node:fs/promises' +import { basename, dirname, isAbsolute, relative, resolve } from 'node:path' + +const MAX_SIGNED_FILE_GROWTH_BYTES = 4 * 1024 * 1024 +const MAX_RETURNED_PAYLOAD_BYTES = 64 * 1024 * 1024 + +function localPath(root, portablePath) { + if ( + typeof portablePath !== 'string' || + portablePath.length === 0 || + portablePath.includes('\\') || + portablePath.startsWith('/') || + portablePath.split('/').some((segment) => !segment || segment === '.' || segment === '..') + ) { + throw new Error(`Runtime native signing rejects non-portable path: ${portablePath}`) + } + return resolve(root, ...portablePath.split('/')) +} + +async function sha256File(path) { + const hash = createHash('sha256') + for await (const chunk of createReadStream(path)) { + hash.update(chunk) + } + return `sha256:${hash.digest('hex')}` +} + +async function assertRegularFile(path, label) { + const metadata = await lstat(path) + if (metadata.isSymbolicLink()) { + throw new Error(`Runtime native signing ${label} is a symbolic link: ${path}`) + } + if (!metadata.isFile()) { + throw new Error(`Runtime native signing ${label} is not a regular file: ${path}`) + } + return metadata +} + +async function verifySourceFile(runtimeRoot, entry) { + const path = localPath(runtimeRoot, entry.path) + const metadata = await assertRegularFile(path, 'source') + if (metadata.size !== entry.sourceSize) { + throw new Error(`Runtime native signing source has wrong authenticated size: ${entry.path}`) + } + if ((await sha256File(path)) !== entry.sourceSha256) { + throw new Error(`Runtime native signing source has wrong authenticated hash: ${entry.path}`) + } +} + +function assertExclusiveStageLocation(runtimeRoot, stagingRoot) { + const relativeStage = relative(runtimeRoot, stagingRoot) + if (relativeStage === '' || (!relativeStage.startsWith('..') && !isAbsolute(relativeStage))) { + throw new Error('Runtime native signing staging root must be outside the runtime') + } +} + +async function resolveSigningRoots(runtimeRoot, stagingRoot) { + const runtimeMetadata = await lstat(runtimeRoot) + if (runtimeMetadata.isSymbolicLink() || !runtimeMetadata.isDirectory()) { + throw new Error('Runtime native signing source root must be a real directory') + } + const physicalRuntimeRoot = await realpath(runtimeRoot) + const physicalStagingParent = await realpath(dirname(stagingRoot)) + const physicalStagingRoot = resolve(physicalStagingParent, basename(stagingRoot)) + assertExclusiveStageLocation(physicalRuntimeRoot, physicalStagingRoot) + return { physicalRuntimeRoot, physicalStagingRoot } +} + +async function assertPathAbsent(path) { + try { + await lstat(path) + } catch (error) { + if (error.code === 'ENOENT') { + return + } + throw error + } + throw new Error('Runtime native signing requires an exclusive staging root') +} + +async function copySigningFile(runtimeRoot, stagingRoot, entry) { + const source = localPath(runtimeRoot, entry.path) + const destination = localPath(stagingRoot, entry.path) + await mkdir(dirname(destination), { recursive: true }) + await copyFile(source, destination, constants.COPYFILE_EXCL) + const metadata = await assertRegularFile(destination, 'staged file') + if ( + metadata.size !== entry.sourceSize || + (await sha256File(destination)) !== entry.sourceSha256 + ) { + throw new Error(`Runtime native signing staged bytes changed during copy: ${entry.path}`) + } + return { path: entry.path, sourceSha256: entry.sourceSha256, sourceSize: entry.sourceSize } +} + +export async function stageSshRelayRuntimeNativeSigningPayload({ + runtimeRoot, + stagingRoot, + selection +}) { + const absoluteRuntimeRoot = resolve(runtimeRoot) + const absoluteStagingRoot = resolve(stagingRoot) + const { physicalRuntimeRoot, physicalStagingRoot } = await resolveSigningRoots( + absoluteRuntimeRoot, + absoluteStagingRoot + ) + await assertPathAbsent(physicalStagingRoot) + await Promise.all( + selection.verificationFiles.map((entry) => verifySourceFile(physicalRuntimeRoot, entry)) + ) + if (selection.signingFiles.length === 0) { + return { tupleId: selection.tupleId, stagingRequired: false, stagedFiles: [], stagedSize: 0 } + } + + await mkdir(physicalStagingRoot) + try { + const stagedFiles = [] + for (const entry of selection.signingFiles) { + stagedFiles.push(await copySigningFile(physicalRuntimeRoot, physicalStagingRoot, entry)) + } + return { + tupleId: selection.tupleId, + stagingRequired: true, + stagedFiles, + stagedSize: stagedFiles.reduce((total, entry) => total + entry.sourceSize, 0) + } + } catch (error) { + // Why: a partial signing payload must never be mistaken for a complete retryable input. + await rm(physicalStagingRoot, { recursive: true, force: true }) + throw error + } +} + +function expectedDirectories(files) { + const directories = new Set() + for (const file of files) { + const segments = file.path.split('/') + for (let depth = 1; depth < segments.length; depth += 1) { + directories.add(segments.slice(0, depth).join('/')) + } + } + return directories +} + +async function walkReturnedTree( + root, + segments = [], + result = { directories: new Set(), files: new Map() } +) { + for (const directoryEntry of await readdir(root, { withFileTypes: true })) { + const childSegments = [...segments, directoryEntry.name] + const portablePath = childSegments.join('/') + const path = resolve(root, directoryEntry.name) + const metadata = await lstat(path) + if (metadata.isSymbolicLink()) { + throw new Error( + `Runtime native signing returned tree contains a symbolic link: ${portablePath}` + ) + } + if (metadata.isDirectory()) { + result.directories.add(portablePath) + await walkReturnedTree(path, childSegments, result) + } else if (metadata.isFile()) { + result.files.set(portablePath, { path, metadata }) + } else { + throw new Error( + `Runtime native signing returned tree contains a special entry: ${portablePath}` + ) + } + } + return result +} + +function assertExactReturnedClosure(actual, expectedFiles) { + const expectedFilePaths = new Set(expectedFiles.map((entry) => entry.path)) + for (const entry of expectedFiles) { + if (!actual.files.has(entry.path)) { + throw new Error(`Runtime native signing is missing returned file: ${entry.path}`) + } + } + for (const path of actual.files.keys()) { + if (!expectedFilePaths.has(path)) { + throw new Error(`Runtime native signing has unexpected returned file: ${path}`) + } + } + const directories = expectedDirectories(expectedFiles) + for (const path of actual.directories) { + if (!directories.has(path)) { + throw new Error(`Runtime native signing has unexpected returned directory: ${path}`) + } + } + for (const path of directories) { + if (!actual.directories.has(path)) { + throw new Error(`Runtime native signing is missing returned directory: ${path}`) + } + } +} + +export async function verifySshRelayRuntimeNativeSigningReturn({ returnedRoot, selection }) { + if (selection.signingFiles.length === 0) { + throw new Error('Runtime native signing selection has no returned payload') + } + const absoluteReturnedRoot = resolve(returnedRoot) + const rootMetadata = await lstat(absoluteReturnedRoot) + if (rootMetadata.isSymbolicLink() || !rootMetadata.isDirectory()) { + throw new Error('Runtime native signing returned root must be a real directory') + } + const actual = await walkReturnedTree(absoluteReturnedRoot) + assertExactReturnedClosure(actual, selection.signingFiles) + + let returnedSize = 0 + const returnedFiles = [] + for (const entry of selection.signingFiles) { + const returned = actual.files.get(entry.path) + if ( + returned.metadata.size <= 0 || + returned.metadata.size > entry.sourceSize + MAX_SIGNED_FILE_GROWTH_BYTES + ) { + throw new Error(`Runtime native signing returned file exceeds size bound: ${entry.path}`) + } + returnedSize += returned.metadata.size + if (returnedSize > MAX_RETURNED_PAYLOAD_BYTES) { + throw new Error('Runtime native signing returned payload exceeds total size bound') + } + const signedSha256 = await sha256File(returned.path) + if (signedSha256 === entry.sourceSha256) { + throw new Error(`Runtime native signing returned file did not change: ${entry.path}`) + } + returnedFiles.push({ + path: entry.path, + role: entry.role, + sourceSha256: entry.sourceSha256, + signedSha256, + signedSize: returned.metadata.size + }) + } + return { tupleId: selection.tupleId, returnedFiles, returnedSize } +} diff --git a/config/scripts/ssh-relay-runtime-native-signing-payload.test.mjs b/config/scripts/ssh-relay-runtime-native-signing-payload.test.mjs new file mode 100644 index 00000000000..2b36836edaa --- /dev/null +++ b/config/scripts/ssh-relay-runtime-native-signing-payload.test.mjs @@ -0,0 +1,360 @@ +import { createHash } from 'node:crypto' +import { + appendFile, + cp, + lstat, + mkdir, + mkdtemp, + readFile, + rm, + symlink, + truncate, + writeFile +} from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + +import { describe, expect, it } from 'vitest' + +import { expectedSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { + stageSshRelayRuntimeNativeSigningPayload, + verifySshRelayRuntimeNativeSigningReturn +} from './ssh-relay-runtime-native-signing-payload.mjs' +import { buildSshRelayRuntimeNativeSigningSelection } from './ssh-relay-runtime-native-signing-selection.mjs' + +function digest(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +function platformForTuple(tupleId) { + return tupleId.startsWith('linux-') ? 'linux' : tupleId.startsWith('darwin-') ? 'darwin' : 'win32' +} + +async function runtimeFixture(tupleId) { + const root = await mkdtemp(join(tmpdir(), 'ssh-relay-native-signing-payload-')) + const runtimeRoot = join(root, 'runtime') + await mkdir(runtimeRoot) + const entries = [] + for (const entry of expectedSshRelayRuntimeClosureEntries(tupleId)) { + if (entry.type === 'directory') { + await mkdir(join(runtimeRoot, ...entry.path.split('/')), { recursive: true }) + entries.push(entry) + continue + } + const bytes = Buffer.from(`fixture:${tupleId}:${entry.path}`) + const filePath = join(runtimeRoot, ...entry.path.split('/')) + await mkdir(dirname(filePath), { recursive: true }) + await writeFile(filePath, bytes, { mode: entry.mode }) + entries.push({ ...entry, size: bytes.length, sha256: digest(bytes) }) + } + return { + root, + runtimeRoot, + identity: { + tupleId, + os: platformForTuple(tupleId), + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries + } + } +} + +function windowsAssessments(identity, preservedPath) { + const nativeEntries = identity.entries.filter( + (entry) => + entry.type === 'file' && + ['node-pty-native', 'parcel-watcher-native', 'native-runtime'].includes(entry.role) + ) + return nativeEntries.map((entry) => + entry.path === preservedPath + ? { + path: entry.path, + sourceSha256: entry.sha256, + status: 'valid-upstream', + signerSubject: 'CN=Microsoft Corporation', + signerThumbprint: 'B'.repeat(40) + } + : { path: entry.path, sourceSha256: entry.sha256, status: 'unsigned' } + ) +} + +async function createReturnedTree(stagingRoot, returnedRoot, selection) { + await cp(stagingRoot, returnedRoot, { recursive: true, errorOnExist: true, force: false }) + for (const entry of selection.signingFiles) { + await appendFile(join(returnedRoot, ...entry.path.split('/')), ':signed') + } +} + +describe('SSH relay runtime native signing payload', () => { + it('authenticates Linux native bytes without creating a signing payload', async () => { + const fixture = await runtimeFixture('linux-x64-glibc') + try { + const selection = buildSshRelayRuntimeNativeSigningSelection(fixture.identity, []) + const stagingRoot = join(fixture.root, 'stage') + await expect( + stageSshRelayRuntimeNativeSigningPayload({ + runtimeRoot: fixture.runtimeRoot, + stagingRoot, + selection + }) + ).resolves.toEqual({ + tupleId: 'linux-x64-glibc', + stagingRequired: false, + stagedFiles: [], + stagedSize: 0 + }) + await expect(lstat(stagingRoot)).rejects.toMatchObject({ code: 'ENOENT' }) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('verifies all native bytes before exclusively staging only macOS signing files', async () => { + const fixture = await runtimeFixture('darwin-arm64') + try { + const selection = buildSshRelayRuntimeNativeSigningSelection(fixture.identity, []) + const stagingRoot = join(fixture.root, 'stage') + const report = await stageSshRelayRuntimeNativeSigningPayload({ + runtimeRoot: fixture.runtimeRoot, + stagingRoot, + selection + }) + + expect(report.stagedFiles.map((entry) => entry.path)).toEqual( + selection.signingFiles.map((entry) => entry.path) + ) + await expect(lstat(join(stagingRoot, 'bin', 'node'))).rejects.toMatchObject({ + code: 'ENOENT' + }) + for (const entry of selection.signingFiles) { + await expect(readFile(join(stagingRoot, ...entry.path.split('/')))).resolves.toEqual( + await readFile(join(fixture.runtimeRoot, ...entry.path.split('/'))) + ) + } + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('does not stage a Windows file whose valid upstream signature is preserved', async () => { + const fixture = await runtimeFixture('win32-x64') + try { + const preservedPath = 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe' + const selection = buildSshRelayRuntimeNativeSigningSelection( + fixture.identity, + windowsAssessments(fixture.identity, preservedPath) + ) + const stagingRoot = join(fixture.root, 'stage') + const report = await stageSshRelayRuntimeNativeSigningPayload({ + runtimeRoot: fixture.runtimeRoot, + stagingRoot, + selection + }) + + expect(report.stagedFiles).toHaveLength(4) + await expect(lstat(join(stagingRoot, ...preservedPath.split('/')))).rejects.toMatchObject({ + code: 'ENOENT' + }) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('rejects source mutation before creating a stage', async () => { + const fixture = await runtimeFixture('darwin-x64') + try { + const selection = buildSshRelayRuntimeNativeSigningSelection(fixture.identity, []) + const stagingRoot = join(fixture.root, 'stage') + await appendFile(join(fixture.runtimeRoot, 'bin', 'node'), ':mutated') + await expect( + stageSshRelayRuntimeNativeSigningPayload({ + runtimeRoot: fixture.runtimeRoot, + stagingRoot, + selection + }) + ).rejects.toThrow(/authenticated size/i) + await expect(lstat(stagingRoot)).rejects.toMatchObject({ code: 'ENOENT' }) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('rejects an existing or source-nested staging root', async () => { + const fixture = await runtimeFixture('darwin-arm64') + try { + const selection = buildSshRelayRuntimeNativeSigningSelection(fixture.identity, []) + const existing = join(fixture.root, 'existing') + await mkdir(existing) + await expect( + stageSshRelayRuntimeNativeSigningPayload({ + runtimeRoot: fixture.runtimeRoot, + stagingRoot: existing, + selection + }) + ).rejects.toThrow(/exclusive/i) + await expect( + stageSshRelayRuntimeNativeSigningPayload({ + runtimeRoot: fixture.runtimeRoot, + stagingRoot: join(fixture.runtimeRoot, 'nested-stage'), + selection + }) + ).rejects.toThrow(/outside the runtime/i) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('removes a partial stage when authenticated signing-file copy fails', async () => { + const fixture = await runtimeFixture('darwin-arm64') + try { + const selection = buildSshRelayRuntimeNativeSigningSelection(fixture.identity, []) + selection.signingFiles[1].sourceSha256 = `sha256:${'f'.repeat(64)}` + const stagingRoot = join(fixture.root, 'stage') + await expect( + stageSshRelayRuntimeNativeSigningPayload({ + runtimeRoot: fixture.runtimeRoot, + stagingRoot, + selection + }) + ).rejects.toThrow(/changed during copy/i) + await expect(lstat(stagingRoot)).rejects.toMatchObject({ code: 'ENOENT' }) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('accepts an exact returned tree only after every staged hash changes', async () => { + const fixture = await runtimeFixture('darwin-arm64') + try { + const selection = buildSshRelayRuntimeNativeSigningSelection(fixture.identity, []) + const stagingRoot = join(fixture.root, 'stage') + const returnedRoot = join(fixture.root, 'returned') + await stageSshRelayRuntimeNativeSigningPayload({ + runtimeRoot: fixture.runtimeRoot, + stagingRoot, + selection + }) + await createReturnedTree(stagingRoot, returnedRoot, selection) + + const report = await verifySshRelayRuntimeNativeSigningReturn({ returnedRoot, selection }) + expect(report.returnedFiles).toHaveLength(3) + expect(report.returnedFiles.every((entry) => entry.signedSha256 !== entry.sourceSha256)).toBe( + true + ) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('rejects unchanged, missing, extra, and oversized returned bytes', async () => { + const fixture = await runtimeFixture('darwin-x64') + try { + const selection = buildSshRelayRuntimeNativeSigningSelection(fixture.identity, []) + const stagingRoot = join(fixture.root, 'stage') + await stageSshRelayRuntimeNativeSigningPayload({ + runtimeRoot: fixture.runtimeRoot, + stagingRoot, + selection + }) + + const unchangedRoot = join(fixture.root, 'unchanged') + await cp(stagingRoot, unchangedRoot, { recursive: true }) + await expect( + verifySshRelayRuntimeNativeSigningReturn({ returnedRoot: unchangedRoot, selection }) + ).rejects.toThrow(/did not change/i) + + const missingRoot = join(fixture.root, 'missing') + await createReturnedTree(stagingRoot, missingRoot, selection) + await rm(join(missingRoot, ...selection.signingFiles[0].path.split('/'))) + await expect( + verifySshRelayRuntimeNativeSigningReturn({ returnedRoot: missingRoot, selection }) + ).rejects.toThrow(/missing returned file/i) + + const extraRoot = join(fixture.root, 'extra') + await createReturnedTree(stagingRoot, extraRoot, selection) + await writeFile(join(extraRoot, 'extra.node'), 'extra') + await expect( + verifySshRelayRuntimeNativeSigningReturn({ returnedRoot: extraRoot, selection }) + ).rejects.toThrow(/unexpected returned file/i) + + const oversizedRoot = join(fixture.root, 'oversized') + await createReturnedTree(stagingRoot, oversizedRoot, selection) + const first = selection.signingFiles[0] + await truncate( + join(oversizedRoot, ...first.path.split('/')), + first.sourceSize + 4 * 1024 * 1024 + 1 + ) + await expect( + verifySshRelayRuntimeNativeSigningReturn({ returnedRoot: oversizedRoot, selection }) + ).rejects.toThrow(/size bound/i) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it.skipIf(process.platform === 'win32')( + 'rejects a symlink in returned signing bytes', + async () => { + const fixture = await runtimeFixture('darwin-arm64') + try { + const selection = buildSshRelayRuntimeNativeSigningSelection(fixture.identity, []) + const returnedRoot = join(fixture.root, 'returned') + const target = join(fixture.root, 'target') + const first = selection.signingFiles[0] + await writeFile(target, 'signed') + await mkdir(dirname(join(returnedRoot, ...first.path.split('/'))), { recursive: true }) + await symlink(target, join(returnedRoot, ...first.path.split('/'))) + await expect( + verifySshRelayRuntimeNativeSigningReturn({ returnedRoot, selection }) + ).rejects.toThrow(/symbolic link/i) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + } + ) + + it.skipIf(process.platform === 'win32')('rejects a symlinked native source file', async () => { + const fixture = await runtimeFixture('darwin-x64') + try { + const selection = buildSshRelayRuntimeNativeSigningSelection(fixture.identity, []) + const first = selection.signingFiles[0] + const sourcePath = join(fixture.runtimeRoot, ...first.path.split('/')) + const target = join(fixture.root, 'native-target') + await cp(sourcePath, target) + await rm(sourcePath) + await symlink(target, sourcePath) + await expect( + stageSshRelayRuntimeNativeSigningPayload({ + runtimeRoot: fixture.runtimeRoot, + stagingRoot: join(fixture.root, 'stage'), + selection + }) + ).rejects.toThrow(/source is a symbolic link/i) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it.skipIf(process.platform === 'win32')( + 'rejects a symlinked staging parent that resolves inside the runtime', + async () => { + const fixture = await runtimeFixture('darwin-arm64') + try { + const selection = buildSshRelayRuntimeNativeSigningSelection(fixture.identity, []) + const redirectedParent = join(fixture.root, 'redirected-parent') + await symlink(fixture.runtimeRoot, redirectedParent) + await expect( + stageSshRelayRuntimeNativeSigningPayload({ + runtimeRoot: fixture.runtimeRoot, + stagingRoot: join(redirectedParent, 'stage'), + selection + }) + ).rejects.toThrow(/outside the runtime/i) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + } + ) +}) diff --git a/config/scripts/ssh-relay-runtime-native-signing-selection.mjs b/config/scripts/ssh-relay-runtime-native-signing-selection.mjs new file mode 100644 index 00000000000..fdc5debc3d3 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-native-signing-selection.mjs @@ -0,0 +1,160 @@ +import { buildSshRelayRuntimeNativeSigningPlan } from './ssh-relay-runtime-native-signing-plan.mjs' + +const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/ +const WINDOWS_UNSIGNED_FIELDS = ['path', 'sourceSha256', 'status'] +const WINDOWS_VALID_FIELDS = ['path', 'signerSubject', 'signerThumbprint', 'sourceSha256', 'status'] + +function assertExactFields(value, expected, label) { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`Runtime native signing ${label} must be an object`) + } + const actual = Object.keys(value).sort() + if ( + actual.length !== expected.length || + actual.some((field, index) => field !== expected[index]) + ) { + throw new Error(`Runtime native signing ${label} has unexpected fields`) + } +} + +function authenticatedFile(identityEntries, planEntry) { + const entry = identityEntries.get(planEntry.path) + if (!entry || entry.type !== 'file') { + throw new Error(`Runtime native signing identity is missing file: ${planEntry.path}`) + } + if (!Number.isSafeInteger(entry.size) || entry.size <= 0) { + throw new Error(`Runtime native signing identity has invalid size: ${planEntry.path}`) + } + if (!SHA256_PATTERN.test(entry.sha256)) { + throw new Error(`Runtime native signing identity has invalid digest: ${planEntry.path}`) + } + if (entry.sha256 !== planEntry.sourceSha256 || entry.role !== planEntry.role) { + throw new Error(`Runtime native signing identity disagrees with plan: ${planEntry.path}`) + } + return { + path: entry.path, + role: entry.role, + sourceSha256: entry.sha256, + sourceSize: entry.size + } +} + +function indexIdentityFiles(identity) { + const entries = new Map() + for (const entry of identity.entries) { + if (entry.type === 'file') { + entries.set(entry.path, entry) + } + } + return entries +} + +function containsAsciiControl(value) { + return [...value].some((character) => { + const codePoint = character.codePointAt(0) + return codePoint <= 0x1f || codePoint === 0x7f + }) +} + +function assertSignerIdentity(assessment) { + if ( + typeof assessment.signerSubject !== 'string' || + assessment.signerSubject.length === 0 || + assessment.signerSubject.length > 1024 || + containsAsciiControl(assessment.signerSubject) + ) { + throw new Error( + `Runtime native signing assessment has invalid signer subject: ${assessment.path}` + ) + } + if (!/^[0-9a-f]{40}$/i.test(assessment.signerThumbprint)) { + throw new Error( + `Runtime native signing assessment has invalid signer thumbprint: ${assessment.path}` + ) + } +} + +function windowsSelection(plan, identityEntries, assessments) { + if (!Array.isArray(assessments)) { + throw new Error('Runtime native signing Windows assessments must be an array') + } + const candidates = new Map(plan.signingCandidates.map((entry) => [entry.path, entry])) + const indexed = new Map() + for (const assessment of assessments) { + if (!assessment || typeof assessment.path !== 'string' || !candidates.has(assessment.path)) { + throw new Error(`Runtime native signing has unexpected assessment: ${assessment?.path}`) + } + if (indexed.has(assessment.path)) { + throw new Error(`Runtime native signing has duplicate assessment: ${assessment.path}`) + } + if (!['unsigned', 'valid-upstream'].includes(assessment.status)) { + throw new Error(`Runtime native signing rejects signature status: ${assessment.status}`) + } + assertExactFields( + assessment, + assessment.status === 'unsigned' ? WINDOWS_UNSIGNED_FIELDS : WINDOWS_VALID_FIELDS, + 'assessment' + ) + const candidate = candidates.get(assessment.path) + if (assessment.sourceSha256 !== candidate.sourceSha256) { + throw new Error(`Runtime native signing assessment has wrong source hash: ${assessment.path}`) + } + if (assessment.status === 'valid-upstream') { + assertSignerIdentity(assessment) + } + indexed.set(assessment.path, assessment) + } + + return plan.signingCandidates.map((candidate) => { + const assessment = indexed.get(candidate.path) + if (!assessment) { + throw new Error(`Runtime native signing is missing assessment: ${candidate.path}`) + } + const file = authenticatedFile(identityEntries, candidate) + return assessment.status === 'unsigned' + ? { ...file, action: 'signpath-required' } + : { + ...file, + action: 'preserve-valid-upstream', + signerSubject: assessment.signerSubject, + signerThumbprint: assessment.signerThumbprint.toUpperCase() + } + }) +} + +export function buildSshRelayRuntimeNativeSigningSelection(identity, assessments) { + const plan = buildSshRelayRuntimeNativeSigningPlan(identity) + const identityEntries = indexIdentityFiles(identity) + const immutableVendorFiles = plan.immutableVendorFiles.map((entry) => ({ + ...authenticatedFile(identityEntries, entry), + action: entry.action + })) + const verificationFiles = plan.verificationFiles.map((entry) => + authenticatedFile(identityEntries, entry) + ) + + let candidateFiles + if (plan.platform === 'win32') { + candidateFiles = windowsSelection(plan, identityEntries, assessments) + } else { + if (!Array.isArray(assessments) || assessments.length !== 0) { + throw new Error(`Runtime native signing ${plan.platform} does not accept assessments`) + } + candidateFiles = plan.signingCandidates.map((entry) => ({ + ...authenticatedFile(identityEntries, entry), + action: entry.action + })) + } + + return { + tupleId: plan.tupleId, + platform: plan.platform, + policy: plan.policy, + immutableVendorFiles, + signingFiles: candidateFiles.filter((entry) => entry.action !== 'preserve-valid-upstream'), + preservedUpstreamFiles: candidateFiles.filter( + (entry) => entry.action === 'preserve-valid-upstream' + ), + verificationFiles + } +} diff --git a/config/scripts/ssh-relay-runtime-native-signing-selection.test.mjs b/config/scripts/ssh-relay-runtime-native-signing-selection.test.mjs new file mode 100644 index 00000000000..c23c107fdd6 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-native-signing-selection.test.mjs @@ -0,0 +1,167 @@ +import { describe, expect, it } from 'vitest' + +import { expectedSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { buildSshRelayRuntimeNativeSigningPlan } from './ssh-relay-runtime-native-signing-plan.mjs' +import { buildSshRelayRuntimeNativeSigningSelection } from './ssh-relay-runtime-native-signing-selection.mjs' + +const DIGEST = `sha256:${'a'.repeat(64)}` + +function identityFor(tupleId) { + const os = tupleId.startsWith('linux-') + ? 'linux' + : tupleId.startsWith('darwin-') + ? 'darwin' + : 'win32' + return { + tupleId, + os, + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries: expectedSshRelayRuntimeClosureEntries(tupleId).map((entry) => + entry.type === 'file' ? { ...entry, size: 100, sha256: DIGEST } : entry + ) + } +} + +function windowsAssessments(identity, preservedPath) { + return buildSshRelayRuntimeNativeSigningPlan(identity).signingCandidates.map((entry) => + entry.path === preservedPath + ? { + path: entry.path, + sourceSha256: entry.sourceSha256, + status: 'valid-upstream', + signerSubject: 'CN=Microsoft Corporation', + signerThumbprint: 'A'.repeat(40) + } + : { path: entry.path, sourceSha256: entry.sourceSha256, status: 'unsigned' } + ) +} + +describe('SSH relay runtime native signing selection', () => { + it('keeps Linux hash-only and official Node verification exact', () => { + const selection = buildSshRelayRuntimeNativeSigningSelection(identityFor('linux-x64-glibc'), []) + + expect(selection.signingFiles).toEqual([]) + expect(selection.immutableVendorFiles).toEqual([ + expect.objectContaining({ path: 'bin/node', action: 'preserve-exact-bytes', sourceSize: 100 }) + ]) + expect(selection.verificationFiles).toHaveLength(3) + }) + + it('requires every macOS candidate and ignores no assessment input', () => { + const identity = identityFor('darwin-arm64') + const selection = buildSshRelayRuntimeNativeSigningSelection(identity, []) + + expect(selection.signingFiles.map((entry) => [entry.path, entry.action])).toEqual([ + ['node_modules/@parcel/watcher-darwin-arm64/watcher.node', 'developer-id-required'], + ['node_modules/node-pty/build/Release/pty.node', 'developer-id-required'], + ['node_modules/node-pty/build/Release/spawn-helper', 'developer-id-required'] + ]) + expect(() => + buildSshRelayRuntimeNativeSigningSelection(identity, [ + { path: selection.signingFiles[0].path, status: 'unsigned', sourceSha256: DIGEST } + ]) + ).toThrow(/does not accept assessments/i) + }) + + it('stages only unsigned Windows candidates and preserves valid upstream identity', () => { + const identity = identityFor('win32-x64') + const preservedPath = 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe' + const selection = buildSshRelayRuntimeNativeSigningSelection( + identity, + windowsAssessments(identity, preservedPath) + ) + + expect(selection.signingFiles).toHaveLength(4) + expect(selection.preservedUpstreamFiles).toEqual([ + expect.objectContaining({ + path: preservedPath, + action: 'preserve-valid-upstream', + signerSubject: 'CN=Microsoft Corporation', + signerThumbprint: 'A'.repeat(40) + }) + ]) + expect(selection.verificationFiles).toHaveLength(6) + }) + + it('rejects invalid Windows signature states and malformed signer identity', () => { + const identity = identityFor('win32-arm64') + const assessments = windowsAssessments(identity) + assessments[0].status = 'hash-mismatch' + expect(() => buildSshRelayRuntimeNativeSigningSelection(identity, assessments)).toThrow( + /signature status/i + ) + + const malformedSigner = windowsAssessments( + identity, + 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe' + ) + malformedSigner.find((entry) => entry.status === 'valid-upstream').signerThumbprint = 'bad' + expect(() => buildSshRelayRuntimeNativeSigningSelection(identity, malformedSigner)).toThrow( + /thumbprint/i + ) + + const controlSubject = windowsAssessments( + identity, + 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe' + ) + controlSubject.find((entry) => entry.status === 'valid-upstream').signerSubject = + 'CN=Microsoft\nCorporation' + expect(() => buildSshRelayRuntimeNativeSigningSelection(identity, controlSubject)).toThrow( + /signer subject/i + ) + }) + + it('rejects assessment fields outside the exact status-qualified schema', () => { + const identity = identityFor('win32-x64') + const unexpected = windowsAssessments(identity) + unexpected[0].signerSubject = 'CN=Unexpected' + expect(() => buildSshRelayRuntimeNativeSigningSelection(identity, unexpected)).toThrow( + /unexpected fields/i + ) + + const preserved = windowsAssessments( + identity, + 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe' + ) + preserved.find((entry) => entry.status === 'valid-upstream').unexpected = true + expect(() => buildSshRelayRuntimeNativeSigningSelection(identity, preserved)).toThrow( + /unexpected fields/i + ) + }) + + it('rejects missing, duplicate, extra, and hash-mismatched assessments', () => { + const identity = identityFor('win32-x64') + const assessments = windowsAssessments(identity) + expect(() => + buildSshRelayRuntimeNativeSigningSelection(identity, assessments.slice(1)) + ).toThrow(/missing assessment/i) + + expect(() => + buildSshRelayRuntimeNativeSigningSelection(identity, [assessments[0], ...assessments]) + ).toThrow(/duplicate assessment/i) + + expect(() => + buildSshRelayRuntimeNativeSigningSelection(identity, [ + ...assessments, + { path: 'extra.exe', sourceSha256: DIGEST, status: 'unsigned' } + ]) + ).toThrow(/unexpected assessment/i) + + const mismatched = structuredClone(assessments) + mismatched[0].sourceSha256 = `sha256:${'b'.repeat(64)}` + expect(() => buildSshRelayRuntimeNativeSigningSelection(identity, mismatched)).toThrow( + /source hash/i + ) + }) + + it('rejects malformed authenticated size and digest metadata', () => { + const invalidSize = identityFor('darwin-x64') + invalidSize.entries.find((entry) => entry.role === 'node').size = -1 + expect(() => buildSshRelayRuntimeNativeSigningSelection(invalidSize, [])).toThrow(/size/i) + + const invalidDigest = identityFor('darwin-x64') + invalidDigest.entries.find((entry) => entry.role === 'node').sha256 = 'sha256:nope' + expect(() => buildSshRelayRuntimeNativeSigningSelection(invalidDigest, [])).toThrow(/digest/i) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 8578c1b7368..fd24d8b48ef 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -82,9 +82,17 @@ describe('SSH relay runtime artifact workflow', () => { expect(source.match(/ssh-relay-runtime-provenance\.test\.mjs/g)).toHaveLength(4) expect(source.match(/ssh-relay-runtime-toolchain\.test\.mjs/g)).toHaveLength(4) expect(source.match(/ssh-relay-runtime-native-signing-plan\.test\.mjs/g)).toHaveLength(4) + expect(source.match(/ssh-relay-runtime-native-signing-selection\.test\.mjs/g)).toHaveLength(4) + expect(source.match(/ssh-relay-runtime-native-signing-payload\.test\.mjs/g)).toHaveLength(4) expect( source.match(/node --check config\/scripts\/ssh-relay-runtime-native-signing-plan\.mjs/g) ).toHaveLength(2) + expect( + source.match(/node --check config\/scripts\/ssh-relay-runtime-native-signing-selection\.mjs/g) + ).toHaveLength(2) + expect( + source.match(/node --check config\/scripts\/ssh-relay-runtime-native-signing-payload\.mjs/g) + ).toHaveLength(2) expect( source.match(/node --check config\/scripts\/ssh-relay-runtime-closure\.mjs/g) ).toHaveLength(2) From 9e08ac123039b827ebacbe1918ef8d3c3b989ae9 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 21:53:52 -0400 Subject: [PATCH 087/267] docs(ssh): record native signing staging proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 3 +- ...github-release-implementation-checklist.md | 112 +++++++++++++++++- 2 files changed, 113 insertions(+), 2 deletions(-) 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 a8ed842ce85..25f949a1fe3 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 @@ -31,7 +31,8 @@ complete a box. - Proven: native-signing plan commit `9bdae7f5b` and CI correction `9c0357235` pass locally and on all six native jobs in exact-head run [29381495240](https://github.com/stablyai/orca/actions/runs/29381495240). - - Active: add credential-free exact signing-stage and returned-byte verification contracts. + - Implemented locally at `c847c4a11`: credential-free exact signing selection, exclusive staging, + returned-tree verification, and explicit POSIX/Windows CI wiring; exact-head runner proof is next. - 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. 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 da9a3ba8926..cf0e6409267 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps 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** — implementation commit `9bdae7f5b` and native-CI correction `9c0357235` now pass the full local and replacement exact-head gate under E-M3-NATIVE-SIGNING-PLAN-CI-001: all six native jobs syntax-check and execute the 6-test plan suite, all six artifact builds pass, PR Checks and Golden E2E pass, and only the unchanged Windows arm64 build-26100 floor rejects hosted build 26200. Credentialed native signing, returned-byte verification, and target trust remain open; no tuple is enabled or published.
      +Session checkpoint: **In progress — 2026-07-14, Codex implementation owner** — credential-free native signing selection, exclusive payload staging, exact returned-tree verification, and explicit POSIX/Windows workflow wiring are implemented at `c847c4a11` and locally green under E-M3-NATIVE-SIGNING-STAGE-LOCAL-001. Replacement exact-head GitHub-runner proof must explicitly execute both new suites in every native job family before this slice is complete. The slice uses no credentials or signing service, establishes no native trust, publishes nothing, enables no tuple, and leaves legacy as 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) @@ -675,6 +675,20 @@ E-M3-NATIVE-SIGNING-PLAN-WORKFLOW-LOCAL-001. Replacement exact-head proof passes E-M3-NATIVE-SIGNING-PLAN-CI-001; the next safe package is credential-free exact signing-stage and returned-byte verification, without credentials or publication. +**Active signing-stage contract slice — 2026-07-14, Codex implementation owner:** consume the proven +plan to normalize one exact candidate selection before filesystem or signing side effects. macOS +must select every Developer ID target. Windows may preserve an exact source file only when a native +assessment classifies its existing Authenticode signature as valid upstream; it may stage only a +truly unsigned candidate, while invalid, unknown, malformed, missing, duplicate, or extra assessment +states fail closed. Verify every native source file against its identity size/hash before creating an +exclusive payload tree, never stage official Node, preserve portable relative paths, and clean up a +partial stage on failure. A returned tree must contain exactly the staged regular files, no links or +special entries, remain within explicit size/growth bounds, and change every staged source hash. +This slice does not invoke Apple/SignPath, mutate a runtime, accept native trust, publish, aggregate, +or enable a tuple. Implementation commit `c847c4a11` and local proof +E-M3-NATIVE-SIGNING-STAGE-LOCAL-001 close the credential-free contract and workflow-source gates; +exact-head execution on all six native runner families remains required. + **Baseline correction — 2026-07-14, Codex implementation owner:** E-M3-LINUX-BASELINE-LOCAL-RED-001 proves the existing Linux x64 candidate cannot load its patched `node-pty` on Rocky 8.9/glibc 2.28 because the Ubuntu 24.04 producer emitted references to @@ -7768,6 +7782,102 @@ diff --check`. - Follow-up: keep every tuple disabled; implement a credential-free exact signing-stage/returned-byte contract that consumes the proven plan before any protected Apple or SignPath job is connected. +### E-M3-NATIVE-SIGNING-STAGE-LOCAL-RED-001 — Missing signing-stage modules fail the focused contract + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: pre-implementation head `30496bacf11a7e727ca6bf97c23ececd89c351ad`. +- Command: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-native-signing-selection.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-payload.test.mjs + ``` + +- Result: expected RED. Both purpose-named test files failed to load because + `ssh-relay-runtime-native-signing-selection.mjs` and + `ssh-relay-runtime-native-signing-payload.mjs` did not exist. No production or artifact behavior + changed. The exact RED duration was not retained and is deliberately not reconstructed. +- Oracle proved: the new selection, staging, and returned-tree requirements were not already + satisfied by the signing-plan module or an unrelated artifact test. +- Does not prove: any implementation behavior, native runner execution, signing, native trust, + release aggregation, SSH behavior, or an enabled tuple. +- Correction: implement the two purpose-named modules, keep official Node out of signing payloads, + and require both native workflow families to execute their contracts explicitly. + +### E-M3-NATIVE-SIGNING-STAGE-LOCAL-001 — Exact selection, exclusive staging, and returned-tree contracts pass + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: implementation commit `c847c4a11d60ee37d161564ca3685955ce3c2d6d`, based on + `30496bacf11a7e727ca6bf97c23ececd89c351ad`. +- Runner/remote/network: local macOS 26.2 build 25C56 arm64, Node v26.0.0 and pnpm 10.24.0; no + remote, network, credential, signing service, native-trust environment, or publication path was + used. +- Commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-native-signing-selection.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-payload.test.mjs + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-native-signing-selection.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-payload.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-*.test.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-selection.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-selection.test.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-payload.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-payload.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-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-selection.mjs \ + config/scripts/ssh-relay-runtime-native-signing-selection.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-payload.mjs \ + config/scripts/ssh-relay-runtime-native-signing-payload.test.mjs \ + docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md + git diff --check + ``` + +- Result: PASS. The focused selection/payload command passed 2 files/18 tests in 247 ms. The two + suites plus workflow contract passed 3 files/23 tests in 264 ms. The complete purpose-named SSH + relay script suite passed 25 files/125 tests in 2.55 seconds. Four direct syntax checks, typecheck, + full lint and reliability gates, max-lines (355 grandfathered suppressions with no new bypass), + bundled-skill verification, localization catalog/coverage, focused formatting, and + `git diff --check` passed. Lint emitted only existing unrelated warnings; local Node 26 emitted the + expected repository Node-24 engine warning. An intermediate lint run rejected an ASCII-control + regular expression and formatting check rejected all four new files; the implementation was + corrected without a disable and the complete gate was rerun green. +- Selection oracle: Linux accepts no assessments and creates no signing payload. macOS selects all + three non-Node Developer ID candidates. Windows requires exactly one hash-bound assessment per PE + candidate, stages only `unsigned`, preserves only `valid-upstream` with a bounded non-control + signer subject and exact 40-hex thumbprint, and rejects invalid/unknown status, missing, duplicate, + extra, hash-mismatched, malformed, or status-incompatible fields. +- Payload oracle: every native source file is authenticated by safe-integer size and SHA-256 before + an exclusive stage is created; official Node is never copied. The physical stage must be outside + the runtime, source/staged files must be regular, portable relative paths are preserved, and a + partial stage is removed on copy/authentication failure. Linux verifies without creating a stage. +- Returned-tree oracle: the root and every entry must be real directories or regular files; links, + special entries, unexpected/missing files or directories, unchanged hashes, per-file growth over + 4 MiB, or aggregate size over 64 MiB reject. Every accepted returned file records its exact new + size and SHA-256 without mutating the runtime. +- Workflow oracle: POSIX and Windows native job families each syntax-check both sources and tests and + explicitly name both suites. The workflow contract locks four occurrences per test and two source + syntax-check occurrences so one shell family cannot silently omit the package. +- Does not prove: Node 24 behavior or parsing on GitHub Actions, native filesystem behavior on all + six runners, Apple/SignPath credentials or calls, native trust, final signed-byte identity, + Gatekeeper/quarantine/notarized-app provenance, Authenticode/Defender/WDAC policy, oldest missing + snapshots, release aggregation, SSH transfer/install, packaged desktop use, fallback/performance, + or an enabled tuple. +- Follow-up: push the implementation and evidence separately, then inspect an exact-head run to prove + both suites executed under Node 24.18.0 in all six native jobs while existing build/smoke/equality + controls retain their outcomes. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From e478ae458fa634845e9d9a7fd1d5a01cb41ed6ed Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 22:11:07 -0400 Subject: [PATCH 088/267] docs(ssh): record native signing staging CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 6 +- ...github-release-implementation-checklist.md | 63 ++++++++++++++++++- 2 files changed, 65 insertions(+), 4 deletions(-) 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 25f949a1fe3..098be9426d2 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 @@ -31,8 +31,10 @@ complete a box. - Proven: native-signing plan commit `9bdae7f5b` and CI correction `9c0357235` pass locally and on all six native jobs in exact-head run [29381495240](https://github.com/stablyai/orca/actions/runs/29381495240). - - Implemented locally at `c847c4a11`: credential-free exact signing selection, exclusive staging, - returned-tree verification, and explicit POSIX/Windows CI wiring; exact-head runner proof is next. + - Proven locally and in exact-head run + [29382772805](https://github.com/stablyai/orca/actions/runs/29382772805): credential-free exact + signing selection, exclusive staging, returned-tree verification, and POSIX/Windows CI wiring. + - Active: target-native pre-sign assessment and real candidate-payload execution without credentials. - 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. 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 cf0e6409267..b086e3e3b0e 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps 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 native signing selection, exclusive payload staging, exact returned-tree verification, and explicit POSIX/Windows workflow wiring are implemented at `c847c4a11` and locally green under E-M3-NATIVE-SIGNING-STAGE-LOCAL-001. Replacement exact-head GitHub-runner proof must explicitly execute both new suites in every native job family before this slice is complete. The slice uses no credentials or signing service, establishes no native trust, publishes nothing, enables no tuple, and leaves legacy as the production default.
      +Session checkpoint: **In progress — 2026-07-14, Codex implementation owner** — credential-free native signing selection, exclusive payload staging, exact returned-tree verification, and explicit POSIX/Windows workflow wiring are implemented at `c847c4a11` and pass locally plus all six exact-head native jobs under E-M3-NATIVE-SIGNING-STAGE-LOCAL-001 and E-M3-NATIVE-SIGNING-STAGE-CI-001. The next safe package is target-native pre-sign assessment and real candidate-payload execution without credentials, signing calls, native-trust claims, publication, tuple enablement, or any change to the legacy 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) @@ -687,7 +687,8 @@ special entries, remain within explicit size/growth bounds, and change every sta This slice does not invoke Apple/SignPath, mutate a runtime, accept native trust, publish, aggregate, or enable a tuple. Implementation commit `c847c4a11` and local proof E-M3-NATIVE-SIGNING-STAGE-LOCAL-001 close the credential-free contract and workflow-source gates; -exact-head execution on all six native runner families remains required. +exact-head run 29382772805 closes execution on all six native runner families under +E-M3-NATIVE-SIGNING-STAGE-CI-001. **Baseline correction — 2026-07-14, Codex implementation owner:** E-M3-LINUX-BASELINE-LOCAL-RED-001 proves the existing Linux x64 candidate cannot load its patched @@ -7878,6 +7879,64 @@ diff --check`. both suites executed under Node 24.18.0 in all six native jobs while existing build/smoke/equality controls retain their outcomes. +### E-M3-NATIVE-SIGNING-STAGE-CI-001 — Both staging suites execute on all six native jobs + +- Date: 2026-07-14 (run timestamps 2026-07-15 UTC) +- Owner: Codex implementation owner +- Source/run: exact draft-PR head `9e08ac123039b827ebacbe1918ef8d3c3b989ae9`, containing + implementation `c847c4a11d60ee37d161564ca3685955ce3c2d6d` and local evidence + `9e08ac123039b827ebacbe1918ef8d3c3b989ae9`; Actions run + [29382772805](https://github.com/stablyai/orca/actions/runs/29382772805). +- Commands: + + ```sh + gh run view 29382772805 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh run view 29382772805 --repo stablyai/orca --job 87249764747 --log | \ + awk -F '\t' '$2 == "Run runtime artifact contract tests"' | \ + rg 'native-signing-(selection|payload)|Test Files|Tests|Duration' + gh run view 29382772805 --repo stablyai/orca --job 87249764693 --log | \ + awk -F '\t' '$2 == "Run runtime artifact contract tests"' | \ + rg 'native-signing-(selection|payload)|Test Files|Tests|Duration' + gh run view 29382772805 --repo stablyai/orca --job 87251086934 --log | \ + rg 'Operating System|OsBuildNumber|26100|26200' + gh run view 29382772825 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh run view 29382772821 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + ``` + +- Contract result: PASS on Linux x64/arm64, macOS x64/arm64, and Windows x64/arm64. Every native + build job reports `Run runtime artifact contract tests` successful under exact Node 24.18.0. The + shared POSIX log on macOS arm64 records four new syntax checks, selection 7/7 in 9 ms, payload + 11/11 in 204 ms, and aggregate 24 files/121 tests in 3.94 seconds. The shared PowerShell log on + Windows x64 records the same four syntax checks, selection 7/7 in 27 ms, payload 8 passed plus the + three declared POSIX-only symlink skips in 1.736 seconds, and aggregate 25 files/119 passed plus + six total platform skips out of 125 in 6.04 seconds. +- Native build/regression controls: all six two-build, smoke, exact-equality, metadata, and + unpublished-upload jobs passed. Durations were Linux x64 4m16s, Linux arm64 4m26s, macOS x64 + 5m16s, macOS arm64 2m58s, Windows x64 4m58s, and Windows arm64 10m40s. Linux x64/arm64 + digest-pinned glibc 2.28/libstdc++ 6.0.25 supplemental jobs passed in 37s/54s. Windows x64 exact + build-20348 floor passed in 1m23s. +- Expected aggregate status: the artifact workflow concluded failure only because Windows arm64 + hosted image 10.0.26200 again failed the exact declared 10.0.26100 floor in 4m9s. This preserves + E-M3-WINDOWS-ARM64-BASELINE-CI-RED-001 and is not a signing-stage regression. +- Other exact-head controls: PR Checks + [29382772825](https://github.com/stablyai/orca/actions/runs/29382772825) passed in 14m8s after lint, + typecheck, Git 2.25 compatibility, full tests, unpacked-app build, and packaged-CLI smoke. Golden + E2E [29382772821](https://github.com/stablyai/orca/actions/runs/29382772821) passed macOS/Linux in + 3m00s/4m29s. +- Oracle proved: both shell families parse and execute the exact credential-free selection and + payload contracts on every target-native runner while the artifact build, executable smoke, + clean-build equality, oldest-userland, packaging, and E2E regression controls retain their prior + outcomes. +- Does not prove: real candidate assessment or staging, Apple/SignPath credentials or calls, + returned signed bytes, native trust, final post-signing hashes, Gatekeeper/quarantine/notarized-app + provenance, Authenticode/Defender/WDAC policy, exact missing oldest snapshots, release + aggregation, SSH transfer/install, packaged desktop use, fallback/performance, or an enabled tuple. +- Follow-up: keep every tuple disabled; execute pre-sign assessment and staging against the actual + target-native candidate tree without credentials before connecting any protected signing job. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 1a79e492145a1297949a10ab6870d2118c5f5cd3 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 22:22:05 -0400 Subject: [PATCH 089/267] build(ssh): assess native signing candidates Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 95 +++++- ...ssh-relay-runtime-native-signing-stage.mjs | 87 +++++ ...elay-runtime-native-signing-stage.test.mjs | 218 ++++++++++++ ...untime-windows-authenticode-assessment.mjs | 224 +++++++++++++ ...e-windows-authenticode-assessment.test.mjs | 313 ++++++++++++++++++ .../ssh-relay-runtime-workflow.test.mjs | 45 +++ 6 files changed, 981 insertions(+), 1 deletion(-) create mode 100644 config/scripts/ssh-relay-runtime-native-signing-stage.mjs create mode 100644 config/scripts/ssh-relay-runtime-native-signing-stage.test.mjs create mode 100644 config/scripts/ssh-relay-runtime-windows-authenticode-assessment.mjs create mode 100644 config/scripts/ssh-relay-runtime-windows-authenticode-assessment.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index ccb0c6cd780..e12a73b799f 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -131,6 +131,10 @@ jobs: node --check config/scripts/ssh-relay-runtime-native-signing-selection.test.mjs node --check config/scripts/ssh-relay-runtime-native-signing-payload.mjs node --check config/scripts/ssh-relay-runtime-native-signing-payload.test.mjs + node --check config/scripts/ssh-relay-runtime-windows-authenticode-assessment.mjs + node --check config/scripts/ssh-relay-runtime-windows-authenticode-assessment.test.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-stage.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-stage.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 \ @@ -146,6 +150,8 @@ jobs: config/scripts/ssh-relay-runtime-native-signing-plan.test.mjs \ config/scripts/ssh-relay-runtime-native-signing-selection.test.mjs \ config/scripts/ssh-relay-runtime-native-signing-payload.test.mjs \ + config/scripts/ssh-relay-runtime-windows-authenticode-assessment.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-stage.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 \ @@ -224,6 +230,25 @@ jobs: --evidence-directory "$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}" \ --source-date-epoch "$source_date_epoch" \ --git-commit "$source_commit" + first_output="$output_root/first" + identity="$first_output/orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json" + signing_stage="$output_root/signing-stage" + signing_report="$output_root/orca-ssh-relay-runtime-${{ matrix.tuple }}.signing-stage.json" + node config/scripts/ssh-relay-runtime-native-signing-stage.mjs \ + --identity "$identity" \ + --runtime-directory "$first_output/runtime" \ + --staging-directory "$signing_stage" > "$signing_report" + node -e ' + const { readFileSync } = require("node:fs") + const report = JSON.parse(readFileSync(process.argv[1], "utf8")) + if (report.platform !== "linux" || report.assessments.length !== 0 || + report.signingFiles.length !== 0 || report.preservedUpstreamFiles.length !== 0 || + report.payload.stagingRequired !== false || report.payload.stagedFiles.length !== 0) { + throw new Error("Linux signing-stage report violates the hash-only contract") + } + ' "$signing_report" + test ! -e "$signing_stage" + cp "$signing_report" "$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}/" exit 0 fi mkdir -p "$output_root" @@ -256,9 +281,27 @@ jobs: --tuple '${{ matrix.tuple }}' \ --first-output-directory "$first_output" \ --second-output-directory "$second_output" + signing_stage="$output_root/signing-stage" + signing_report="$output_root/orca-ssh-relay-runtime-${{ matrix.tuple }}.signing-stage.json" + node config/scripts/ssh-relay-runtime-native-signing-stage.mjs \ + --identity "$first_output/orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json" \ + --runtime-directory "$first_output/runtime" \ + --staging-directory "$signing_stage" > "$signing_report" + node -e ' + const { readFileSync } = require("node:fs") + const report = JSON.parse(readFileSync(process.argv[1], "utf8")) + if (report.platform !== "darwin" || report.assessments.length !== 0 || + report.signingFiles.length !== 3 || report.preservedUpstreamFiles.length !== 0 || + report.payload.stagingRequired !== true || report.payload.stagedFiles.length !== 3) { + throw new Error("macOS signing-stage report violates the Developer ID contract") + } + ' "$signing_report" + test "$(find "$signing_stage" -type f | wc -l | tr -d ' ')" = 3 + test ! -e "$signing_stage/bin/node" + rm -rf -- "$signing_stage" mkdir -p "$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}" cp "$first_output"/*.tar.xz "$first_output"/*.spdx.json "$first_output"/*.provenance.json "$first_output"/*.identity.json \ - "$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}/" + "$signing_report" "$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}/" - name: Upload unpublished artifact evidence uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 @@ -400,6 +443,14 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime native signing payload script syntax check failed' } node --check config/scripts/ssh-relay-runtime-native-signing-payload.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime native signing payload test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-windows-authenticode-assessment.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime Windows Authenticode assessment script syntax check failed' } + node --check config/scripts/ssh-relay-runtime-windows-authenticode-assessment.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime Windows Authenticode assessment test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-native-signing-stage.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime native signing stage script syntax check failed' } + node --check config/scripts/ssh-relay-runtime-native-signing-stage.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime native signing stage 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 ` @@ -415,6 +466,8 @@ jobs: config/scripts/ssh-relay-runtime-native-signing-plan.test.mjs ` config/scripts/ssh-relay-runtime-native-signing-selection.test.mjs ` config/scripts/ssh-relay-runtime-native-signing-payload.test.mjs ` + config/scripts/ssh-relay-runtime-windows-authenticode-assessment.test.mjs ` + config/scripts/ssh-relay-runtime-native-signing-stage.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 ` @@ -524,6 +577,45 @@ jobs: } throw 'runtime reproducibility verification failed' } + $signingStage = Join-Path $outputRoot 'signing-stage' + $signingReport = Join-Path $outputRoot 'orca-ssh-relay-runtime-${{ matrix.tuple }}.signing-stage.json' + $reportLines = & node config/scripts/ssh-relay-runtime-native-signing-stage.mjs ` + --identity (Join-Path $firstOutput 'orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json') ` + --runtime-directory (Join-Path $firstOutput 'runtime') ` + --staging-directory $signingStage + if ($LASTEXITCODE -ne 0) { throw 'runtime native signing assessment/staging failed' } + $reportJson = $reportLines -join [Environment]::NewLine + $report = $reportJson | ConvertFrom-Json + if ( + $report.platform -ne 'win32' -or + @($report.assessments).Count -ne 5 -or + @($report.signingFiles).Count -ne 3 -or + @($report.preservedUpstreamFiles).Count -ne 2 -or + -not $report.payload.stagingRequired -or + @($report.payload.stagedFiles).Count -ne 3 + ) { + throw 'Windows signing-stage report violates the Authenticode contract' + } + $preservedPaths = @($report.preservedUpstreamFiles | ForEach-Object { $_.path }) + foreach ($path in @( + 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe', + 'node_modules/node-pty/build/Release/conpty/conpty.dll' + )) { + if ($path -notin $preservedPaths) { throw "Required upstream signature was not preserved: $path" } + } + if (@(Get-ChildItem -LiteralPath $signingStage -Recurse -File).Count -ne 3) { + throw 'Windows signing stage must contain exactly three unsigned PE files' + } + if (Test-Path -LiteralPath (Join-Path $signingStage 'bin/node.exe')) { + throw 'Official Node must never enter the Windows signing stage' + } + [System.IO.File]::WriteAllText( + $signingReport, + "$reportJson`n", + [System.Text.UTF8Encoding]::new($false) + ) + $reportJson + Remove-Item -LiteralPath $signingStage -Recurse -Force $evidence = Join-Path $env:GITHUB_WORKSPACE 'runtime-evidence/${{ matrix.tuple }}' New-Item -ItemType Directory -Path $evidence -Force | Out-Null $firstArchive = Get-ChildItem -LiteralPath $firstOutput -Filter '*.zip' -File | Select-Object -First 1 @@ -531,6 +623,7 @@ jobs: Get-ChildItem -LiteralPath $firstOutput -File | Where-Object { $_.Name -match '\.(?:spdx|provenance|identity)\.json$' } | Copy-Item -Destination $evidence + Copy-Item -LiteralPath $signingReport -Destination $evidence - name: Upload unpublished artifact evidence uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 diff --git a/config/scripts/ssh-relay-runtime-native-signing-stage.mjs b/config/scripts/ssh-relay-runtime-native-signing-stage.mjs new file mode 100644 index 00000000000..7e8a5afe3c3 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-native-signing-stage.mjs @@ -0,0 +1,87 @@ +import { resolve } from 'node:path' +import { pathToFileURL } from 'node:url' + +import { stageSshRelayRuntimeNativeSigningPayload } from './ssh-relay-runtime-native-signing-payload.mjs' +import { readSshRelayRuntimeNativeSigningIdentity } from './ssh-relay-runtime-native-signing-plan.mjs' +import { buildSshRelayRuntimeNativeSigningSelection } from './ssh-relay-runtime-native-signing-selection.mjs' +import { assessSshRelayRuntimeWindowsAuthenticode } from './ssh-relay-runtime-windows-authenticode-assessment.mjs' + +const ARGUMENT_FIELDS = new Map([ + ['--identity', 'identityPath'], + ['--runtime-directory', 'runtimeRoot'], + ['--staging-directory', 'stagingRoot'] +]) + +function valueAfter(argv, index, flag) { + const value = argv[index + 1] + if (!value || value.startsWith('--')) { + throw new Error(`Runtime native signing stage ${flag} requires a value`) + } + return value +} + +export function parseSshRelayRuntimeNativeSigningStageArguments(argv) { + const result = {} + for (let index = 0; index < argv.length; index += 2) { + const flag = argv[index] + const field = ARGUMENT_FIELDS.get(flag) + if (!field) { + throw new Error(`Unknown runtime native signing stage argument: ${flag}`) + } + if (result[field]) { + throw new Error(`Duplicate runtime native signing stage argument: ${flag}`) + } + result[field] = resolve(valueAfter(argv, index, flag)) + } + for (const field of ARGUMENT_FIELDS.values()) { + if (!result[field]) { + throw new Error(`Missing required runtime native signing stage argument: ${field}`) + } + } + return result +} + +export async function prepareSshRelayRuntimeNativeSigningStage({ + identity, + runtimeRoot, + stagingRoot, + platform = process.platform, + assessWindowsImpl = assessSshRelayRuntimeWindowsAuthenticode +}) { + if (identity?.os !== platform) { + // Why: pre-sign assessment is evidence only when the candidate executes on its target-native host. + throw new Error(`Runtime native signing stage requires target-native host: ${identity?.os}`) + } + const assessments = + platform === 'win32' ? await assessWindowsImpl({ identity, runtimeRoot, platform }) : [] + const selection = buildSshRelayRuntimeNativeSigningSelection(identity, assessments) + const payload = await stageSshRelayRuntimeNativeSigningPayload({ + runtimeRoot, + stagingRoot, + selection + }) + return { + tupleId: selection.tupleId, + platform: selection.platform, + policy: selection.policy, + assessments, + immutableVendorFiles: selection.immutableVendorFiles, + signingFiles: selection.signingFiles, + preservedUpstreamFiles: selection.preservedUpstreamFiles, + payload + } +} + +async function main() { + const options = parseSshRelayRuntimeNativeSigningStageArguments(process.argv.slice(2)) + const identity = await readSshRelayRuntimeNativeSigningIdentity(options.identityPath) + const report = await prepareSshRelayRuntimeNativeSigningStage({ ...options, identity }) + process.stdout.write(`${JSON.stringify(report, 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 native signing stage failed: ${error.stack ?? error}\n`) + process.exitCode = 1 + }) +} diff --git a/config/scripts/ssh-relay-runtime-native-signing-stage.test.mjs b/config/scripts/ssh-relay-runtime-native-signing-stage.test.mjs new file mode 100644 index 00000000000..9ddd4872eb3 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-native-signing-stage.test.mjs @@ -0,0 +1,218 @@ +import { createHash } from 'node:crypto' +import { lstat, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + +import { describe, expect, it } from 'vitest' + +import { expectedSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { + parseSshRelayRuntimeNativeSigningStageArguments, + prepareSshRelayRuntimeNativeSigningStage +} from './ssh-relay-runtime-native-signing-stage.mjs' +import { buildSshRelayRuntimeNativeSigningPlan } from './ssh-relay-runtime-native-signing-plan.mjs' + +function digest(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +function platformForTuple(tupleId) { + return tupleId.startsWith('linux-') ? 'linux' : tupleId.startsWith('darwin-') ? 'darwin' : 'win32' +} + +async function runtimeFixture(tupleId) { + const root = await mkdtemp(join(tmpdir(), 'ssh-relay-native-signing-stage-')) + const runtimeRoot = join(root, 'runtime') + await mkdir(runtimeRoot) + const entries = [] + for (const entry of expectedSshRelayRuntimeClosureEntries(tupleId)) { + if (entry.type === 'directory') { + await mkdir(join(runtimeRoot, ...entry.path.split('/')), { recursive: true }) + entries.push(entry) + continue + } + const bytes = Buffer.from(`fixture:${tupleId}:${entry.path}`) + const filePath = join(runtimeRoot, ...entry.path.split('/')) + await mkdir(dirname(filePath), { recursive: true }) + await writeFile(filePath, bytes, { mode: entry.mode }) + entries.push({ ...entry, size: bytes.length, sha256: digest(bytes) }) + } + return { + root, + runtimeRoot, + identity: { + tupleId, + os: platformForTuple(tupleId), + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries + } + } +} + +describe('SSH relay runtime native signing stage', () => { + it('parses exactly one identity, runtime, and exclusive stage path', () => { + const parsed = parseSshRelayRuntimeNativeSigningStageArguments([ + '--identity', + './identity.json', + '--runtime-directory', + './runtime', + '--staging-directory', + './stage' + ]) + expect(parsed.identityPath).toMatch(/identity[.]json$/) + expect(parsed.runtimeRoot).toMatch(/runtime$/) + expect(parsed.stagingRoot).toMatch(/stage$/) + expect(() => parseSshRelayRuntimeNativeSigningStageArguments([])).toThrow(/missing required/i) + expect(() => + parseSshRelayRuntimeNativeSigningStageArguments([ + '--identity', + 'a', + '--identity', + 'b', + '--runtime-directory', + 'runtime', + '--staging-directory', + 'stage' + ]) + ).toThrow(/duplicate/i) + expect(() => parseSshRelayRuntimeNativeSigningStageArguments(['--unknown', 'value'])).toThrow( + /unknown/i + ) + }) + + it('authenticates Linux candidates without assessment or staging', async () => { + const fixture = await runtimeFixture('linux-arm64-glibc') + let assessmentCalls = 0 + try { + const stagingRoot = join(fixture.root, 'stage') + const report = await prepareSshRelayRuntimeNativeSigningStage({ + identity: fixture.identity, + runtimeRoot: fixture.runtimeRoot, + stagingRoot, + platform: 'linux', + assessWindowsImpl: async () => { + assessmentCalls += 1 + return [] + } + }) + + expect(report).toEqual( + expect.objectContaining({ + tupleId: 'linux-arm64-glibc', + platform: 'linux', + policy: 'linux-hash-only-v1', + assessments: [], + signingFiles: [], + preservedUpstreamFiles: [], + payload: expect.objectContaining({ stagingRequired: false, stagedFiles: [] }) + }) + ) + expect(assessmentCalls).toBe(0) + await expect(lstat(stagingRoot)).rejects.toMatchObject({ code: 'ENOENT' }) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('stages every macOS signing candidate without assessment', async () => { + const fixture = await runtimeFixture('darwin-x64') + try { + const stagingRoot = join(fixture.root, 'stage') + const report = await prepareSshRelayRuntimeNativeSigningStage({ + identity: fixture.identity, + runtimeRoot: fixture.runtimeRoot, + stagingRoot, + platform: 'darwin' + }) + + expect(report.assessments).toEqual([]) + expect(report.signingFiles).toHaveLength(3) + expect(report.payload.stagedFiles).toHaveLength(3) + await expect(lstat(join(stagingRoot, 'bin', 'node'))).rejects.toMatchObject({ + code: 'ENOENT' + }) + for (const entry of report.payload.stagedFiles) { + await expect(readFile(join(stagingRoot, ...entry.path.split('/')))).resolves.toEqual( + await readFile(join(fixture.runtimeRoot, ...entry.path.split('/'))) + ) + } + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('assesses Windows once and stages only unsigned exact candidates', async () => { + const fixture = await runtimeFixture('win32-arm64') + const preservedPath = 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe' + let assessmentCalls = 0 + try { + const plan = buildSshRelayRuntimeNativeSigningPlan(fixture.identity) + const assessments = plan.signingCandidates.map((entry) => + entry.path === preservedPath + ? { + path: entry.path, + sourceSha256: entry.sourceSha256, + status: 'valid-upstream', + signerSubject: 'CN=Microsoft Corporation', + signerThumbprint: 'C'.repeat(40) + } + : { path: entry.path, sourceSha256: entry.sourceSha256, status: 'unsigned' } + ) + const stagingRoot = join(fixture.root, 'stage') + const report = await prepareSshRelayRuntimeNativeSigningStage({ + identity: fixture.identity, + runtimeRoot: fixture.runtimeRoot, + stagingRoot, + platform: 'win32', + assessWindowsImpl: async (options) => { + assessmentCalls += 1 + expect(options).toEqual( + expect.objectContaining({ + identity: fixture.identity, + runtimeRoot: fixture.runtimeRoot + }) + ) + return assessments + } + }) + + expect(assessmentCalls).toBe(1) + expect(report.assessments).toHaveLength(5) + expect(report.signingFiles).toHaveLength(4) + expect(report.preservedUpstreamFiles).toEqual([ + expect.objectContaining({ path: preservedPath, signerThumbprint: 'C'.repeat(40) }) + ]) + expect(report.payload.stagedFiles).toHaveLength(4) + await expect(lstat(join(stagingRoot, ...preservedPath.split('/')))).rejects.toMatchObject({ + code: 'ENOENT' + }) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('rejects cross-host execution before assessment or staging', async () => { + const fixture = await runtimeFixture('win32-x64') + let assessmentCalls = 0 + try { + const stagingRoot = join(fixture.root, 'stage') + await expect( + prepareSshRelayRuntimeNativeSigningStage({ + identity: fixture.identity, + runtimeRoot: fixture.runtimeRoot, + stagingRoot, + platform: 'linux', + assessWindowsImpl: async () => { + assessmentCalls += 1 + return [] + } + }) + ).rejects.toThrow(/target-native host/i) + expect(assessmentCalls).toBe(0) + await expect(lstat(stagingRoot)).rejects.toMatchObject({ code: 'ENOENT' }) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) +}) diff --git a/config/scripts/ssh-relay-runtime-windows-authenticode-assessment.mjs b/config/scripts/ssh-relay-runtime-windows-authenticode-assessment.mjs new file mode 100644 index 00000000000..414ca574783 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-windows-authenticode-assessment.mjs @@ -0,0 +1,224 @@ +import { createHash } from 'node:crypto' +import { spawnSync } from 'node:child_process' +import { createReadStream } from 'node:fs' +import { lstat, realpath } from 'node:fs/promises' +import { resolve } from 'node:path' + +import { buildSshRelayRuntimeNativeSigningPlan } from './ssh-relay-runtime-native-signing-plan.mjs' + +const AUTHENTICODE_JSON_FIELDS = ['signerSubject', 'signerThumbprint', 'status'] +const POWERSHELL_TIMEOUT_MS = 30_000 +const POWERSHELL_MAX_BUFFER_BYTES = 64 * 1024 + +const POWERSHELL_AUTHENTICODE_SCRIPT = String.raw` +$ErrorActionPreference = 'Stop' +$signature = Get-AuthenticodeSignature -LiteralPath $env:ORCA_SSH_RELAY_AUTHENTICODE_FILE +$certificate = $signature.SignerCertificate +[pscustomobject]@{ + status = $signature.Status.ToString() + signerSubject = if ($null -eq $certificate) { $null } else { $certificate.Subject } + signerThumbprint = if ($null -eq $certificate) { $null } else { $certificate.Thumbprint } +} | ConvertTo-Json -Compress +` + +function localPath(root, portablePath) { + if ( + typeof portablePath !== 'string' || + portablePath.length === 0 || + portablePath.includes('\\') || + portablePath.startsWith('/') || + portablePath.split('/').some((segment) => !segment || segment === '.' || segment === '..') + ) { + throw new Error(`Runtime Authenticode assessment rejects non-portable path: ${portablePath}`) + } + return resolve(root, ...portablePath.split('/')) +} + +async function sha256File(path) { + const hash = createHash('sha256') + for await (const chunk of createReadStream(path)) { + hash.update(chunk) + } + return `sha256:${hash.digest('hex')}` +} + +function containsAsciiControl(value) { + return [...value].some((character) => { + const codePoint = character.codePointAt(0) + return codePoint <= 0x1f || codePoint === 0x7f + }) +} + +function assertExactFields(value, expected, label) { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`Runtime Authenticode ${label} must be an object`) + } + const actual = Object.keys(value).sort() + if ( + actual.length !== expected.length || + actual.some((field, index) => field !== expected[index]) + ) { + throw new Error(`Runtime Authenticode ${label} has unexpected fields`) + } +} + +export function parseSshRelayRuntimeWindowsAuthenticodeJson(stdout) { + const source = typeof stdout === 'string' ? stdout.trim() : '' + if (!source) { + throw new Error('PowerShell did not return runtime Authenticode JSON') + } + let result + try { + result = JSON.parse(source) + } catch (error) { + throw new Error(`PowerShell returned malformed runtime Authenticode JSON: ${error.message}`) + } + assertExactFields(result, AUTHENTICODE_JSON_FIELDS, 'result') + if ( + typeof result.status !== 'string' || + ![null, 'string'].includes( + result.signerSubject === null ? null : typeof result.signerSubject + ) || + ![null, 'string'].includes( + result.signerThumbprint === null ? null : typeof result.signerThumbprint + ) + ) { + throw new Error('Runtime Authenticode result has malformed field types') + } + return result +} + +export function classifySshRelayRuntimeWindowsAuthenticode(signature) { + if (signature.status === 'NotSigned') { + if (signature.signerSubject !== null || signature.signerThumbprint !== null) { + throw new Error('Runtime Authenticode unsigned result unexpectedly contains a certificate') + } + return { status: 'unsigned' } + } + if (signature.status !== 'Valid') { + throw new Error(`Runtime Authenticode rejects signature status: ${signature.status}`) + } + if ( + typeof signature.signerSubject !== 'string' || + signature.signerSubject.length === 0 || + signature.signerSubject.length > 1024 || + containsAsciiControl(signature.signerSubject) || + typeof signature.signerThumbprint !== 'string' || + !/^[0-9a-f]{40}$/i.test(signature.signerThumbprint) + ) { + throw new Error('Runtime Authenticode valid result has malformed certificate identity') + } + return { + status: 'valid-upstream', + signerSubject: signature.signerSubject, + signerThumbprint: signature.signerThumbprint.toUpperCase() + } +} + +export function getSshRelayRuntimeWindowsAuthenticodeJson(path, spawnSyncImpl = spawnSync) { + // Why: the candidate path stays out of PowerShell source so spaces and metacharacters are data only. + const result = spawnSyncImpl( + 'pwsh', + ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', POWERSHELL_AUTHENTICODE_SCRIPT], + { + encoding: 'utf8', + env: { ...process.env, ORCA_SSH_RELAY_AUTHENTICODE_FILE: path }, + maxBuffer: POWERSHELL_MAX_BUFFER_BYTES, + timeout: POWERSHELL_TIMEOUT_MS, + windowsHide: true + } + ) + if (result?.error) { + throw result.error + } + if (result?.stderr?.trim()) { + throw new Error( + `PowerShell wrote to stderr during runtime Authenticode assessment: ${result.stderr.trim()}` + ) + } + if (result?.status !== 0) { + throw new Error( + `PowerShell runtime Authenticode assessment failed with exit code ${result?.status ?? ''}` + ) + } + if (typeof result.stdout !== 'string' || result.stdout.trim() === '') { + throw new Error('PowerShell did not return runtime Authenticode JSON') + } + return result.stdout +} + +async function authenticatedCandidate(physicalRuntimeRoot, identityEntry, planEntry) { + const path = localPath(physicalRuntimeRoot, planEntry.path) + const metadata = await lstat(path) + if (metadata.isSymbolicLink()) { + throw new Error(`Runtime Authenticode candidate is a symbolic link: ${planEntry.path}`) + } + if (!metadata.isFile()) { + throw new Error(`Runtime Authenticode candidate is not a regular file: ${planEntry.path}`) + } + if ((await realpath(path)) !== path) { + throw new Error(`Runtime Authenticode candidate traverses a symbolic link: ${planEntry.path}`) + } + if (metadata.size !== identityEntry.size) { + throw new Error( + `Runtime Authenticode candidate has wrong authenticated size: ${planEntry.path}` + ) + } + if ((await sha256File(path)) !== planEntry.sourceSha256) { + throw new Error( + `Runtime Authenticode candidate has wrong authenticated hash: ${planEntry.path}` + ) + } + return { path, planEntry } +} + +async function assertCandidateUnchanged(candidate) { + if ((await sha256File(candidate.path)) !== candidate.planEntry.sourceSha256) { + throw new Error( + `Runtime Authenticode candidate changed during assessment: ${candidate.planEntry.path}` + ) + } +} + +export async function assessSshRelayRuntimeWindowsAuthenticode({ + identity, + runtimeRoot, + platform = process.platform, + spawnSyncImpl = spawnSync +}) { + if (platform !== 'win32') { + throw new Error('Runtime Authenticode assessment requires Windows') + } + const plan = buildSshRelayRuntimeNativeSigningPlan(identity) + if (plan.platform !== 'win32') { + throw new Error( + `Runtime Authenticode assessment rejects non-Windows tuple: ${identity.tupleId}` + ) + } + const runtimeMetadata = await lstat(runtimeRoot) + if (runtimeMetadata.isSymbolicLink() || !runtimeMetadata.isDirectory()) { + throw new Error('Runtime Authenticode source root must be a real directory') + } + const physicalRuntimeRoot = await realpath(runtimeRoot) + const identityEntries = new Map(identity.entries.map((entry) => [entry.path, entry])) + const candidates = await Promise.all( + plan.signingCandidates.map((entry) => + authenticatedCandidate(physicalRuntimeRoot, identityEntries.get(entry.path), entry) + ) + ) + + const assessments = [] + for (const candidate of candidates) { + const signature = parseSshRelayRuntimeWindowsAuthenticodeJson( + getSshRelayRuntimeWindowsAuthenticodeJson(candidate.path, spawnSyncImpl) + ) + const classification = classifySshRelayRuntimeWindowsAuthenticode(signature) + await assertCandidateUnchanged(candidate) + assessments.push({ + path: candidate.planEntry.path, + sourceSha256: candidate.planEntry.sourceSha256, + ...classification + }) + } + return assessments +} diff --git a/config/scripts/ssh-relay-runtime-windows-authenticode-assessment.test.mjs b/config/scripts/ssh-relay-runtime-windows-authenticode-assessment.test.mjs new file mode 100644 index 00000000000..5c287dbc1aa --- /dev/null +++ b/config/scripts/ssh-relay-runtime-windows-authenticode-assessment.test.mjs @@ -0,0 +1,313 @@ +import { createHash } from 'node:crypto' +import { writeFileSync } from 'node:fs' +import { lstat, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + +import { describe, expect, it } from 'vitest' + +import { expectedSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { + assessSshRelayRuntimeWindowsAuthenticode, + classifySshRelayRuntimeWindowsAuthenticode, + getSshRelayRuntimeWindowsAuthenticodeJson, + parseSshRelayRuntimeWindowsAuthenticodeJson +} from './ssh-relay-runtime-windows-authenticode-assessment.mjs' + +function digest(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +async function windowsFixture(tupleId = 'win32-x64') { + const root = await mkdtemp(join(tmpdir(), 'ssh-relay-authenticode-assessment-')) + const runtimeRoot = join(root, 'runtime') + await mkdir(runtimeRoot) + const entries = [] + for (const entry of expectedSshRelayRuntimeClosureEntries(tupleId)) { + if (entry.type === 'directory') { + await mkdir(join(runtimeRoot, ...entry.path.split('/')), { recursive: true }) + entries.push(entry) + continue + } + const bytes = Buffer.from(`fixture:${tupleId}:${entry.path}`) + const filePath = join(runtimeRoot, ...entry.path.split('/')) + await mkdir(dirname(filePath), { recursive: true }) + await writeFile(filePath, bytes) + entries.push({ ...entry, size: bytes.length, sha256: digest(bytes) }) + } + return { + root, + runtimeRoot, + identity: { + tupleId, + os: 'win32', + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries + } + } +} + +function signatureJson({ + status = 'NotSigned', + signerSubject = null, + signerThumbprint = null +} = {}) { + return JSON.stringify({ status, signerSubject, signerThumbprint }) +} + +describe('SSH relay runtime Windows Authenticode assessment', () => { + it('classifies only exact unsigned and valid-upstream signature states', () => { + expect( + classifySshRelayRuntimeWindowsAuthenticode({ + status: 'NotSigned', + signerSubject: null, + signerThumbprint: null + }) + ).toEqual({ status: 'unsigned' }) + expect( + classifySshRelayRuntimeWindowsAuthenticode({ + status: 'Valid', + signerSubject: 'CN=Microsoft Corporation', + signerThumbprint: 'a'.repeat(40) + }) + ).toEqual({ + status: 'valid-upstream', + signerSubject: 'CN=Microsoft Corporation', + signerThumbprint: 'A'.repeat(40) + }) + + for (const status of ['HashMismatch', 'NotTrusted', 'UnknownError', 'PublisherMismatch']) { + expect(() => + classifySshRelayRuntimeWindowsAuthenticode({ + status, + signerSubject: null, + signerThumbprint: null + }) + ).toThrow(/rejects signature status/i) + } + }) + + it('rejects malformed JSON, fields, and status-qualified certificate metadata', () => { + expect(() => parseSshRelayRuntimeWindowsAuthenticodeJson('')).toThrow(/did not return/i) + expect(() => parseSshRelayRuntimeWindowsAuthenticodeJson('{')).toThrow(/malformed/i) + expect(() => + parseSshRelayRuntimeWindowsAuthenticodeJson( + JSON.stringify({ + status: 'NotSigned', + signerSubject: null, + signerThumbprint: null, + extra: true + }) + ) + ).toThrow(/unexpected fields/i) + expect(() => + classifySshRelayRuntimeWindowsAuthenticode({ + status: 'NotSigned', + signerSubject: 'CN=Unexpected', + signerThumbprint: null + }) + ).toThrow(/unsigned.*certificate/i) + expect(() => + classifySshRelayRuntimeWindowsAuthenticode({ + status: 'Valid', + signerSubject: '', + signerThumbprint: 'bad' + }) + ).toThrow(/valid.*certificate/i) + }) + + it('uses a bounded noninteractive PowerShell probe with the path only in the environment', () => { + const calls = [] + const stdout = signatureJson() + const result = getSshRelayRuntimeWindowsAuthenticodeJson( + 'C:\\Path With Spaces\\candidate.node', + (command, args, options) => { + calls.push({ command, args, options }) + return { status: 0, stdout, stderr: '' } + } + ) + + expect(result).toBe(stdout) + expect(calls).toHaveLength(1) + expect(calls[0].command).toBe('pwsh') + expect(calls[0].args).toEqual( + expect.arrayContaining(['-NoLogo', '-NoProfile', '-NonInteractive', '-Command']) + ) + expect(calls[0].args.join(' ')).not.toContain('Path With Spaces') + expect(calls[0].options).toEqual( + expect.objectContaining({ + encoding: 'utf8', + timeout: 30_000, + maxBuffer: 64 * 1024, + windowsHide: true, + env: expect.objectContaining({ + ORCA_SSH_RELAY_AUTHENTICODE_FILE: 'C:\\Path With Spaces\\candidate.node' + }) + }) + ) + }) + + it('fails on probe errors, stderr, nonzero exit, or empty output', () => { + expect(() => + getSshRelayRuntimeWindowsAuthenticodeJson('candidate.node', () => ({ + error: new Error('timed out'), + status: null, + stdout: '', + stderr: '' + })) + ).toThrow(/timed out/i) + expect(() => + getSshRelayRuntimeWindowsAuthenticodeJson('candidate.node', () => ({ + status: 0, + stdout: signatureJson(), + stderr: 'warning' + })) + ).toThrow(/stderr/i) + expect(() => + getSshRelayRuntimeWindowsAuthenticodeJson('candidate.node', () => ({ + status: 7, + stdout: '', + stderr: '' + })) + ).toThrow(/exit code 7/i) + expect(() => + getSshRelayRuntimeWindowsAuthenticodeJson('candidate.node', () => ({ + status: 0, + stdout: '', + stderr: '' + })) + ).toThrow(/did not return/i) + }) + + it('hash-binds and assesses every exact Windows signing candidate', async () => { + const fixture = await windowsFixture() + const calls = [] + try { + const assessments = await assessSshRelayRuntimeWindowsAuthenticode({ + identity: fixture.identity, + runtimeRoot: fixture.runtimeRoot, + platform: 'win32', + spawnSyncImpl: (_command, _args, options) => { + calls.push(options.env.ORCA_SSH_RELAY_AUTHENTICODE_FILE) + const isUpstream = + options.env.ORCA_SSH_RELAY_AUTHENTICODE_FILE.endsWith('OpenConsole.exe') + return { + status: 0, + stdout: isUpstream + ? signatureJson({ + status: 'Valid', + signerSubject: 'CN=Microsoft Corporation', + signerThumbprint: 'b'.repeat(40) + }) + : signatureJson(), + stderr: '' + } + } + }) + + expect(assessments).toHaveLength(5) + expect(calls).toHaveLength(5) + expect(assessments.find((entry) => entry.status === 'valid-upstream')).toEqual( + expect.objectContaining({ + path: 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe', + signerSubject: 'CN=Microsoft Corporation', + signerThumbprint: 'B'.repeat(40) + }) + ) + expect(assessments.filter((entry) => entry.status === 'unsigned')).toHaveLength(4) + for (const assessment of assessments) { + expect(assessment.sourceSha256).toMatch(/^sha256:[0-9a-f]{64}$/) + } + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('rejects host mismatch and source mutation before the first probe', async () => { + const fixture = await windowsFixture('win32-arm64') + let calls = 0 + const spawnSyncImpl = () => { + calls += 1 + return { status: 0, stdout: signatureJson(), stderr: '' } + } + try { + await expect( + assessSshRelayRuntimeWindowsAuthenticode({ + identity: fixture.identity, + runtimeRoot: fixture.runtimeRoot, + platform: 'darwin', + spawnSyncImpl + }) + ).rejects.toThrow(/requires Windows/i) + + const candidate = fixture.identity.entries.find( + (entry) => entry.type === 'file' && entry.role === 'node-pty-native' + ) + await writeFile(join(fixture.runtimeRoot, ...candidate.path.split('/')), 'mutated') + await expect( + assessSshRelayRuntimeWindowsAuthenticode({ + identity: fixture.identity, + runtimeRoot: fixture.runtimeRoot, + platform: 'win32', + spawnSyncImpl + }) + ).rejects.toThrow(/authenticated (size|hash)/i) + expect(calls).toBe(0) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('rejects a candidate changed during its PowerShell assessment', async () => { + const fixture = await windowsFixture() + let calls = 0 + try { + await expect( + assessSshRelayRuntimeWindowsAuthenticode({ + identity: fixture.identity, + runtimeRoot: fixture.runtimeRoot, + platform: 'win32', + spawnSyncImpl: (_command, _args, options) => { + calls += 1 + writeFileSync(options.env.ORCA_SSH_RELAY_AUTHENTICODE_FILE, 'changed-during-probe') + return { status: 0, stdout: signatureJson(), stderr: '' } + } + }) + ).rejects.toThrow(/changed during assessment/i) + expect(calls).toBe(1) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it.skipIf(process.platform === 'win32')( + 'rejects a symlinked candidate before PowerShell', + async () => { + const fixture = await windowsFixture() + try { + const candidate = fixture.identity.entries.find( + (entry) => entry.type === 'file' && entry.role === 'node-pty-native' + ) + const candidatePath = join(fixture.runtimeRoot, ...candidate.path.split('/')) + const target = join(fixture.root, 'candidate-target') + await writeFile(target, await readFile(candidatePath)) + await rm(candidatePath) + await symlink(target, candidatePath) + await expect( + assessSshRelayRuntimeWindowsAuthenticode({ + identity: fixture.identity, + runtimeRoot: fixture.runtimeRoot, + platform: 'win32', + spawnSyncImpl: () => { + throw new Error('should not probe') + } + }) + ).rejects.toThrow(/symbolic link/i) + expect((await lstat(candidatePath)).isSymbolicLink()).toBe(true) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + } + ) +}) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index fd24d8b48ef..d90ce593555 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -84,6 +84,10 @@ describe('SSH relay runtime artifact workflow', () => { expect(source.match(/ssh-relay-runtime-native-signing-plan\.test\.mjs/g)).toHaveLength(4) expect(source.match(/ssh-relay-runtime-native-signing-selection\.test\.mjs/g)).toHaveLength(4) expect(source.match(/ssh-relay-runtime-native-signing-payload\.test\.mjs/g)).toHaveLength(4) + expect( + source.match(/ssh-relay-runtime-windows-authenticode-assessment\.test\.mjs/g) + ).toHaveLength(4) + expect(source.match(/ssh-relay-runtime-native-signing-stage\.test\.mjs/g)).toHaveLength(4) expect( source.match(/node --check config\/scripts\/ssh-relay-runtime-native-signing-plan\.mjs/g) ).toHaveLength(2) @@ -93,6 +97,14 @@ describe('SSH relay runtime artifact workflow', () => { expect( source.match(/node --check config\/scripts\/ssh-relay-runtime-native-signing-payload\.mjs/g) ).toHaveLength(2) + expect( + source.match( + /node --check config\/scripts\/ssh-relay-runtime-windows-authenticode-assessment\.mjs/g + ) + ).toHaveLength(2) + expect( + source.match(/node --check config\/scripts\/ssh-relay-runtime-native-signing-stage\.mjs/g) + ).toHaveLength(2) expect( source.match(/node --check config\/scripts\/ssh-relay-runtime-closure\.mjs/g) ).toHaveLength(2) @@ -158,6 +170,39 @@ describe('SSH relay runtime artifact workflow', () => { expect(source).not.toMatch(/releases\/|gh release|contents:\s*write/i) }) + it('assesses and stages real first-build candidates without signing authority', async () => { + const source = await readFile(workflowUrl, 'utf8') + const workflow = parse(source) + const posixRun = workflow.jobs['build-posix-runtime'].steps.find( + (step) => step.name === 'Build twice, inspect, smoke, and compare exact runtime' + ).run + const windowsRun = workflow.jobs['build-windows-runtime'].steps.find( + (step) => step.name === 'Build twice, inspect, smoke, and compare exact runtime' + ).run + + expect(posixRun.match(/ssh-relay-runtime-native-signing-stage\.mjs/g)).toHaveLength(2) + expect(windowsRun.match(/ssh-relay-runtime-native-signing-stage\.mjs/g)).toHaveLength(1) + expect(posixRun.indexOf('ssh-relay-runtime-native-signing-stage.mjs')).toBeGreaterThan( + posixRun.indexOf('ssh-relay-runtime-linux-build-evidence.mjs') + ) + expect(posixRun.lastIndexOf('ssh-relay-runtime-native-signing-stage.mjs')).toBeGreaterThan( + posixRun.indexOf('ssh-relay-runtime-reproducibility.mjs') + ) + expect(windowsRun.indexOf('ssh-relay-runtime-native-signing-stage.mjs')).toBeGreaterThan( + windowsRun.indexOf('ssh-relay-runtime-reproducibility.mjs') + ) + expect(posixRun).toContain('Linux signing-stage report violates the hash-only contract') + expect(posixRun).toContain('macOS signing-stage report violates the Developer ID contract') + expect(posixRun).toContain('test ! -e "$signing_stage/bin/node"') + expect(windowsRun).toContain('Windows signing-stage report violates the Authenticode contract') + expect(windowsRun).toContain('@($report.signingFiles).Count -ne 3') + expect(windowsRun).toContain('@($report.preservedUpstreamFiles).Count -ne 2') + expect(windowsRun).toContain('Required upstream signature was not preserved') + expect(windowsRun).toContain("Join-Path $signingStage 'bin/node.exe'") + expect(source.match(/\.signing-stage\.json/g)).toHaveLength(3) + expect(source).not.toMatch(/SIGNPATH_|APPLE_(?:ID|KEY)|Developer ID Application/) + }) + it('builds Linux native modules in the pinned oldest userland with an offline build phase', async () => { const source = await readFile(workflowUrl, 'utf8') const workflow = parse(source) From 82f6c0cefa9d7ac031d56bcfd0b85a2ab71a7d8b Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 22:24:56 -0400 Subject: [PATCH 090/267] docs(ssh): record native assessment proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 3 +- ...github-release-implementation-checklist.md | 109 +++++++++++++++++- 2 files changed, 110 insertions(+), 2 deletions(-) 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 098be9426d2..fafaf26916b 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 @@ -34,7 +34,8 @@ complete a box. - Proven locally and in exact-head run [29382772805](https://github.com/stablyai/orca/actions/runs/29382772805): credential-free exact signing selection, exclusive staging, returned-tree verification, and POSIX/Windows CI wiring. - - Active: target-native pre-sign assessment and real candidate-payload execution without credentials. + - Implemented locally at `1a79e4921`: target-native pre-sign assessment and real first-build + candidate staging without credentials; exact-head native runner evidence is next. - 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. 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 b086e3e3b0e..a2f7fc21d33 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps 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 native signing selection, exclusive payload staging, exact returned-tree verification, and explicit POSIX/Windows workflow wiring are implemented at `c847c4a11` and pass locally plus all six exact-head native jobs under E-M3-NATIVE-SIGNING-STAGE-LOCAL-001 and E-M3-NATIVE-SIGNING-STAGE-CI-001. The next safe package is target-native pre-sign assessment and real candidate-payload execution without credentials, signing calls, native-trust claims, publication, tuple enablement, or any change to the legacy production default.
      +Session checkpoint: **In progress — 2026-07-14, Codex implementation owner** — target-native pre-sign assessment, real first-build candidate staging, unpublished JSON evidence, and cleanup are implemented at `1a79e4921` and locally green under E-M3-NATIVE-ASSESSMENT-LOCAL-001. Exact-head proof must now show Linux hash-only verification, three-file macOS staging, and Windows Authenticode preservation/staging on both architectures. This package uses no credentials or signing service, establishes no native trust, publishes nothing, enables no tuple, and leaves legacy as 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) @@ -690,6 +690,16 @@ E-M3-NATIVE-SIGNING-STAGE-LOCAL-001 close the credential-free contract and workf exact-head run 29382772805 closes execution on all six native runner families under E-M3-NATIVE-SIGNING-STAGE-CI-001. +**Active target-native assessment slice — 2026-07-14, Codex implementation owner:** authenticate +every real first-build candidate before and after bounded platform inspection. Linux must create no +payload; macOS must stage exactly three non-Node candidates; Windows must invoke one noninteractive, +30-second/64-KiB-bounded PowerShell Authenticode probe per exact PE path, accept only `NotSigned` or +`Valid`, preserve exact `OpenConsole.exe` and `conpty.dll` bytes only when valid, and stage exactly +the three unsigned Orca-built `.node` files. Candidate paths stay out of PowerShell source, any +hash/status/certificate drift fails closed, stage JSON is retained only as unpublished evidence, +and the temporary payload is removed. Local implementation passes at `1a79e4921` under +E-M3-NATIVE-ASSESSMENT-LOCAL-001; exact-head native execution remains required. + **Baseline correction — 2026-07-14, Codex implementation owner:** E-M3-LINUX-BASELINE-LOCAL-RED-001 proves the existing Linux x64 candidate cannot load its patched `node-pty` on Rocky 8.9/glibc 2.28 because the Ubuntu 24.04 producer emitted references to @@ -7937,6 +7947,103 @@ diff --check`. - Follow-up: keep every tuple disabled; execute pre-sign assessment and staging against the actual target-native candidate tree without credentials before connecting any protected signing job. +### E-M3-NATIVE-ASSESSMENT-LOCAL-RED-001 — Missing native assessment and orchestration modules fail focused contracts + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: pre-implementation head `e478ae458fa634845e9d9a7fd1d5a01cb41ed6ed`. +- Commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-windows-authenticode-assessment.test.mjs + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-native-signing-stage.test.mjs + ``` + +- Result: expected RED. The Authenticode suite failed 1 file/0 tests in 396 ms because + `ssh-relay-runtime-windows-authenticode-assessment.mjs` did not exist. The target-native stage + suite independently failed 1 file/0 tests in 159 ms because + `ssh-relay-runtime-native-signing-stage.mjs` did not exist. No production or artifact behavior + changed. +- Oracle proved: neither actual Windows signature classification nor target-native first-build + orchestration was already provided by the credential-free selection/payload modules. +- Does not prove: any implementation, real native candidate inspection, credentials, signing, + native trust, release aggregation, SSH behavior, or an enabled tuple. +- Correction: add purpose-named bounded Authenticode assessment and target-native stage modules, + then wire real first-build candidate execution into all native artifact jobs. + +### E-M3-NATIVE-ASSESSMENT-LOCAL-001 — Bounded target-native assessment and first-build staging pass locally + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: implementation commit `1a79e492145a1297949a10ab6870d2118c5f5cd3`, based on exact staging-CI evidence commit + `e478ae458fa634845e9d9a7fd1d5a01cb41ed6ed`. +- Runner/remote/network: local macOS 26.2 build 25C56 arm64, Node v26.0.0, pnpm 10.24.0, and + PowerShell 7 at `/opt/homebrew/bin/pwsh`; no remote, network, credential, signing service, + native-trust environment, or publication path was used. Windows execution was dependency-injected + locally and remains an explicit CI requirement. +- Commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-windows-authenticode-assessment.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-stage.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-*.test.mjs + node --check config/scripts/ssh-relay-runtime-windows-authenticode-assessment.mjs + node --check config/scripts/ssh-relay-runtime-windows-authenticode-assessment.test.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-stage.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-stage.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-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-windows-authenticode-assessment.mjs \ + config/scripts/ssh-relay-runtime-windows-authenticode-assessment.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-stage.mjs \ + config/scripts/ssh-relay-runtime-native-signing-stage.test.mjs + node --input-type=module -e \ + 'import {readFile} from "node:fs/promises"; import {parse} from "yaml"; const workflow=parse(await readFile(".github/workflows/ssh-relay-runtime-artifacts.yml","utf8")); process.stdout.write(workflow.jobs["build-windows-runtime"].steps.find((step)=>step.name==="Build twice, inspect, smoke, and compare exact runtime").run)' | \ + pwsh -NoLogo -NoProfile -NonInteractive -Command \ + '$source=[Console]::In.ReadToEnd(); $tokens=$null; $errors=$null; [System.Management.Automation.Language.Parser]::ParseInput($source,[ref]$tokens,[ref]$errors)|Out-Null; if($errors.Count){exit 1}' + git diff --check + ``` + +- Result: PASS. The assessment, stage, and workflow suites passed 3 files/19 tests in 1.31 seconds; + the complete SSH relay script suite passed 27 files/139 tests in 5.82 seconds. Four direct syntax + checks, typecheck, full lint/reliability gates, max-lines (355 grandfathered suppressions and no + new bypass), bundled-skill verification, localization catalog/coverage, focused formatting, + direct PowerShell parsing of the modified Windows step, and `git diff --check` passed. Lint emitted + only existing unrelated warnings; local Node 26 emitted the expected repository Node-24 engine + warning. +- Assessment oracle: the Windows module derives exactly five candidates from the authenticated + identity, validates every size/hash before the first probe, passes the local path only through a + dedicated environment variable, and uses `pwsh -NoLogo -NoProfile -NonInteractive` with a + 30-second timeout, 64-KiB output bound, and hidden window. It accepts only exact three-field JSON: + `NotSigned` without certificate metadata or `Valid` with a bounded non-control subject and exact + 40-hex thumbprint. Hash mismatch, invalid/unknown status, malformed fields/JSON, stderr, nonzero + exit, timeout/error, links, host mismatch, or mutation during the probe reject. +- Orchestration oracle: host platform must equal the identity OS before assessment. Linux + authenticates three native files without creating a stage; macOS stages exactly its three non-Node + candidates; Windows runs assessment once, preserves valid-upstream candidates, and stages only + unsigned candidates. Official Node remains verification-only. +- Workflow oracle: both native shell families syntax-check and execute both new suites. The actual + first clean-build tree is assessed only after archive/tree verification and clean-build equality. + Linux requires no stage, macOS requires three staged files, and Windows requires five assessments, + three unsigned staged `.node` files, and the exact preserved upstream `OpenConsole.exe` and + `conpty.dll` paths. Every temporary stage excludes official Node, is removed after inspection, and + emits one unpublished `.signing-stage.json` evidence file. No signing secret, credential, service, + permission, or publication authority was introduced. +- Does not prove: real Windows PowerShell status/signer output, actual macOS/Linux candidate staging, + Node 24 behavior, target-native filesystem behavior, Apple/SignPath calls, returned signed bytes, + native trust, final post-signing hashes, missing oldest snapshots, release aggregation, SSH + transfer/install, packaged desktop use, fallback/performance, or an enabled tuple. +- Follow-up: push the implementation and evidence separately; require exact-head logs and downloaded + stage reports from all six jobs before accepting real candidate behavior. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 6b93976909f9df6373414ecc65daf6e3eaff29b9 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 22:42:29 -0400 Subject: [PATCH 091/267] docs(ssh): record native assessment CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 6 +- ...github-release-implementation-checklist.md | 89 ++++++++++++++++++- 2 files changed, 91 insertions(+), 4 deletions(-) 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 fafaf26916b..d9164dd0aa1 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 @@ -34,8 +34,10 @@ complete a box. - Proven locally and in exact-head run [29382772805](https://github.com/stablyai/orca/actions/runs/29382772805): credential-free exact signing selection, exclusive staging, returned-tree verification, and POSIX/Windows CI wiring. - - Implemented locally at `1a79e4921`: target-native pre-sign assessment and real first-build - candidate staging without credentials; exact-head native runner evidence is next. + - Proven locally and in exact-head run + [29384042509](https://github.com/stablyai/orca/actions/runs/29384042509): target-native pre-sign + assessment and real first-build candidate staging without credentials. + - Active next: exact returned-file application and final post-sign runtime identity contracts. - 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. 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 a2f7fc21d33..2a530f99a94 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps 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** — target-native pre-sign assessment, real first-build candidate staging, unpublished JSON evidence, and cleanup are implemented at `1a79e4921` and locally green under E-M3-NATIVE-ASSESSMENT-LOCAL-001. Exact-head proof must now show Linux hash-only verification, three-file macOS staging, and Windows Authenticode preservation/staging on both architectures. This package uses no credentials or signing service, establishes no native trust, publishes nothing, enables no tuple, and leaves legacy as the production default.
      +Session checkpoint: **In progress — 2026-07-14, Codex implementation owner** — target-native pre-sign assessment, real first-build candidate staging, unpublished JSON evidence, and cleanup are implemented at `1a79e4921` and pass locally plus all six exact-head native jobs under E-M3-NATIVE-ASSESSMENT-LOCAL-001 and E-M3-NATIVE-ASSESSMENT-CI-001. The next safe credential-free package is exact returned-file application plus final post-sign runtime identity/closure contracts; real Apple/SignPath calls 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) @@ -698,7 +698,8 @@ payload; macOS must stage exactly three non-Node candidates; Windows must invoke the three unsigned Orca-built `.node` files. Candidate paths stay out of PowerShell source, any hash/status/certificate drift fails closed, stage JSON is retained only as unpublished evidence, and the temporary payload is removed. Local implementation passes at `1a79e4921` under -E-M3-NATIVE-ASSESSMENT-LOCAL-001; exact-head native execution remains required. +E-M3-NATIVE-ASSESSMENT-LOCAL-001; exact-head run 29384042509 closes all six real candidate cells +under E-M3-NATIVE-ASSESSMENT-CI-001. **Baseline correction — 2026-07-14, Codex implementation owner:** E-M3-LINUX-BASELINE-LOCAL-RED-001 proves the existing Linux x64 candidate cannot load its patched @@ -8044,6 +8045,90 @@ diff --check`. - Follow-up: push the implementation and evidence separately; require exact-head logs and downloaded stage reports from all six jobs before accepting real candidate behavior. +### E-M3-NATIVE-ASSESSMENT-CI-001 — All six real candidates pass native assessment and exclusive staging + +- Date: 2026-07-14 (run timestamps 2026-07-15 UTC) +- Owner: Codex implementation owner +- Source/run: exact draft-PR head `82f6c0cefa9d7ac031d56bcfd0b85a2ab71a7d8b`, containing + implementation `1a79e492145a1297949a10ab6870d2118c5f5cd3` and local evidence + `82f6c0cefa9d7ac031d56bcfd0b85a2ab71a7d8b`; Actions run + [29384042509](https://github.com/stablyai/orca/actions/runs/29384042509). +- Commands: + + ```sh + gh run view 29384042509 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh run view 29384042509 --repo stablyai/orca --job 87253495614 --log | \ + awk -F '\t' '$2 == "Run runtime artifact contract tests"' | \ + rg 'native-signing-stage|windows-authenticode-assessment|Test Files|Tests|Duration' + gh run view 29384042509 --repo stablyai/orca --job 87253495617 --log | \ + awk -F '\t' '$2 == "Run runtime artifact contract tests"' | \ + rg 'native-signing-stage|windows-authenticode-assessment|Test Files|Tests|Duration' + gh run download 29384042509 --repo stablyai/orca \ + --dir /private/tmp/orca-8450-run-29384042509-artifacts \ + --pattern 'ssh-relay-runtime-*' + node --input-type=module # inline six-report identity/cardinality/hash audit recorded below + gh run view 29384042507 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh run view 29384042498 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + ``` + +- Contract result: PASS on Linux x64/arm64, macOS x64/arm64, and Windows x64/arm64. Every native + build job syntax-checked and ran both new suites under exact Node 24.18.0. The POSIX log records + Authenticode contract 8/8, target-native stage 5/5, and aggregate 26 files/135 tests in 6.78 + seconds. The PowerShell log records Authenticode contract 7 passed plus one POSIX-only symlink skip, + target-native stage 5/5, and aggregate 27 files/132 passed plus seven total platform skips out of + 139 in 7.33 seconds. +- Real first-build result: PASS. Both Linux reports authenticate the exact three native identity + entries, select/stage zero files, and create no payload. Both macOS reports select and stage exactly + three non-Node files, with no assessment or preserved-upstream entry. Both Windows reports contain + exactly five hash-bound PowerShell assessments, preserve exactly `OpenConsole.exe` and + `conpty.dll` as `Valid`, and stage exactly the three `NotSigned` Orca-built `.node` files. All six + builds confirm official Node is verification-only, delete the temporary stage, and upload only the + JSON report plus the prior unpublished evidence set. +- Downloaded-report audit: PASS. Six artifacts contained exactly one identity and signing-stage + report each. An inline Node audit required tuple/platform equality; matched every immutable, + assessment, signing, preserved, and staged path/size/SHA-256 back to the identity; required staged + paths and size totals to equal the signing selection; enforced the platform cardinalities; and + rechecked official-Node exclusion and the exact Windows preserved set. Exact report evidence: + + | Tuple | Report SHA-256 | Assess | Sign | Preserve | Stage | Staged bytes | + | ----------------- | ------------------------------------------------------------------ | ------ | ---- | -------- | ----- | ------------ | + | linux-x64-glibc | `9376647f4ac66060a82bab660752b961fedfc374d1b3fbc76fe60b6f35590a3b` | 0 | 0 | 0 | 0 | 0 | + | linux-arm64-glibc | `453fe413e81aa475149f92180f54713293de0562fe86b049746d8ec77c449f1e` | 0 | 0 | 0 | 0 | 0 | + | darwin-x64 | `ae9ac0afb40bd5a07ff2dba78d9a4c62bb7662f4f21a9483bd0c7f5469ef0162` | 0 | 3 | 0 | 3 | 393,744 | + | darwin-arm64 | `dbfae0d923d3a18cd042939d8fa174e1e0420aad548bb5caf6256aed2b37a22e` | 0 | 3 | 0 | 3 | 459,592 | + | win32-x64 | `69d6685d8c8dc54682a3af25402a1e489d11fc4a1771fd9ca599ea2c7864ddff` | 5 | 3 | 2 | 3 | 2,114,048 | + | win32-arm64 | `50f85ebb09695bdc4443add6f267add66f099ee2b4b0fe9108a35f1f7db644f3` | 5 | 3 | 2 | 3 | 2,224,128 | + +- Windows signer evidence: all four preserved entries report exact subject + `CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US`. x64 + `OpenConsole.exe` and `conpty.dll` use thumbprint + `3F56A45111684D454E231CFDC4DA5C8D370F9816`. arm64 `conpty.dll` uses that thumbprint and arm64 + `OpenConsole.exe` uses `F5877012FBD62FABCBDC8D8CEE9C9585BA30DF79`. These observations bind the + unchanged source bytes but do not establish final Orca native trust. +- Native build/regression controls: all six double-build, smoke, exact-equality, metadata, stage, + cleanup, and unpublished-upload jobs passed in Linux x64 3m58s, Linux arm64 4m48s, macOS x64 + 6m08s, macOS arm64 3m32s, Windows x64 5m35s, and Windows arm64 9m02s. Linux x64/arm64 oldest- + userland supplements passed in 37s/53s; Windows x64 exact build-20348 floor passed in 1m24s. +- Expected aggregate status: the artifact workflow concluded failure only because Windows arm64 + hosted image 10.0.26200 again rejected the exact declared 10.0.26100 floor in 3m46s. The failure + preserves E-M3-WINDOWS-ARM64-BASELINE-CI-RED-001 and is not an assessment/staging regression. +- Other exact-head controls: PR Checks + [29384042507](https://github.com/stablyai/orca/actions/runs/29384042507) passed in 13m59s. Golden E2E + [29384042498](https://github.com/stablyai/orca/actions/runs/29384042498) passed macOS/Linux in + 4m02s/4m31s. +- Oracle proved: real first-build native candidate bytes on all six runner families obey the exact + credential-free pre-sign assessment, preservation, exclusive staging, evidence, and cleanup + contract without changing prior build/runtime outcomes. +- Does not prove: Apple Developer ID or SignPath invocation/approval, returned signed bytes, final + post-signing hashes, Gatekeeper/quarantine/notarized-app provenance, final SignPath signer policy, + Defender/WDAC, exact missing oldest snapshots, release aggregation, SSH transfer/install, packaged + desktop use, fallback/performance, or an enabled tuple. +- Follow-up: keep every tuple disabled; implement exact returned-file application and final runtime + identity/closure contracts before any credentialed signing job is allowed to feed aggregation. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 3eeea7bdb8b10825416d7f9bac03d5a820a997f5 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 22:53:48 -0400 Subject: [PATCH 092/267] build(ssh): apply native signing returns safely Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 8 + ...ssh-relay-runtime-native-signing-apply.mjs | 206 +++++++++++ ...elay-runtime-native-signing-apply.test.mjs | 322 ++++++++++++++++++ .../ssh-relay-runtime-workflow.test.mjs | 4 + 4 files changed, 540 insertions(+) create mode 100644 config/scripts/ssh-relay-runtime-native-signing-apply.mjs create mode 100644 config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index e12a73b799f..79c0088adfa 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -135,6 +135,8 @@ jobs: node --check config/scripts/ssh-relay-runtime-windows-authenticode-assessment.test.mjs node --check config/scripts/ssh-relay-runtime-native-signing-stage.mjs node --check config/scripts/ssh-relay-runtime-native-signing-stage.test.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-apply.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-apply.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 \ @@ -152,6 +154,7 @@ jobs: config/scripts/ssh-relay-runtime-native-signing-payload.test.mjs \ config/scripts/ssh-relay-runtime-windows-authenticode-assessment.test.mjs \ config/scripts/ssh-relay-runtime-native-signing-stage.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-apply.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 \ @@ -451,6 +454,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime native signing stage script syntax check failed' } node --check config/scripts/ssh-relay-runtime-native-signing-stage.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime native signing stage test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-native-signing-apply.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime native signing apply script syntax check failed' } + node --check config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime native signing apply 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 ` @@ -468,6 +475,7 @@ jobs: config/scripts/ssh-relay-runtime-native-signing-payload.test.mjs ` config/scripts/ssh-relay-runtime-windows-authenticode-assessment.test.mjs ` config/scripts/ssh-relay-runtime-native-signing-stage.test.mjs ` + config/scripts/ssh-relay-runtime-native-signing-apply.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-native-signing-apply.mjs b/config/scripts/ssh-relay-runtime-native-signing-apply.mjs new file mode 100644 index 00000000000..ae061635548 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-native-signing-apply.mjs @@ -0,0 +1,206 @@ +import { constants } from 'node:fs' +import { chmod, copyFile, lstat, mkdir, realpath, rm } from 'node:fs/promises' +import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path' +import { isDeepStrictEqual } from 'node:util' + +import { assertSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' +import { verifySshRelayRuntimeNativeSigningReturn } from './ssh-relay-runtime-native-signing-payload.mjs' +import { buildSshRelayRuntimeNativeSigningSelection } from './ssh-relay-runtime-native-signing-selection.mjs' +import { verifyRuntimeTree } from './verify-ssh-relay-runtime.mjs' + +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.isSymbolicLink() || !metadata.isDirectory()) { + throw new Error(`Runtime native signing ${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 native signing apply requires an exclusive output root') +} + +export async function assertSshRelayRuntimeNativeSigningApplyRoots({ + sourceRuntimeRoot, + returnedRoot, + outputRuntimeRoot +}) { + const absoluteOutput = resolve(outputRuntimeRoot) + const [physicalSource, physicalReturned, physicalOutputParent] = await Promise.all([ + physicalDirectory(resolve(sourceRuntimeRoot), 'source root'), + physicalDirectory(resolve(returnedRoot), 'returned root'), + physicalDirectory(dirname(absoluteOutput), 'output parent') + ]) + const physicalOutput = resolve(physicalOutputParent, basename(absoluteOutput)) + const roots = [physicalSource, physicalReturned, physicalOutput] + for (let left = 0; left < roots.length; left += 1) { + for (let right = left + 1; right < roots.length; right += 1) { + if (containsPath(roots[left], roots[right]) || containsPath(roots[right], roots[left])) { + throw new Error('Runtime native signing apply roots must be physically disjoint') + } + } + } + await assertAbsent(physicalOutput) + return { physicalSource, physicalReturned, physicalOutput } +} + +function localPath(root, portablePath) { + return resolve(root, ...portablePath.split('/')) +} + +function assertIdentityBoundSelection(identity, selection) { + if ( + !selection || + typeof selection !== 'object' || + !Array.isArray(selection.signingFiles) || + !Array.isArray(selection.preservedUpstreamFiles) + ) { + throw new Error('Runtime native signing apply requires a complete selection') + } + const assessments = + identity.os === 'win32' + ? [ + ...selection.signingFiles.map((entry) => ({ + path: entry.path, + sourceSha256: entry.sourceSha256, + status: 'unsigned' + })), + ...selection.preservedUpstreamFiles.map((entry) => ({ + path: entry.path, + sourceSha256: entry.sourceSha256, + status: 'valid-upstream', + signerSubject: entry.signerSubject, + signerThumbprint: entry.signerThumbprint + })) + ] + : [] + const expected = buildSshRelayRuntimeNativeSigningSelection(identity, assessments) + if (!isDeepStrictEqual(selection, expected)) { + // Why: signing selections cross a job boundary and must remain bound to the authenticated tree. + throw new Error('Runtime native signing apply selection and identity disagree') + } +} + +function finalIdentity(identity, returnedFiles) { + const returned = new Map(returnedFiles.map((entry) => [entry.path, entry])) + const entries = identity.entries.map((entry) => { + const signed = returned.get(entry.path) + return signed + ? { ...entry, size: signed.signedSize, sha256: signed.signedSha256 } + : { ...entry } + }) + const files = entries.filter((entry) => entry.type === 'file') + const { + archive: _archive, + contentId: _contentId, + expandedSize: _size, + fileCount: _count, + ...base + } = identity + const candidate = { + ...base, + entries, + fileCount: files.length, + expandedSize: files.reduce((total, entry) => total + entry.size, 0) + } + return { ...candidate, contentId: computeSshRelayRuntimeContentId(candidate) } +} + +async function copyRuntimeTree({ + identity, + returnedFiles, + physicalSource, + physicalReturned, + physicalOutput, + copyFileImpl +}) { + const returnedPaths = new Set(returnedFiles.map((entry) => entry.path)) + const directories = identity.entries + .filter((entry) => entry.type === 'directory') + .sort((left, right) => left.path.split('/').length - right.path.split('/').length) + for (const entry of directories) { + const destination = localPath(physicalOutput, entry.path) + await mkdir(destination, { mode: entry.mode }) + if (process.platform !== 'win32') { + await chmod(destination, entry.mode) + } + } + for (const entry of identity.entries.filter((candidate) => candidate.type === 'file')) { + const sourceRoot = returnedPaths.has(entry.path) ? physicalReturned : physicalSource + const destination = localPath(physicalOutput, entry.path) + await copyFileImpl(localPath(sourceRoot, entry.path), destination, constants.COPYFILE_EXCL) + if (process.platform !== 'win32') { + await chmod(destination, entry.mode) + } + } +} + +export async function applySshRelayRuntimeNativeSigningReturn({ + sourceRuntimeRoot, + returnedRoot, + outputRuntimeRoot, + identity, + selection, + copyFileImpl = copyFile +}) { + assertSshRelayRuntimeClosureEntries(identity) + assertIdentityBoundSelection(identity, selection) + if (selection.signingFiles.length === 0) { + throw new Error('Runtime native signing apply selection has no signing files') + } + const roots = await assertSshRelayRuntimeNativeSigningApplyRoots({ + sourceRuntimeRoot, + returnedRoot, + outputRuntimeRoot + }) + await verifyRuntimeTree(roots.physicalSource, identity) + const returned = await verifySshRelayRuntimeNativeSigningReturn({ + returnedRoot: roots.physicalReturned, + selection + }) + const candidateIdentity = finalIdentity(identity, returned.returnedFiles) + if (candidateIdentity.contentId === identity.contentId) { + throw new Error('Runtime native signing apply did not change content identity') + } + assertSshRelayRuntimeClosureEntries(candidateIdentity) + + let outputCreated = false + try { + await mkdir(roots.physicalOutput) + outputCreated = true + await copyRuntimeTree({ + identity, + returnedFiles: returned.returnedFiles, + ...roots, + copyFileImpl + }) + await verifyRuntimeTree(roots.physicalOutput, candidateIdentity) + return { + tupleId: identity.tupleId, + sourceContentId: identity.contentId, + finalContentId: candidateIdentity.contentId, + returnedFiles: returned.returnedFiles, + identity: candidateIdentity + } + } catch (error) { + if (outputCreated) { + // Why: an incomplete post-sign tree must never survive as a launchable or archivable candidate. + await rm(roots.physicalOutput, { recursive: true, force: true }) + } + throw error + } +} diff --git a/config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs b/config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs new file mode 100644 index 00000000000..34e9700241a --- /dev/null +++ b/config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs @@ -0,0 +1,322 @@ +import { createHash } from 'node:crypto' +import { + appendFile, + copyFile, + cp, + lstat, + mkdir, + mkdtemp, + readFile, + rm, + writeFile +} from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + +import { describe, expect, it } from 'vitest' + +import { expectedSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' +import { + applySshRelayRuntimeNativeSigningReturn, + assertSshRelayRuntimeNativeSigningApplyRoots +} from './ssh-relay-runtime-native-signing-apply.mjs' +import { buildSshRelayRuntimeNativeSigningPlan } from './ssh-relay-runtime-native-signing-plan.mjs' +import { buildSshRelayRuntimeNativeSigningSelection } from './ssh-relay-runtime-native-signing-selection.mjs' +import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' + +function digest(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +function platformForTuple(tupleId) { + return tupleId.startsWith('linux-') ? 'linux' : tupleId.startsWith('darwin-') ? 'darwin' : 'win32' +} + +async function runtimeFixture(tupleId) { + const root = await mkdtemp(join(tmpdir(), 'ssh-relay-native-signing-apply-')) + const runtimeRoot = join(root, 'runtime') + await mkdir(runtimeRoot) + const entries = [] + for (const entry of expectedSshRelayRuntimeClosureEntries(tupleId)) { + if (entry.type === 'directory') { + await mkdir(join(runtimeRoot, ...entry.path.split('/')), { + recursive: true, + mode: entry.mode + }) + entries.push(entry) + continue + } + const bytes = Buffer.from(`fixture:${tupleId}:${entry.path}`) + const filePath = join(runtimeRoot, ...entry.path.split('/')) + await mkdir(dirname(filePath), { recursive: true }) + await writeFile(filePath, bytes, { mode: entry.mode }) + entries.push({ ...entry, size: bytes.length, sha256: digest(bytes) }) + } + const os = platformForTuple(tupleId) + const base = { + identitySchemaVersion: 1, + tupleId, + os, + architecture: tupleId.includes('arm64') ? 'arm64' : 'x64', + compatibility: sshRelayRuntimeCompatibility[tupleId], + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries + } + const files = entries.filter((entry) => entry.type === 'file') + return { + root, + runtimeRoot, + identity: { + ...base, + archive: { + fileName: `orca-relay-runtime-${tupleId}.fixture`, + size: 1, + sha256: digest('stale-unsigned-archive') + }, + contentId: computeSshRelayRuntimeContentId(base), + fileCount: files.length, + expandedSize: files.reduce((total, entry) => total + entry.size, 0) + } + } +} + +function selectionFor(identity) { + if (identity.os !== 'win32') { + return buildSshRelayRuntimeNativeSigningSelection(identity, []) + } + const preserved = new Set([ + 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe', + 'node_modules/node-pty/build/Release/conpty/conpty.dll' + ]) + const assessments = buildSshRelayRuntimeNativeSigningPlan(identity).signingCandidates.map( + (entry) => + preserved.has(entry.path) + ? { + path: entry.path, + sourceSha256: entry.sourceSha256, + status: 'valid-upstream', + signerSubject: 'CN=Microsoft Corporation', + signerThumbprint: 'D'.repeat(40) + } + : { path: entry.path, sourceSha256: entry.sourceSha256, status: 'unsigned' } + ) + return buildSshRelayRuntimeNativeSigningSelection(identity, assessments) +} + +async function returnedTree(fixture, selection) { + const root = join(fixture.root, 'returned') + for (const entry of selection.signingFiles) { + const source = join(fixture.runtimeRoot, ...entry.path.split('/')) + const destination = join(root, ...entry.path.split('/')) + await mkdir(dirname(destination), { recursive: true }) + await cp(source, destination) + await appendFile(destination, ':signed') + } + return root +} + +describe('SSH relay runtime native signing return application', () => { + it('applies only exact returned files into a new full runtime and derives final identity', async () => { + for (const tupleId of ['darwin-arm64', 'win32-x64']) { + const fixture = await runtimeFixture(tupleId) + try { + const selection = selectionFor(fixture.identity) + const returnedRoot = await returnedTree(fixture, selection) + const outputRuntimeRoot = join(fixture.root, 'final-runtime') + const originalIdentity = structuredClone(fixture.identity) + const nodePath = fixture.identity.os === 'win32' ? 'bin/node.exe' : 'bin/node' + const sourceNode = await readFile(join(fixture.runtimeRoot, ...nodePath.split('/'))) + const sourceSigningBytes = new Map() + for (const entry of selection.signingFiles) { + sourceSigningBytes.set( + entry.path, + await readFile(join(fixture.runtimeRoot, ...entry.path.split('/'))) + ) + } + + const result = await applySshRelayRuntimeNativeSigningReturn({ + sourceRuntimeRoot: fixture.runtimeRoot, + returnedRoot, + outputRuntimeRoot, + identity: fixture.identity, + selection + }) + + expect(fixture.identity).toEqual(originalIdentity) + expect(result.identity.contentId).not.toBe(fixture.identity.contentId) + expect(result.identity).not.toHaveProperty('archive') + expect(result.identity.fileCount).toBe(fixture.identity.fileCount) + expect(result.returnedFiles).toHaveLength(selection.signingFiles.length) + expect(await readFile(join(outputRuntimeRoot, ...nodePath.split('/')))).toEqual(sourceNode) + expect(result.identity.entries.find((entry) => entry.path === nodePath)).toEqual( + fixture.identity.entries.find((entry) => entry.path === nodePath) + ) + for (const returned of result.returnedFiles) { + const finalEntry = result.identity.entries.find((entry) => entry.path === returned.path) + expect(finalEntry).toEqual( + expect.objectContaining({ size: returned.signedSize, sha256: returned.signedSha256 }) + ) + expect(await readFile(join(outputRuntimeRoot, ...returned.path.split('/')))).toEqual( + await readFile(join(returnedRoot, ...returned.path.split('/'))) + ) + expect(await readFile(join(fixture.runtimeRoot, ...returned.path.split('/')))).toEqual( + sourceSigningBytes.get(returned.path) + ) + } + for (const preserved of selection.preservedUpstreamFiles) { + expect(await readFile(join(outputRuntimeRoot, ...preserved.path.split('/')))).toEqual( + await readFile(join(fixture.runtimeRoot, ...preserved.path.split('/'))) + ) + expect(result.identity.entries.find((entry) => entry.path === preserved.path)).toEqual( + fixture.identity.entries.find((entry) => entry.path === preserved.path) + ) + } + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + } + }) + + it('rejects existing, source-nested, returned-nested, or overlapping roots', async () => { + const fixture = await runtimeFixture('darwin-x64') + try { + const selection = selectionFor(fixture.identity) + const returnedRoot = await returnedTree(fixture, selection) + const existing = join(fixture.root, 'existing') + await mkdir(existing) + await expect( + assertSshRelayRuntimeNativeSigningApplyRoots({ + sourceRuntimeRoot: fixture.runtimeRoot, + returnedRoot, + outputRuntimeRoot: existing + }) + ).rejects.toThrow(/exclusive/i) + await expect( + assertSshRelayRuntimeNativeSigningApplyRoots({ + sourceRuntimeRoot: fixture.runtimeRoot, + returnedRoot, + outputRuntimeRoot: join(fixture.runtimeRoot, 'nested') + }) + ).rejects.toThrow(/disjoint/i) + await expect( + assertSshRelayRuntimeNativeSigningApplyRoots({ + sourceRuntimeRoot: fixture.runtimeRoot, + returnedRoot, + outputRuntimeRoot: join(returnedRoot, 'nested') + }) + ).rejects.toThrow(/disjoint/i) + await expect( + assertSshRelayRuntimeNativeSigningApplyRoots({ + sourceRuntimeRoot: fixture.runtimeRoot, + returnedRoot: fixture.runtimeRoot, + outputRuntimeRoot: join(fixture.root, 'output') + }) + ).rejects.toThrow(/disjoint/i) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('removes the complete output if returned bytes change after initial verification', async () => { + const fixture = await runtimeFixture('darwin-arm64') + try { + const selection = selectionFor(fixture.identity) + const returnedRoot = await returnedTree(fixture, selection) + const outputRuntimeRoot = join(fixture.root, 'final-runtime') + const first = selection.signingFiles[0] + const firstReturnedSuffix = join(...first.path.split('/')) + let raced = false + await expect( + applySshRelayRuntimeNativeSigningReturn({ + sourceRuntimeRoot: fixture.runtimeRoot, + returnedRoot, + outputRuntimeRoot, + identity: fixture.identity, + selection, + copyFileImpl: async (source, destination, mode) => { + if (!raced && source.endsWith(firstReturnedSuffix)) { + raced = true + await appendFile(source, ':raced') + } + await copyFile(source, destination, mode) + } + }) + ).rejects.toThrow(/integrity mismatch/i) + await expect(lstat(outputRuntimeRoot)).rejects.toMatchObject({ code: 'ENOENT' }) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('rejects Linux and a mismatched selection before creating output', async () => { + const fixture = await runtimeFixture('linux-x64-glibc') + try { + const outputRuntimeRoot = join(fixture.root, 'final-runtime') + await expect( + applySshRelayRuntimeNativeSigningReturn({ + sourceRuntimeRoot: fixture.runtimeRoot, + returnedRoot: join(fixture.root, 'missing-return'), + outputRuntimeRoot, + identity: fixture.identity, + selection: selectionFor(fixture.identity) + }) + ).rejects.toThrow(/no signing files/i) + await expect(lstat(outputRuntimeRoot)).rejects.toMatchObject({ code: 'ENOENT' }) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('rejects a selection whose authenticated source bytes disagree with the identity', async () => { + const fixture = await runtimeFixture('darwin-x64') + try { + const selection = selectionFor(fixture.identity) + selection.signingFiles[0].sourceSha256 = `sha256:${'f'.repeat(64)}` + const outputRuntimeRoot = join(fixture.root, 'final-runtime') + await expect( + applySshRelayRuntimeNativeSigningReturn({ + sourceRuntimeRoot: fixture.runtimeRoot, + returnedRoot: join(fixture.root, 'missing-return'), + outputRuntimeRoot, + identity: fixture.identity, + selection + }) + ).rejects.toThrow(/selection and identity disagree/i) + await expect(lstat(outputRuntimeRoot)).rejects.toMatchObject({ code: 'ENOENT' }) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('removes the output if unsigned source bytes change during the copy', async () => { + const fixture = await runtimeFixture('win32-x64') + try { + const selection = selectionFor(fixture.identity) + const returnedRoot = await returnedTree(fixture, selection) + const outputRuntimeRoot = join(fixture.root, 'final-runtime') + const nodeSuffix = join('bin', 'node.exe') + let raced = false + await expect( + applySshRelayRuntimeNativeSigningReturn({ + sourceRuntimeRoot: fixture.runtimeRoot, + returnedRoot, + outputRuntimeRoot, + identity: fixture.identity, + selection, + copyFileImpl: async (source, destination, mode) => { + if (!raced && source.endsWith(nodeSuffix)) { + raced = true + await appendFile(source, ':raced') + } + await copyFile(source, destination, mode) + } + }) + ).rejects.toThrow(/integrity mismatch/i) + await expect(lstat(outputRuntimeRoot)).rejects.toMatchObject({ code: 'ENOENT' }) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) +}) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index d90ce593555..2a93d082323 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -88,6 +88,7 @@ describe('SSH relay runtime artifact workflow', () => { source.match(/ssh-relay-runtime-windows-authenticode-assessment\.test\.mjs/g) ).toHaveLength(4) expect(source.match(/ssh-relay-runtime-native-signing-stage\.test\.mjs/g)).toHaveLength(4) + expect(source.match(/ssh-relay-runtime-native-signing-apply\.test\.mjs/g)).toHaveLength(4) expect( source.match(/node --check config\/scripts\/ssh-relay-runtime-native-signing-plan\.mjs/g) ).toHaveLength(2) @@ -105,6 +106,9 @@ describe('SSH relay runtime artifact workflow', () => { expect( source.match(/node --check config\/scripts\/ssh-relay-runtime-native-signing-stage\.mjs/g) ).toHaveLength(2) + expect( + source.match(/node --check config\/scripts\/ssh-relay-runtime-native-signing-apply\.mjs/g) + ).toHaveLength(2) expect( source.match(/node --check config\/scripts\/ssh-relay-runtime-closure\.mjs/g) ).toHaveLength(2) From a0e9ea91773287bd3779f6fb3c800aa0f5f1be01 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 22:55:22 -0400 Subject: [PATCH 093/267] docs(ssh): record signing return apply proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 4 +- ...github-release-implementation-checklist.md | 96 ++++++++++++++++++- 2 files changed, 98 insertions(+), 2 deletions(-) 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 d9164dd0aa1..a9ec81eaa6d 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 @@ -37,7 +37,9 @@ complete a box. - Proven locally and in exact-head run [29384042509](https://github.com/stablyai/orca/actions/runs/29384042509): target-native pre-sign assessment and real first-build candidate staging without credentials. - - Active next: exact returned-file application and final post-sign runtime identity contracts. + - Proven locally at `3eeea7bdb` under `E-M3-NATIVE-SIGNING-APPLY-LOCAL-001`: exact returned-file + application into a fresh full runtime and final post-sign content identity contracts. + - Active next: exact-head Node 24 CI proof on all six native jobs; no signing credential is used. - 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. 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 2a530f99a94..fc4f1f73e80 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps 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** — target-native pre-sign assessment, real first-build candidate staging, unpublished JSON evidence, and cleanup are implemented at `1a79e4921` and pass locally plus all six exact-head native jobs under E-M3-NATIVE-ASSESSMENT-LOCAL-001 and E-M3-NATIVE-ASSESSMENT-CI-001. The next safe credential-free package is exact returned-file application plus final post-sign runtime identity/closure contracts; real Apple/SignPath calls and native trust remain separately gated. Nothing is published or enabled, and legacy remains the production default.
      +Session checkpoint: **In progress — 2026-07-14, Codex implementation owner** — exact returned-file application plus final post-sign runtime identity/closure contracts are implemented at `3eeea7bdb` and locally green under E-M3-NATIVE-SIGNING-APPLY-LOCAL-001. Exact-head Node 24 execution on all six native jobs is the active gate; real Apple/SignPath calls, returned production signatures, 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) @@ -8129,6 +8129,100 @@ diff --check`. - Follow-up: keep every tuple disabled; implement exact returned-file application and final runtime identity/closure contracts before any credentialed signing job is allowed to feed aggregation. +### E-M3-NATIVE-SIGNING-APPLY-LOCAL-RED-001 — Missing return-application module fails the focused contract + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: pre-implementation head `6b93976909f9df6373414ecc65daf6e3eaff29b9`. +- Command: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs + ``` + +- Result: expected RED. The purpose-named test file failed to load because + `ssh-relay-runtime-native-signing-apply.mjs` did not exist: 1 failed file/0 tests in 525 ms. No + runtime, workflow, signing, publication, or production behavior changed. +- Oracle proved: the existing payload verifier authenticated the returned file closure but did not + yet construct and re-authenticate a complete post-sign runtime or derive its final content ID. +- Does not prove: any implementation behavior, native runner execution, signed bytes, native trust, + release aggregation, SSH behavior, or an enabled tuple. +- Correction: add a purpose-named apply module that keeps all roots physically disjoint, copies into + an exclusive candidate, substitutes only the exact verified returned files, derives the new full + identity, and deletes any candidate that fails final-tree verification. + +### E-M3-NATIVE-SIGNING-APPLY-LOCAL-001 — Exact returned-file application and final identity contracts pass locally + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: implementation commit `3eeea7bdb8b10825416d7f9bac03d5a820a997f5`, based on exact + native-assessment evidence head `6b93976909f9df6373414ecc65daf6e3eaff29b9`. +- Runner/remote/network: local macOS 26.2 build 25C56 arm64, Node v26.0.0 and pnpm 10.24.0; no + remote, network, credential, signing service, native-trust environment, artifact publication, or + production path was used. +- Commands: + + ```sh + node --check config/scripts/ssh-relay-runtime-native-signing-apply.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-plan.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-selection.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-payload.test.mjs \ + config/scripts/ssh-relay-runtime-windows-authenticode-assessment.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-stage.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs + 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 + pnpm exec oxfmt --check \ + .github/workflows/ssh-relay-runtime-artifacts.yml \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-apply.mjs \ + config/scripts/ssh-relay-runtime-native-signing-apply.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 complete signing-contract family passed 7 files/48 tests in 721 ms; the final + apply-plus-workflow check passed 2 files/12 tests in 319 ms; and the complete purpose-named SSH + relay script suite passed 28 files/145 tests in 3.79 seconds. Both direct syntax checks, typecheck, + full lint/reliability gates, max-lines (355 grandfathered suppressions and no new bypass), + bundled-skill verification, localization catalog/coverage, focused formatting, and + `git diff --check` passed. Lint emitted only existing unrelated warnings; local Node 26 emitted the + expected repository Node-24 engine warning. An intermediate focused run exposed a macOS `/var` + versus `/private/var` test-oracle alias and an intermediate lint run rejected three missing braces; + both were corrected without weakening assertions or adding a disable, and the complete gates were + rerun green. +- Apply oracle: source, returned, and exclusive output roots must be physically disjoint real + locations. The complete unsigned source tree is authenticated first; the selection is + independently reconstructed from and deeply matched to that identity; and the exact bounded + returned closure is reverified. The implementation creates a fresh full-runtime candidate, + substitutes only the returned signer files, preserves official Node and valid upstream Windows + bytes exactly, drops stale unsigned-archive metadata, recalculates file sizes, hashes, expanded + size, file count, and content ID, then re-verifies the complete final tree before returning it. + It never mutates the unsigned source and removes the whole output on partial copy, source race, + returned-file race, mode failure, or final integrity failure. +- Root/race oracle: existing, nested, overlapping, or physically redirected roots reject before an + output is owned. The output is cleaned only after this invocation wins its exclusive creation, so + a racing pre-existing path is never removed. Tests force returned and unsigned-source mutation + after initial verification and require final-tree failure plus complete output cleanup. +- Workflow oracle: POSIX and Windows native contract jobs each syntax-check the source and test and + explicitly execute the suite. The workflow lock requires four test-path occurrences and two + source syntax-check occurrences. No fake signer output is produced and no post-sign artifact is + built until a real protected signing job returns bytes. +- Does not prove: parsing or execution under Node 24/GitHub Actions, real Apple or SignPath returned + bytes, native signature/trust policy, Gatekeeper/quarantine/notarized-app provenance, + Authenticode/Defender/WDAC policy, exact missing oldest snapshots, release aggregation, SSH + transfer/install, packaged desktop use, fallback/performance, or an enabled tuple. +- Follow-up: commit this evidence separately, push both commits, and require exact-head POSIX and + Windows logs to show the apply suite executing under Node 24.18.0 on all six native jobs while all + prior artifact, smoke, equality, baseline, PR-check, and Golden controls retain their outcomes. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 2d4eab2c592adb0fd6396d6312394dddc6be6bc2 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 23:10:51 -0400 Subject: [PATCH 094/267] docs(ssh): record signing return apply CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 6 +- ...github-release-implementation-checklist.md | 61 ++++++++++++++++++- 2 files changed, 64 insertions(+), 3 deletions(-) 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 a9ec81eaa6d..1a5ef6af10d 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 @@ -37,9 +37,11 @@ complete a box. - Proven locally and in exact-head run [29384042509](https://github.com/stablyai/orca/actions/runs/29384042509): target-native pre-sign assessment and real first-build candidate staging without credentials. - - Proven locally at `3eeea7bdb` under `E-M3-NATIVE-SIGNING-APPLY-LOCAL-001`: exact returned-file + - Proven locally and in exact-head run + [29385274738](https://github.com/stablyai/orca/actions/runs/29385274738): exact returned-file application into a fresh full runtime and final post-sign content identity contracts. - - Active next: exact-head Node 24 CI proof on all six native jobs; no signing credential is used. + - Active next: remaining credential-free native-trust/release failure contracts; real signing and + missing oldest-floor snapshots remain externally 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. 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 fc4f1f73e80..a77702e0b95 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps 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** — exact returned-file application plus final post-sign runtime identity/closure contracts are implemented at `3eeea7bdb` and locally green under E-M3-NATIVE-SIGNING-APPLY-LOCAL-001. Exact-head Node 24 execution on all six native jobs is the active gate; real Apple/SignPath calls, returned production signatures, and native trust remain separately gated. Nothing is published or enabled, and legacy remains the production default.
      +Session checkpoint: **In progress — 2026-07-14, Codex implementation owner** — exact returned-file application plus final post-sign runtime identity/closure contracts are proven locally and on all six native jobs under E-M3-NATIVE-SIGNING-APPLY-LOCAL-001 and E-M3-NATIVE-SIGNING-APPLY-CI-001. The next safe package is remaining credential-free native-trust/release failure contracts; real Apple/SignPath calls, returned production signatures, 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) @@ -8223,6 +8223,65 @@ diff --check`. Windows logs to show the apply suite executing under Node 24.18.0 on all six native jobs while all prior artifact, smoke, equality, baseline, PR-check, and Golden controls retain their outcomes. +### E-M3-NATIVE-SIGNING-APPLY-CI-001 — Return application executes on all six native jobs + +- Date: 2026-07-14 (run timestamps 2026-07-15 UTC) +- Owner: Codex implementation owner +- Source/run: exact draft-PR head `a0e9ea91773287bd3779f6fb3c800aa0f5f1be01`, containing apply + implementation `3eeea7bdb8b10825416d7f9bac03d5a820a997f5` and local evidence + `a0e9ea91773287bd3779f6fb3c800aa0f5f1be01`; Actions run + [29385274738](https://github.com/stablyai/orca/actions/runs/29385274738). +- Commands: + + ```sh + gh run view 29385274738 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh run view 29385274738 --repo stablyai/orca --job 87257092104 --log | \ + awk -F '\t' '$2 == "Run runtime artifact contract tests"' | \ + rg 'native-signing-apply|Test Files|Tests|Duration' + gh run view 29385274738 --repo stablyai/orca --job 87257092096 --log | \ + awk -F '\t' '$2 == "Run runtime artifact contract tests"' | \ + rg 'native-signing-apply|Test Files|Tests|Duration' + gh run view 29385274738 --repo stablyai/orca --job 87258165496 --log | \ + rg 'Operating System|OsBuildNumber|26100|26200|declared Windows floor' + gh run view 29385274716 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh run view 29385274721 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + ``` + +- Contract result: PASS on Linux x64/arm64, macOS x64/arm64, and Windows x64/arm64. Every native + build job reports its purpose-named contract step successful under exact Node 24.18.0. The macOS + arm64 POSIX log records both apply syntax checks, the apply suite passing 6/6 in 245 ms, and the + aggregate 27 files/141 tests in 3.58 seconds. The Windows x64 PowerShell log records both syntax + checks, the apply suite passing 6/6 in 1.638 seconds, and the aggregate 28 files/138 passed plus + seven declared platform skips out of 145 in 7.13 seconds. +- Native build/regression controls: all six double-build, smoke, exact-equality, metadata, + assessment/stage, cleanup, and unpublished-upload jobs passed. Durations were Linux x64 3m51s, + Linux arm64 4m54s, macOS x64 5m58s, macOS arm64 2m31s, Windows x64 5m22s, and Windows arm64 + 8m37s. Digest-pinned Linux x64/arm64 glibc 2.28/libstdc++ 6.0.25 supplements passed in 49s/58s. + Windows x64 passed exact build 20348 in 1m39s. +- Expected aggregate status: the artifact workflow concluded failure only because the Windows arm64 + hosted image identified itself as Windows 11 build 10.0.26200 and rejected the exact declared + build-26100 floor after 3m47s. The report records platform and architecture checks true with + `osBuild: false`, preserving E-M3-WINDOWS-ARM64-BASELINE-CI-RED-001 without weakening its oracle. +- Other exact-head controls: PR Checks + [29385274716](https://github.com/stablyai/orca/actions/runs/29385274716) passed in 13m50s after lint, + typecheck, Git 2.25 compatibility, full tests, unpacked-app build, and packaged-CLI smoke. Golden + E2E [29385274721](https://github.com/stablyai/orca/actions/runs/29385274721) passed Linux/macOS in + 4m42s/6m54s. +- Oracle proved: both native shell families parse and execute the exact post-sign application + contract on every target-native runner while all existing artifact, bundled-Node, PTY/watcher, + clean-build equality, oldest-userland, packaging, and E2E controls retain their prior outcomes. +- Does not prove: real Apple/SignPath returned bytes, final native trust, Gatekeeper/quarantine or + notarized-app provenance, Authenticode/Defender/WDAC policy, exact missing oldest snapshots, + release aggregation, SSH transfer/install, packaged desktop use, fallback/performance, or an + enabled tuple. +- Follow-up: keep every tuple disabled and all signing credentials disconnected. Continue with + credential-free native-trust and release-DAG failure contracts; require separately provisioned + protected signing services and exact-floor runners before any native-signing/trust checkbox or + aggregate output can pass. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From debb13f3011dc750e54826a4820f109e7d80f959 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 23:21:00 -0400 Subject: [PATCH 095/267] build(ssh): verify macOS runtime signatures Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 8 + ...y-runtime-macos-signature-verification.mjs | 273 ++++++++++++++ ...time-macos-signature-verification.test.mjs | 355 ++++++++++++++++++ ...ssh-relay-runtime-native-signing-apply.mjs | 38 +- ...h-relay-runtime-native-signing-payload.mjs | 4 +- ...relay-runtime-native-signing-selection.mjs | 35 ++ .../ssh-relay-runtime-workflow.test.mjs | 8 + 7 files changed, 683 insertions(+), 38 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-macos-signature-verification.mjs create mode 100644 config/scripts/ssh-relay-runtime-macos-signature-verification.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 79c0088adfa..cbb23eac16b 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -137,6 +137,8 @@ jobs: node --check config/scripts/ssh-relay-runtime-native-signing-stage.test.mjs node --check config/scripts/ssh-relay-runtime-native-signing-apply.mjs node --check config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs + node --check config/scripts/ssh-relay-runtime-macos-signature-verification.mjs + node --check config/scripts/ssh-relay-runtime-macos-signature-verification.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 \ @@ -155,6 +157,7 @@ jobs: config/scripts/ssh-relay-runtime-windows-authenticode-assessment.test.mjs \ config/scripts/ssh-relay-runtime-native-signing-stage.test.mjs \ config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs \ + config/scripts/ssh-relay-runtime-macos-signature-verification.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 \ @@ -458,6 +461,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime native signing apply script syntax check failed' } node --check config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime native signing apply test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-macos-signature-verification.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime macOS signature verification script syntax check failed' } + node --check config/scripts/ssh-relay-runtime-macos-signature-verification.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime macOS signature verification 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 ` @@ -476,6 +483,7 @@ jobs: config/scripts/ssh-relay-runtime-windows-authenticode-assessment.test.mjs ` config/scripts/ssh-relay-runtime-native-signing-stage.test.mjs ` config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs ` + config/scripts/ssh-relay-runtime-macos-signature-verification.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-macos-signature-verification.mjs b/config/scripts/ssh-relay-runtime-macos-signature-verification.mjs new file mode 100644 index 00000000000..b10dda10503 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-macos-signature-verification.mjs @@ -0,0 +1,273 @@ +import { spawnSync } from 'node:child_process' +import { createHash } from 'node:crypto' +import { createReadStream } from 'node:fs' +import { lstat, realpath } from 'node:fs/promises' +import { resolve } from 'node:path' +import { isDeepStrictEqual } from 'node:util' + +import { assertSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' +import { + MAX_RETURNED_PAYLOAD_BYTES, + MAX_SIGNED_FILE_GROWTH_BYTES +} from './ssh-relay-runtime-native-signing-payload.mjs' +import { assertSshRelayRuntimeNativeSigningSelection } from './ssh-relay-runtime-native-signing-selection.mjs' +import { verifyRuntimeTree } from './verify-ssh-relay-runtime.mjs' + +const CODESIGN_PATH = '/usr/bin/codesign' +const CODESIGN_TIMEOUT_MS = 30_000 +const CODESIGN_OUTPUT_BYTES = 64 * 1024 +const NODE_TEAM_IDENTIFIER = 'HX7739G8FX' +const NODE_AUTHORITY = `Developer ID Application: Node.js Foundation (${NODE_TEAM_IDENTIFIER})` +const APPLE_CHAIN = ['Developer ID Certification Authority', 'Apple Root CA'] +const TEAM_IDENTIFIER_PATTERN = /^[A-Z0-9]{10}$/ + +function localPath(root, portablePath) { + return resolve(root, ...portablePath.split('/')) +} + +async function sha256File(path) { + const hash = createHash('sha256') + for await (const chunk of createReadStream(path)) { + hash.update(chunk) + } + return `sha256:${hash.digest('hex')}` +} + +function outputBytes(result) { + return Buffer.byteLength(result?.stdout ?? '') + Buffer.byteLength(result?.stderr ?? '') +} + +function runCodesign(path, args, spawnSyncImpl) { + const result = spawnSyncImpl(CODESIGN_PATH, [...args, path], { + encoding: 'utf8', + maxBuffer: CODESIGN_OUTPUT_BYTES, + timeout: CODESIGN_TIMEOUT_MS, + windowsHide: true + }) + if (result?.error) { + throw new Error(`Runtime macOS codesign probe failed: ${result.error.message}`) + } + if (outputBytes(result) > CODESIGN_OUTPUT_BYTES) { + throw new Error('Runtime macOS codesign probe exceeded its output bound') + } + if (result?.status !== 0) { + throw new Error( + `Runtime macOS codesign probe failed with exit code ${result?.status ?? ''}` + ) + } + return result +} + +export function parseSshRelayRuntimeMacosCodeSignature(stderr) { + if (typeof stderr !== 'string' || stderr.trim() === '') { + throw new Error('Runtime macOS codesign display output is empty') + } + const fields = new Map() + const authorities = [] + for (const line of stderr.split(/\r?\n/u)) { + const separator = line.indexOf('=') + if (separator <= 0) { + continue + } + const key = line.slice(0, separator) + const value = line.slice(separator + 1) + if (key === 'Authority') { + authorities.push(value) + } else if (fields.has(key)) { + throw new Error(`Runtime macOS codesign display repeats field: ${key}`) + } else { + fields.set(key, value) + } + } + if ( + authorities.length !== 3 || + new Set(authorities).size !== authorities.length || + !isDeepStrictEqual(authorities.slice(1), APPLE_CHAIN) + ) { + throw new Error('Runtime macOS codesign has an unexpected Developer ID authority chain') + } + const teamIdentifier = fields.get('TeamIdentifier') + const format = fields.get('Format') + if ( + fields.get('Signature') === 'adhoc' || + !TEAM_IDENTIFIER_PATTERN.test(teamIdentifier ?? '') || + typeof format !== 'string' || + !format.startsWith('Mach-O') + ) { + throw new Error('Runtime macOS codesign is not a Developer ID signature') + } + return { + authority: authorities[0], + authorities, + format, + teamIdentifier + } +} + +function assertSignerPolicy(signature, signerKind, expectedOrcaTeamIdentifier) { + if (signerKind === 'official-node') { + if ( + signature.teamIdentifier !== NODE_TEAM_IDENTIFIER || + signature.authority !== NODE_AUTHORITY + ) { + throw new Error('Runtime macOS signature violates official Node signer policy') + } + return + } + const expectedAuthoritySuffix = ` (${expectedOrcaTeamIdentifier})` + if ( + signature.teamIdentifier !== expectedOrcaTeamIdentifier || + !signature.authority.startsWith('Developer ID Application: ') || + !signature.authority.endsWith(expectedAuthoritySuffix) + ) { + throw new Error('Runtime macOS signature violates Orca signer policy') + } +} + +function assertIdentityTransition(sourceIdentity, finalIdentity, selection) { + assertSshRelayRuntimeClosureEntries(finalIdentity) + if ( + computeSshRelayRuntimeContentId(sourceIdentity) !== sourceIdentity.contentId || + Object.hasOwn(finalIdentity, 'archive') || + finalIdentity.tupleId !== sourceIdentity.tupleId || + finalIdentity.os !== 'darwin' || + finalIdentity.contentId === sourceIdentity.contentId + ) { + throw new Error('Runtime macOS final identity does not match its unsigned source') + } + const signingPaths = new Set(selection.signingFiles.map((entry) => entry.path)) + const finalEntries = new Map(finalIdentity.entries.map((entry) => [entry.path, entry])) + let returnedSize = 0 + for (const sourceEntry of sourceIdentity.entries) { + const finalEntry = finalEntries.get(sourceEntry.path) + if (!finalEntry) { + throw new Error(`Runtime macOS final identity is missing entry: ${sourceEntry.path}`) + } + if (signingPaths.has(sourceEntry.path)) { + if ( + sourceEntry.type !== 'file' || + finalEntry.type !== 'file' || + finalEntry.sha256 === sourceEntry.sha256 || + finalEntry.size <= 0 || + finalEntry.size > sourceEntry.size + MAX_SIGNED_FILE_GROWTH_BYTES || + finalEntry.path !== sourceEntry.path || + finalEntry.role !== sourceEntry.role || + finalEntry.mode !== sourceEntry.mode + ) { + throw new Error(`Runtime macOS signed identity transition is invalid: ${sourceEntry.path}`) + } + returnedSize += finalEntry.size + } else if (!isDeepStrictEqual(finalEntry, sourceEntry)) { + throw new Error(`Runtime macOS unsigned identity entry changed: ${sourceEntry.path}`) + } + finalEntries.delete(sourceEntry.path) + } + if (finalEntries.size !== 0) { + throw new Error( + `Runtime macOS final identity has an extra entry: ${finalEntries.keys().next().value}` + ) + } + if (returnedSize > MAX_RETURNED_PAYLOAD_BYTES) { + throw new Error('Runtime macOS signed identity exceeds the returned payload size bound') + } + const files = finalIdentity.entries.filter((entry) => entry.type === 'file') + const expandedSize = files.reduce((total, entry) => total + entry.size, 0) + if (finalIdentity.fileCount !== files.length || finalIdentity.expandedSize !== expandedSize) { + throw new Error('Runtime macOS final identity totals are inconsistent') + } +} + +async function verifySignedFile({ + path, + entry, + signerKind, + expectedOrcaTeamIdentifier, + spawnSyncImpl +}) { + const metadata = await lstat(path) + if (metadata.isSymbolicLink() || !metadata.isFile()) { + throw new Error(`Runtime macOS signature target is not a regular file: ${entry.path}`) + } + if ((await sha256File(path)) !== entry.sha256) { + throw new Error(`Runtime macOS signature target has wrong authenticated hash: ${entry.path}`) + } + runCodesign(path, ['--verify', '--strict', '--verbose=4'], spawnSyncImpl) + const display = runCodesign(path, ['--display', '--verbose=4'], spawnSyncImpl) + const signature = parseSshRelayRuntimeMacosCodeSignature(display.stderr) + assertSignerPolicy(signature, signerKind, expectedOrcaTeamIdentifier) + if ((await sha256File(path)) !== entry.sha256) { + throw new Error(`Runtime macOS file changed during signature verification: ${entry.path}`) + } + return { + path: entry.path, + role: entry.role, + sha256: entry.sha256, + signerKind, + authority: signature.authority, + teamIdentifier: signature.teamIdentifier + } +} + +export async function verifySshRelayRuntimeMacosSignatures({ + runtimeRoot, + sourceIdentity, + finalIdentity, + selection, + expectedOrcaTeamIdentifier, + platform = process.platform, + spawnSyncImpl = spawnSync +}) { + if (platform !== 'darwin') { + throw new Error('Runtime macOS signature verification requires macOS') + } + if (!TEAM_IDENTIFIER_PATTERN.test(expectedOrcaTeamIdentifier ?? '')) { + throw new Error('Runtime macOS signature verification requires an exact Orca team identifier') + } + assertSshRelayRuntimeNativeSigningSelection(sourceIdentity, selection) + assertIdentityTransition(sourceIdentity, finalIdentity, selection) + const rootMetadata = await lstat(resolve(runtimeRoot)) + if (rootMetadata.isSymbolicLink() || !rootMetadata.isDirectory()) { + throw new Error('Runtime macOS signature verification requires a real runtime root') + } + const physicalRoot = await realpath(resolve(runtimeRoot)) + // Why: native code is probed only after every byte in the final runtime matches its new identity. + await verifyRuntimeTree(physicalRoot, finalIdentity) + + const finalFiles = new Map( + finalIdentity.entries + .filter((entry) => entry.type === 'file') + .map((entry) => [entry.path, entry]) + ) + const targets = [ + ...selection.immutableVendorFiles.map((entry) => ({ + entry: finalFiles.get(entry.path), + signerKind: 'official-node' + })), + ...selection.signingFiles.map((entry) => ({ + entry: finalFiles.get(entry.path), + signerKind: 'orca-built' + })) + ] + const verifiedFiles = [] + for (const target of targets) { + if (!target.entry) { + throw new Error('Runtime macOS signature target is missing from the final identity') + } + verifiedFiles.push( + await verifySignedFile({ + path: localPath(physicalRoot, target.entry.path), + entry: target.entry, + signerKind: target.signerKind, + expectedOrcaTeamIdentifier, + spawnSyncImpl + }) + ) + } + return { + tupleId: finalIdentity.tupleId, + sourceContentId: sourceIdentity.contentId, + finalContentId: finalIdentity.contentId, + verifiedFiles + } +} diff --git a/config/scripts/ssh-relay-runtime-macos-signature-verification.test.mjs b/config/scripts/ssh-relay-runtime-macos-signature-verification.test.mjs new file mode 100644 index 00000000000..d3903158359 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-macos-signature-verification.test.mjs @@ -0,0 +1,355 @@ +import { createHash } from 'node:crypto' +import { appendFileSync } from 'node:fs' +import { appendFile, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + +import { describe, expect, it } from 'vitest' + +import { expectedSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' +import { applySshRelayRuntimeNativeSigningReturn } from './ssh-relay-runtime-native-signing-apply.mjs' +import { buildSshRelayRuntimeNativeSigningSelection } from './ssh-relay-runtime-native-signing-selection.mjs' +import { + parseSshRelayRuntimeMacosCodeSignature, + verifySshRelayRuntimeMacosSignatures +} from './ssh-relay-runtime-macos-signature-verification.mjs' +import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' + +const ORCA_TEAM = 'ABCDE12345' +const NODE_TEAM = 'HX7739G8FX' + +function digest(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +function codeSignature(authority, teamIdentifier) { + return [ + 'Executable=/tmp/runtime/native', + 'Format=Mach-O thin (arm64)', + `Authority=${authority}`, + 'Authority=Developer ID Certification Authority', + 'Authority=Apple Root CA', + `TeamIdentifier=${teamIdentifier}`, + 'Signature size=9000' + ].join('\n') +} + +async function runtimeFixture() { + const root = await mkdtemp(join(tmpdir(), 'ssh-relay-macos-signature-')) + const runtimeRoot = join(root, 'runtime') + await mkdir(runtimeRoot) + const entries = [] + for (const entry of expectedSshRelayRuntimeClosureEntries('darwin-arm64')) { + if (entry.type === 'directory') { + await mkdir(join(runtimeRoot, ...entry.path.split('/')), { + recursive: true, + mode: entry.mode + }) + entries.push(entry) + continue + } + const bytes = Buffer.from(`fixture:${entry.path}`) + const path = join(runtimeRoot, ...entry.path.split('/')) + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, bytes, { mode: entry.mode }) + entries.push({ ...entry, size: bytes.length, sha256: digest(bytes) }) + } + const base = { + identitySchemaVersion: 1, + tupleId: 'darwin-arm64', + os: 'darwin', + architecture: 'arm64', + compatibility: sshRelayRuntimeCompatibility['darwin-arm64'], + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries + } + const files = entries.filter((entry) => entry.type === 'file') + return { + root, + runtimeRoot, + identity: { + ...base, + contentId: computeSshRelayRuntimeContentId(base), + fileCount: files.length, + expandedSize: files.reduce((total, entry) => total + entry.size, 0) + } + } +} + +async function signedFixture() { + const fixture = await runtimeFixture() + const selection = buildSshRelayRuntimeNativeSigningSelection(fixture.identity, []) + const returnedRoot = join(fixture.root, 'returned') + for (const entry of selection.signingFiles) { + const path = join(returnedRoot, ...entry.path.split('/')) + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, `signed:${entry.path}`, { mode: 0o755 }) + } + const finalRuntimeRoot = join(fixture.root, 'final-runtime') + const applied = await applySshRelayRuntimeNativeSigningReturn({ + sourceRuntimeRoot: fixture.runtimeRoot, + returnedRoot, + outputRuntimeRoot: finalRuntimeRoot, + identity: fixture.identity, + selection + }) + return { ...fixture, finalRuntimeRoot, selection, finalIdentity: applied.identity } +} + +function successfulCodesign(calls) { + return (command, args, options) => { + calls.push({ command, args, options }) + if (args[0] === '--verify') { + return { status: 0, stdout: '', stderr: '' } + } + const node = args.at(-1).endsWith(join('bin', 'node')) + return { + status: 0, + stdout: '', + stderr: node + ? codeSignature(`Developer ID Application: Node.js Foundation (${NODE_TEAM})`, NODE_TEAM) + : codeSignature(`Developer ID Application: Orca Test (${ORCA_TEAM})`, ORCA_TEAM) + } + } +} + +describe('SSH relay runtime macOS signature verification', () => { + it('verifies the complete final tree before exact Node and Orca Developer ID policy', async () => { + const fixture = await signedFixture() + const calls = [] + try { + const report = await verifySshRelayRuntimeMacosSignatures({ + runtimeRoot: fixture.finalRuntimeRoot, + sourceIdentity: fixture.identity, + finalIdentity: fixture.finalIdentity, + selection: fixture.selection, + expectedOrcaTeamIdentifier: ORCA_TEAM, + platform: 'darwin', + spawnSyncImpl: successfulCodesign(calls) + }) + + expect(report.tupleId).toBe('darwin-arm64') + expect(report.verifiedFiles).toHaveLength(4) + expect(report.verifiedFiles.find((entry) => entry.role === 'node')).toEqual( + expect.objectContaining({ teamIdentifier: NODE_TEAM, signerKind: 'official-node' }) + ) + expect(calls).toHaveLength(8) + expect(calls.every((call) => call.command === '/usr/bin/codesign')).toBe(true) + expect(calls.every((call) => call.options.timeout === 30_000)).toBe(true) + expect(calls.every((call) => call.options.maxBuffer === 64 * 1024)).toBe(true) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('rejects an unexpected Node authority or Orca team', async () => { + const fixture = await signedFixture() + try { + const wrongNode = successfulCodesign([]) + await expect( + verifySshRelayRuntimeMacosSignatures({ + runtimeRoot: fixture.finalRuntimeRoot, + sourceIdentity: fixture.identity, + finalIdentity: fixture.finalIdentity, + selection: fixture.selection, + expectedOrcaTeamIdentifier: ORCA_TEAM, + platform: 'darwin', + spawnSyncImpl: (command, args, options) => { + const result = wrongNode(command, args, options) + return args[0] === '--display' && args.at(-1).endsWith(join('bin', 'node')) + ? { + ...result, + stderr: codeSignature( + 'Developer ID Application: Other (BADTEAM123)', + 'BADTEAM123' + ) + } + : result + } + }) + ).rejects.toThrow(/official Node signer policy/i) + + await expect( + verifySshRelayRuntimeMacosSignatures({ + runtimeRoot: fixture.finalRuntimeRoot, + sourceIdentity: fixture.identity, + finalIdentity: fixture.finalIdentity, + selection: fixture.selection, + expectedOrcaTeamIdentifier: 'WRONG12345', + platform: 'darwin', + spawnSyncImpl: successfulCodesign([]) + }) + ).rejects.toThrow(/Orca signer policy/i) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('rejects malformed display output, ad-hoc signatures, and strict verification failure', () => { + expect(() => parseSshRelayRuntimeMacosCodeSignature('')).toThrow(/empty/i) + expect(() => + parseSshRelayRuntimeMacosCodeSignature('Authority=A\nAuthority=A\nTeamIdentifier=T') + ).toThrow(/authority chain/i) + expect(() => + parseSshRelayRuntimeMacosCodeSignature('Signature=adhoc\nTeamIdentifier=not set') + ).toThrow(/Developer ID/i) + }) + + it('fails closed on codesign errors, nonzero status, or excessive output', async () => { + const fixture = await signedFixture() + try { + const common = { + runtimeRoot: fixture.finalRuntimeRoot, + sourceIdentity: fixture.identity, + finalIdentity: fixture.finalIdentity, + selection: fixture.selection, + expectedOrcaTeamIdentifier: ORCA_TEAM, + platform: 'darwin' + } + await expect( + verifySshRelayRuntimeMacosSignatures({ + ...common, + spawnSyncImpl: () => ({ error: new Error('timed out') }) + }) + ).rejects.toThrow(/timed out/i) + await expect( + verifySshRelayRuntimeMacosSignatures({ + ...common, + spawnSyncImpl: () => ({ status: 9, stdout: '', stderr: '' }) + }) + ).rejects.toThrow(/exit code 9/i) + await expect( + verifySshRelayRuntimeMacosSignatures({ + ...common, + spawnSyncImpl: () => ({ status: 0, stdout: 'x'.repeat(64 * 1024 + 1), stderr: '' }) + }) + ).rejects.toThrow(/output bound/i) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('authenticates the tree before spawning and rejects mutation during native probes', async () => { + const fixture = await signedFixture() + let calls = 0 + try { + await appendFile(join(fixture.finalRuntimeRoot, 'relay.js'), ':mutated') + await expect( + verifySshRelayRuntimeMacosSignatures({ + runtimeRoot: fixture.finalRuntimeRoot, + sourceIdentity: fixture.identity, + finalIdentity: fixture.finalIdentity, + selection: fixture.selection, + expectedOrcaTeamIdentifier: ORCA_TEAM, + platform: 'darwin', + spawnSyncImpl: () => { + calls += 1 + return { status: 0, stdout: '', stderr: '' } + } + }) + ).rejects.toThrow(/integrity mismatch/i) + expect(calls).toBe(0) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + + const raced = await signedFixture() + try { + const spawn = successfulCodesign([]) + let mutated = false + await expect( + verifySshRelayRuntimeMacosSignatures({ + runtimeRoot: raced.finalRuntimeRoot, + sourceIdentity: raced.identity, + finalIdentity: raced.finalIdentity, + selection: raced.selection, + expectedOrcaTeamIdentifier: ORCA_TEAM, + platform: 'darwin', + spawnSyncImpl: (command, args, options) => { + if (!mutated) { + mutated = true + appendFileSync(args.at(-1), ':raced') + } + return spawn(command, args, options) + } + }) + ).rejects.toThrow(/changed during signature verification/i) + } finally { + await rm(raced.root, { recursive: true, force: true }) + } + }) + + it('rejects cross-platform execution and a stale signing selection', async () => { + const fixture = await signedFixture() + try { + await expect( + verifySshRelayRuntimeMacosSignatures({ + runtimeRoot: fixture.finalRuntimeRoot, + sourceIdentity: fixture.identity, + finalIdentity: fixture.finalIdentity, + selection: fixture.selection, + expectedOrcaTeamIdentifier: ORCA_TEAM, + platform: 'linux', + spawnSyncImpl: () => { + throw new Error('must not spawn') + } + }) + ).rejects.toThrow(/requires macOS/i) + + fixture.selection.signingFiles[0].sourceSha256 = `sha256:${'f'.repeat(64)}` + await expect( + verifySshRelayRuntimeMacosSignatures({ + runtimeRoot: fixture.finalRuntimeRoot, + sourceIdentity: fixture.identity, + finalIdentity: fixture.finalIdentity, + selection: fixture.selection, + expectedOrcaTeamIdentifier: ORCA_TEAM, + platform: 'darwin', + spawnSyncImpl: () => { + throw new Error('must not spawn') + } + }) + ).rejects.toThrow(/selection and identity disagree/i) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('rejects stale archive metadata and out-of-bound signed identity growth before probes', async () => { + const fixture = await signedFixture() + let calls = 0 + try { + const common = { + runtimeRoot: fixture.finalRuntimeRoot, + sourceIdentity: fixture.identity, + selection: fixture.selection, + expectedOrcaTeamIdentifier: ORCA_TEAM, + platform: 'darwin', + spawnSyncImpl: () => { + calls += 1 + return { status: 0, stdout: '', stderr: '' } + } + } + const staleArchive = structuredClone(fixture.finalIdentity) + staleArchive.archive = { fileName: 'unsigned.tar.xz' } + await expect( + verifySshRelayRuntimeMacosSignatures({ ...common, finalIdentity: staleArchive }) + ).rejects.toThrow(/does not match its unsigned source/i) + + const oversized = structuredClone(fixture.finalIdentity) + const first = oversized.entries.find( + (entry) => entry.path === fixture.selection.signingFiles[0].path + ) + const source = fixture.identity.entries.find((entry) => entry.path === first.path) + first.size = source.size + 4 * 1024 * 1024 + 1 + await expect( + verifySshRelayRuntimeMacosSignatures({ ...common, finalIdentity: oversized }) + ).rejects.toThrow(/identity transition is invalid/i) + expect(calls).toBe(0) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) +}) diff --git a/config/scripts/ssh-relay-runtime-native-signing-apply.mjs b/config/scripts/ssh-relay-runtime-native-signing-apply.mjs index ae061635548..26ecf0a0c3f 100644 --- a/config/scripts/ssh-relay-runtime-native-signing-apply.mjs +++ b/config/scripts/ssh-relay-runtime-native-signing-apply.mjs @@ -1,12 +1,11 @@ import { constants } from 'node:fs' import { chmod, copyFile, lstat, mkdir, realpath, rm } from 'node:fs/promises' import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path' -import { isDeepStrictEqual } from 'node:util' import { assertSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' import { verifySshRelayRuntimeNativeSigningReturn } from './ssh-relay-runtime-native-signing-payload.mjs' -import { buildSshRelayRuntimeNativeSigningSelection } from './ssh-relay-runtime-native-signing-selection.mjs' +import { assertSshRelayRuntimeNativeSigningSelection } from './ssh-relay-runtime-native-signing-selection.mjs' import { verifyRuntimeTree } from './verify-ssh-relay-runtime.mjs' function containsPath(parent, candidate) { @@ -62,39 +61,6 @@ function localPath(root, portablePath) { return resolve(root, ...portablePath.split('/')) } -function assertIdentityBoundSelection(identity, selection) { - if ( - !selection || - typeof selection !== 'object' || - !Array.isArray(selection.signingFiles) || - !Array.isArray(selection.preservedUpstreamFiles) - ) { - throw new Error('Runtime native signing apply requires a complete selection') - } - const assessments = - identity.os === 'win32' - ? [ - ...selection.signingFiles.map((entry) => ({ - path: entry.path, - sourceSha256: entry.sourceSha256, - status: 'unsigned' - })), - ...selection.preservedUpstreamFiles.map((entry) => ({ - path: entry.path, - sourceSha256: entry.sourceSha256, - status: 'valid-upstream', - signerSubject: entry.signerSubject, - signerThumbprint: entry.signerThumbprint - })) - ] - : [] - const expected = buildSshRelayRuntimeNativeSigningSelection(identity, assessments) - if (!isDeepStrictEqual(selection, expected)) { - // Why: signing selections cross a job boundary and must remain bound to the authenticated tree. - throw new Error('Runtime native signing apply selection and identity disagree') - } -} - function finalIdentity(identity, returnedFiles) { const returned = new Map(returnedFiles.map((entry) => [entry.path, entry])) const entries = identity.entries.map((entry) => { @@ -158,7 +124,7 @@ export async function applySshRelayRuntimeNativeSigningReturn({ copyFileImpl = copyFile }) { assertSshRelayRuntimeClosureEntries(identity) - assertIdentityBoundSelection(identity, selection) + assertSshRelayRuntimeNativeSigningSelection(identity, selection) if (selection.signingFiles.length === 0) { throw new Error('Runtime native signing apply selection has no signing files') } diff --git a/config/scripts/ssh-relay-runtime-native-signing-payload.mjs b/config/scripts/ssh-relay-runtime-native-signing-payload.mjs index 25b49e9a3be..f3ea08177f3 100644 --- a/config/scripts/ssh-relay-runtime-native-signing-payload.mjs +++ b/config/scripts/ssh-relay-runtime-native-signing-payload.mjs @@ -3,8 +3,8 @@ import { constants, createReadStream } from 'node:fs' import { copyFile, lstat, mkdir, readdir, realpath, rm } from 'node:fs/promises' import { basename, dirname, isAbsolute, relative, resolve } from 'node:path' -const MAX_SIGNED_FILE_GROWTH_BYTES = 4 * 1024 * 1024 -const MAX_RETURNED_PAYLOAD_BYTES = 64 * 1024 * 1024 +export const MAX_SIGNED_FILE_GROWTH_BYTES = 4 * 1024 * 1024 +export const MAX_RETURNED_PAYLOAD_BYTES = 64 * 1024 * 1024 function localPath(root, portablePath) { if ( diff --git a/config/scripts/ssh-relay-runtime-native-signing-selection.mjs b/config/scripts/ssh-relay-runtime-native-signing-selection.mjs index fdc5debc3d3..fdc5b2e41d3 100644 --- a/config/scripts/ssh-relay-runtime-native-signing-selection.mjs +++ b/config/scripts/ssh-relay-runtime-native-signing-selection.mjs @@ -1,3 +1,5 @@ +import { isDeepStrictEqual } from 'node:util' + import { buildSshRelayRuntimeNativeSigningPlan } from './ssh-relay-runtime-native-signing-plan.mjs' const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/ @@ -158,3 +160,36 @@ export function buildSshRelayRuntimeNativeSigningSelection(identity, assessments verificationFiles } } + +export function assertSshRelayRuntimeNativeSigningSelection(identity, selection) { + if ( + !selection || + typeof selection !== 'object' || + !Array.isArray(selection.signingFiles) || + !Array.isArray(selection.preservedUpstreamFiles) + ) { + throw new Error('Runtime native signing requires a complete selection') + } + const assessments = + identity.os === 'win32' + ? [ + ...selection.signingFiles.map((entry) => ({ + path: entry.path, + sourceSha256: entry.sourceSha256, + status: 'unsigned' + })), + ...selection.preservedUpstreamFiles.map((entry) => ({ + path: entry.path, + sourceSha256: entry.sourceSha256, + status: 'valid-upstream', + signerSubject: entry.signerSubject, + signerThumbprint: entry.signerThumbprint + })) + ] + : [] + const expected = buildSshRelayRuntimeNativeSigningSelection(identity, assessments) + if (!isDeepStrictEqual(selection, expected)) { + // Why: signing selections cross a job boundary and must remain bound to authenticated bytes. + throw new Error('Runtime native signing selection and identity disagree') + } +} diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 2a93d082323..aa6cacb5888 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -89,6 +89,9 @@ describe('SSH relay runtime artifact workflow', () => { ).toHaveLength(4) expect(source.match(/ssh-relay-runtime-native-signing-stage\.test\.mjs/g)).toHaveLength(4) expect(source.match(/ssh-relay-runtime-native-signing-apply\.test\.mjs/g)).toHaveLength(4) + expect(source.match(/ssh-relay-runtime-macos-signature-verification\.test\.mjs/g)).toHaveLength( + 4 + ) expect( source.match(/node --check config\/scripts\/ssh-relay-runtime-native-signing-plan\.mjs/g) ).toHaveLength(2) @@ -109,6 +112,11 @@ describe('SSH relay runtime artifact workflow', () => { expect( source.match(/node --check config\/scripts\/ssh-relay-runtime-native-signing-apply\.mjs/g) ).toHaveLength(2) + expect( + source.match( + /node --check config\/scripts\/ssh-relay-runtime-macos-signature-verification\.mjs/g + ) + ).toHaveLength(2) expect( source.match(/node --check config\/scripts\/ssh-relay-runtime-closure\.mjs/g) ).toHaveLength(2) From 6d47ef1458a9c57cc96fa26e1745eba8d73a7987 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 23:22:17 -0400 Subject: [PATCH 096/267] docs(ssh): record macOS signature contracts Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 6 +- ...github-release-implementation-checklist.md | 105 +++++++++++++++++- 2 files changed, 108 insertions(+), 3 deletions(-) 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 1a5ef6af10d..8050f6335bf 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 @@ -40,8 +40,10 @@ complete a box. - Proven locally and in exact-head run [29385274738](https://github.com/stablyai/orca/actions/runs/29385274738): exact returned-file application into a fresh full runtime and final post-sign content identity contracts. - - Active next: remaining credential-free native-trust/release failure contracts; real signing and - missing oldest-floor snapshots remain externally gated. + - Proven locally at `debb13f30` under `E-M3-MACOS-SIGNATURE-LOCAL-001`: final-tree-first, bounded + strict-codesign and exact Node/Orca Developer ID policy contracts. + - Active next: exact-head Node 24 execution on all six native jobs; real signing and missing + oldest-floor snapshots remain externally 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. 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 a77702e0b95..005fbfb0b0d 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps 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** — exact returned-file application plus final post-sign runtime identity/closure contracts are proven locally and on all six native jobs under E-M3-NATIVE-SIGNING-APPLY-LOCAL-001 and E-M3-NATIVE-SIGNING-APPLY-CI-001. The next safe package is remaining credential-free native-trust/release failure contracts; real Apple/SignPath calls, returned production signatures, missing exact-floor snapshots, and native trust remain separately gated. Nothing is published or enabled, and legacy remains the production default.
      +Session checkpoint: **In progress — 2026-07-14, Codex implementation owner** — credential-free macOS post-sign strict-codesign and signer-policy verification contracts are implemented at `debb13f30` and locally green under E-M3-MACOS-SIGNATURE-LOCAL-001. Exact-head Node 24 execution on all six native jobs is the active gate; real Apple signing, returned production signatures, Gatekeeper/notarization, 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) @@ -8282,6 +8282,109 @@ diff --check`. protected signing services and exact-floor runners before any native-signing/trust checkbox or aggregate output can pass. +### E-M3-MACOS-SIGNATURE-LOCAL-RED-001 — Missing macOS signature verifier fails the focused contract + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: pre-implementation head `2d4eab2c592adb0fd6396d6312394dddc6be6bc2`. +- Command: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-macos-signature-verification.test.mjs + ``` + +- Result: expected RED. The purpose-named test file failed to load because + `ssh-relay-runtime-macos-signature-verification.mjs` did not exist: 1 failed file/0 tests in + 385 ms. No runtime, workflow, credential, signing, publication, or production behavior changed. +- Oracle proved: final-tree application did not yet enforce strict native macOS signature validity + or exact upstream Node/Orca signer policy after returned bytes were substituted. +- Does not prove: any implementation, signed Orca bytes, native trust, Gatekeeper/notarization, + release aggregation, SSH behavior, or an enabled tuple. +- Correction: authenticate the complete final runtime before native probes, bind the selection and + unsigned/final identities, run bounded argument-array codesign probes on every native file, pin + official Node policy, require the configured Orca team, and hash each file again after probing. + +### E-M3-MACOS-SIGNATURE-LOCAL-001 — Final-tree-first macOS Developer ID policy passes locally + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: implementation commit `debb13f3011dc750e54826a4820f109e7d80f959`, based on exact + apply-CI evidence commit `2d4eab2c592adb0fd6396d6312394dddc6be6bc2`. +- Runner/remote/network: local macOS 26.2 build 25C56 arm64, Node v26.0.0 and pnpm 10.24.0; no + remote, network, credential, signing service, artifact publication, Gatekeeper/notarization, or + production path was used. Synthetic Developer ID output was dependency-injected; the two real + official Node checks used previously downloaded unpublished artifacts from run 29384042509. +- Commands: + + ```sh + node --check config/scripts/ssh-relay-runtime-macos-signature-verification.mjs + node --check config/scripts/ssh-relay-runtime-macos-signature-verification.test.mjs + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-native-signing-payload.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-selection.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs \ + config/scripts/ssh-relay-runtime-macos-signature-verification.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + 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 + pnpm exec oxfmt --check \ + .github/workflows/ssh-relay-runtime-artifacts.yml \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-payload.mjs \ + config/scripts/ssh-relay-runtime-native-signing-selection.mjs \ + config/scripts/ssh-relay-runtime-native-signing-apply.mjs \ + config/scripts/ssh-relay-runtime-macos-signature-verification.mjs \ + config/scripts/ssh-relay-runtime-macos-signature-verification.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 + for archive in ; do + tar -xJf "$archive" -C + /usr/bin/codesign --verify --strict --verbose=4 /bin/node + /usr/bin/codesign --display --verbose=4 /bin/node + done + git diff --check + ``` + +- Result: PASS. The signing payload/selection/application/macOS/workflow command passed 5 files/36 + tests in 1.08 seconds; the final macOS-plus-workflow check passed 2 files/13 tests in 2.32 + seconds; and the complete purpose-named SSH relay script suite passed 29 files/152 tests in 8.81 + seconds. Both direct syntax checks, typecheck, full lint/reliability gates, max-lines (355 + grandfathered suppressions and no new bypass), bundled-skill verification, localization + catalog/coverage, focused formatting, and `git diff --check` passed. Lint emitted only existing + unrelated warnings; local Node 26 emitted the expected repository Node-24 engine warning. +- Real official-Node result: PASS for both previously audited Node v24.18.0 candidates. Strict + codesign reports each file valid on disk and satisfying its designated requirement. The arm64 and + x86_64 display output both record signature size 8,986, exact first authority + `Developer ID Application: Node.js Foundation (HX7739G8FX)`, the Developer ID Certification + Authority and Apple Root CA chain, and team identifier `HX7739G8FX`. This authenticates the pinned + vendor policy only; the three Orca-built files in those unsigned candidates remain ad-hoc signed. +- Verification oracle: the verifier runs only on macOS, requires an exact ten-character configured + Orca team ID, reconstructs the selection from the authenticated unsigned identity, rejects stale + unsigned archive metadata, and permits changes only to the exact three selected files within the + established 4-MiB/file and 64-MiB/return bounds. It verifies final totals/content identity and the + complete physical tree before any native command. Every target is a regular file whose SHA-256 + matches before and after two `/usr/bin/codesign` argument-array probes bounded to 30 seconds and + 64 KiB. Node requires the exact pinned authority/team; every Orca-built target requires the Apple + Developer ID chain and configured Orca team. Ad-hoc, malformed/duplicate output, wrong authority, + wrong team, command error/nonzero/oversized output, source/final/selection drift, and probe-time + mutation fail closed. +- Workflow oracle: POSIX and Windows native contract jobs each syntax-check the source/test and + explicitly execute the suite. Four test-path and two source-check occurrences are locked by the + workflow contract. No job invokes codesign on fake returned bytes, connects credentials, signs, + publishes, aggregates, or enables a tuple. +- Does not prove: real Developer ID signatures on the three returned Orca files, the actual Orca + team/authority, target-native post-sign execution, macOS 13.5, quarantine/Gatekeeper, notarized + containing-app provenance, Apple credential/timeout behavior, Windows final trust, release + aggregation, SSH transfer/install, packaged desktop use, fallback/performance, or an enabled + tuple. +- Follow-up: commit this evidence separately, push both commits, and require exact-head POSIX and + Windows logs to show the macOS policy suite under Node 24.18.0 on all six native jobs while every + prior build/smoke/equality/baseline, PR-check, and Golden control retains its outcome. Keep native + signing/trust unchecked until real returned bytes pass target-native policy and endpoint gates. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 0efd19c9c3e82979554804d4e72b6fb5a3cbff7e Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 23:41:45 -0400 Subject: [PATCH 097/267] docs(ssh): record macOS signature policy CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 8 +++-- ...github-release-implementation-checklist.md | 34 ++++++++++++++++++- 2 files changed, 38 insertions(+), 4 deletions(-) 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 8050f6335bf..3db90ccca3a 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 @@ -40,9 +40,11 @@ complete a box. - Proven locally and in exact-head run [29385274738](https://github.com/stablyai/orca/actions/runs/29385274738): exact returned-file application into a fresh full runtime and final post-sign content identity contracts. - - Proven locally at `debb13f30` under `E-M3-MACOS-SIGNATURE-LOCAL-001`: final-tree-first, bounded - strict-codesign and exact Node/Orca Developer ID policy contracts. - - Active next: exact-head Node 24 execution on all six native jobs; real signing and missing + - Proven locally and in exact-head run + [29386372366](https://github.com/stablyai/orca/actions/runs/29386372366): final-tree-first, + bounded strict-codesign and exact Node/Orca Developer ID policy contracts run under Node 24 on + all six native jobs. This is contract proof, not proof of real Orca signatures or native trust. + - Active next: credential-free Windows Authenticode policy verification; real signing and missing oldest-floor snapshots remain externally 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. 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 005fbfb0b0d..86004609a38 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps 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 implemented at `debb13f30` and locally green under E-M3-MACOS-SIGNATURE-LOCAL-001. Exact-head Node 24 execution on all six native jobs is the active gate; real Apple signing, returned production signatures, Gatekeeper/notarization, missing exact-floor snapshots, and native trust remain separately gated. Nothing is published or enabled, and legacy remains the production default.
      +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. Credential-free Windows Authenticode policy verification is the next bounded artifact-only package; 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) @@ -8385,6 +8385,38 @@ diff --check`. prior build/smoke/equality/baseline, PR-check, and Golden control retains its outcome. Keep native signing/trust unchecked until real returned bytes pass target-native policy and endpoint gates. +### E-M3-MACOS-SIGNATURE-CI-001 — macOS signature-policy contracts pass on all six native jobs + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: exact head `6d47ef1458a9c57cc96fa26e1745eba8d73a7987`. +- Runner/remote/network: GitHub-hosted target-native Linux x64/arm64, macOS x64/arm64, and Windows + x64/arm64 artifact jobs under Node v24.18.0. This used no SSH remote, signing credential/service, + artifact publication, production path, or enabled tuple. +- Runs: + - Artifact: [29386372366](https://github.com/stablyai/orca/actions/runs/29386372366) + - PR Checks: [29386372337](https://github.com/stablyai/orca/actions/runs/29386372337) + - Golden E2E: [29386372362](https://github.com/stablyai/orca/actions/runs/29386372362) +- Result: PASS. All six native artifact jobs passed their macOS signature contract step. The macOS + arm64 job ran the focused suite 7/7 in 345 ms and the aggregate 28 files/148 tests in 3.95 + seconds. The Windows x64 job ran the suite 7/7 in 2.944 seconds and the aggregate 29 files/152 + tests (145 pass, 7 platform skips) in 8.69 seconds. Native job durations were Linux x64 4m09s, + Linux arm64 4m37s, macOS x64 10m27s, macOS arm64 2m47s, Windows x64 5m37s, and Windows arm64 + 10m29s. +- Regression controls: both digest-pinned oldest-Linux-userland supplements passed (x64 35s, arm64 + 53s); Windows x64 build-20348 passed in 1m10s. Windows arm64 retained the expected floor failure: + hosted build 10.0.26200 does not prove required build 26100 (`osBuild: false`). PR Checks passed + in about 14m05s; Golden Linux/macOS passed in about 4m37s/3m01s. +- Oracle proved: the credential-free policy suite parses and executes under the pinned release Node + on every target-native shell family while prior artifact and repository controls retain their + expected outcomes. +- Does not prove: real Orca Developer ID signatures, actual Apple signing or returned production + bytes, Gatekeeper/notarization, macOS 13.5, Windows Authenticode/Defender/WDAC, Windows arm64 build + 26100, SSH transfer/install, packaged desktop use, fallback/performance, or an enabled tuple. +- Follow-up: keep native signing/trust and every tuple unchecked. Implement the independent + credential-free Windows final-signature policy contract, and require real returned bytes plus + target-native endpoint gates before recording native-trust evidence. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 072c0d434e91013fc29ce16059da82c1ac7bfc12 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 23:53:56 -0400 Subject: [PATCH 098/267] build(ssh): verify Windows runtime signatures Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 8 + ...runtime-windows-signature-verification.mjs | 207 ++++++++++ ...me-windows-signature-verification.test.mjs | 376 ++++++++++++++++++ .../ssh-relay-runtime-workflow.test.mjs | 8 + 4 files changed, 599 insertions(+) create mode 100644 config/scripts/ssh-relay-runtime-windows-signature-verification.mjs create mode 100644 config/scripts/ssh-relay-runtime-windows-signature-verification.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index cbb23eac16b..5b595170296 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -139,6 +139,8 @@ jobs: node --check config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs node --check config/scripts/ssh-relay-runtime-macos-signature-verification.mjs node --check config/scripts/ssh-relay-runtime-macos-signature-verification.test.mjs + node --check config/scripts/ssh-relay-runtime-windows-signature-verification.mjs + node --check config/scripts/ssh-relay-runtime-windows-signature-verification.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 \ @@ -158,6 +160,7 @@ jobs: config/scripts/ssh-relay-runtime-native-signing-stage.test.mjs \ config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs \ 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-closure.test.mjs \ config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ @@ -465,6 +468,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime macOS signature verification script syntax check failed' } node --check config/scripts/ssh-relay-runtime-macos-signature-verification.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime macOS signature verification test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-windows-signature-verification.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime Windows signature verification script syntax check failed' } + node --check config/scripts/ssh-relay-runtime-windows-signature-verification.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime Windows signature verification 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 ` @@ -484,6 +491,7 @@ jobs: config/scripts/ssh-relay-runtime-native-signing-stage.test.mjs ` config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs ` 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-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-windows-signature-verification.mjs b/config/scripts/ssh-relay-runtime-windows-signature-verification.mjs new file mode 100644 index 00000000000..ae7f5f37d1b --- /dev/null +++ b/config/scripts/ssh-relay-runtime-windows-signature-verification.mjs @@ -0,0 +1,207 @@ +import { spawnSync } from 'node:child_process' +import { createHash } from 'node:crypto' +import { createReadStream } from 'node:fs' +import { lstat, realpath } from 'node:fs/promises' +import { resolve } from 'node:path' +import { isDeepStrictEqual } from 'node:util' + +import { assertSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' +import { + MAX_RETURNED_PAYLOAD_BYTES, + MAX_SIGNED_FILE_GROWTH_BYTES +} from './ssh-relay-runtime-native-signing-payload.mjs' +import { assertSshRelayRuntimeNativeSigningSelection } from './ssh-relay-runtime-native-signing-selection.mjs' +import { + classifySshRelayRuntimeWindowsAuthenticode, + getSshRelayRuntimeWindowsAuthenticodeJson, + parseSshRelayRuntimeWindowsAuthenticodeJson +} from './ssh-relay-runtime-windows-authenticode-assessment.mjs' +import { verifyRuntimeTree } from './verify-ssh-relay-runtime.mjs' + +export const OFFICIAL_NODE_WINDOWS_SIGNER_SUBJECT = + 'CN=OpenJS Foundation, O=OpenJS Foundation, L=San Francisco, S=California, C=US' +export const ORCA_WINDOWS_SIGNER_SUBJECT = + 'CN=SignPath Foundation, O=SignPath Foundation, L=Lewes, S=Delaware, C=US' + +function localPath(root, portablePath) { + return resolve(root, ...portablePath.split('/')) +} + +async function sha256File(path) { + const hash = createHash('sha256') + for await (const chunk of createReadStream(path)) { + hash.update(chunk) + } + return `sha256:${hash.digest('hex')}` +} + +function assertIdentityTransition(sourceIdentity, finalIdentity, selection) { + assertSshRelayRuntimeClosureEntries(finalIdentity) + if ( + computeSshRelayRuntimeContentId(sourceIdentity) !== sourceIdentity.contentId || + computeSshRelayRuntimeContentId(finalIdentity) !== finalIdentity.contentId || + Object.hasOwn(finalIdentity, 'archive') || + finalIdentity.tupleId !== sourceIdentity.tupleId || + finalIdentity.os !== 'win32' || + finalIdentity.contentId === sourceIdentity.contentId + ) { + throw new Error('Runtime Windows final identity does not match its unsigned source') + } + const signingPaths = new Set(selection.signingFiles.map((entry) => entry.path)) + const finalEntries = new Map(finalIdentity.entries.map((entry) => [entry.path, entry])) + let returnedSize = 0 + for (const sourceEntry of sourceIdentity.entries) { + const finalEntry = finalEntries.get(sourceEntry.path) + if (!finalEntry) { + throw new Error(`Runtime Windows final identity is missing entry: ${sourceEntry.path}`) + } + if (signingPaths.has(sourceEntry.path)) { + if ( + sourceEntry.type !== 'file' || + finalEntry.type !== 'file' || + finalEntry.sha256 === sourceEntry.sha256 || + finalEntry.size <= 0 || + finalEntry.size > sourceEntry.size + MAX_SIGNED_FILE_GROWTH_BYTES || + finalEntry.path !== sourceEntry.path || + finalEntry.role !== sourceEntry.role || + finalEntry.mode !== sourceEntry.mode + ) { + throw new Error( + `Runtime Windows signed identity transition is invalid: ${sourceEntry.path}` + ) + } + returnedSize += finalEntry.size + } else if (!isDeepStrictEqual(finalEntry, sourceEntry)) { + throw new Error(`Runtime Windows unsigned identity entry changed: ${sourceEntry.path}`) + } + finalEntries.delete(sourceEntry.path) + } + if (finalEntries.size !== 0) { + throw new Error( + `Runtime Windows final identity has an extra entry: ${finalEntries.keys().next().value}` + ) + } + if (returnedSize > MAX_RETURNED_PAYLOAD_BYTES) { + throw new Error('Runtime Windows signed identity exceeds the returned payload size bound') + } + const files = finalIdentity.entries.filter((entry) => entry.type === 'file') + const expandedSize = files.reduce((total, entry) => total + entry.size, 0) + if (finalIdentity.fileCount !== files.length || finalIdentity.expandedSize !== expandedSize) { + throw new Error('Runtime Windows final identity totals are inconsistent') + } +} + +function assertSignerPolicy(signature, signerKind, expectedSigner) { + if (signature.status !== 'Valid') { + throw new Error('Runtime Windows requires a valid Authenticode signature on every native file') + } + const classified = classifySshRelayRuntimeWindowsAuthenticode(signature) + if (signerKind === 'official-node') { + if (classified.signerSubject !== OFFICIAL_NODE_WINDOWS_SIGNER_SUBJECT) { + throw new Error('Runtime Windows signature violates official Node signer policy') + } + } else if (signerKind === 'orca-built') { + if (classified.signerSubject !== ORCA_WINDOWS_SIGNER_SUBJECT) { + throw new Error('Runtime Windows signature violates Orca signer policy') + } + } else if ( + classified.signerSubject !== expectedSigner.signerSubject || + classified.signerThumbprint !== expectedSigner.signerThumbprint + ) { + throw new Error('Runtime Windows signature violates preserved upstream signer policy') + } + return classified +} + +async function verifySignedFile({ path, entry, signerKind, expectedSigner, spawnSyncImpl }) { + const metadata = await lstat(path) + if (metadata.isSymbolicLink() || !metadata.isFile()) { + throw new Error(`Runtime Windows signature target is not a regular file: ${entry.path}`) + } + if ((await sha256File(path)) !== entry.sha256) { + throw new Error(`Runtime Windows signature target has wrong authenticated hash: ${entry.path}`) + } + const signature = parseSshRelayRuntimeWindowsAuthenticodeJson( + getSshRelayRuntimeWindowsAuthenticodeJson(path, spawnSyncImpl) + ) + const classified = assertSignerPolicy(signature, signerKind, expectedSigner) + if ((await sha256File(path)) !== entry.sha256) { + throw new Error(`Runtime Windows file changed during signature verification: ${entry.path}`) + } + return { + path: entry.path, + role: entry.role, + sha256: entry.sha256, + signerKind, + signerSubject: classified.signerSubject, + signerThumbprint: classified.signerThumbprint + } +} + +function verificationTargets(finalIdentity, selection) { + const finalFiles = new Map( + finalIdentity.entries + .filter((entry) => entry.type === 'file') + .map((entry) => [entry.path, entry]) + ) + const node = new Map(selection.immutableVendorFiles.map((entry) => [entry.path, entry])) + const signing = new Map(selection.signingFiles.map((entry) => [entry.path, entry])) + const preserved = new Map(selection.preservedUpstreamFiles.map((entry) => [entry.path, entry])) + return selection.verificationFiles.map((verification) => { + const entry = finalFiles.get(verification.path) + if (!entry) { + throw new Error('Runtime Windows signature target is missing from the final identity') + } + if (node.has(entry.path)) { + return { entry, signerKind: 'official-node' } + } + if (signing.has(entry.path)) { + return { entry, signerKind: 'orca-built' } + } + const expectedSigner = preserved.get(entry.path) + if (!expectedSigner) { + throw new Error(`Runtime Windows signature target has no signer policy: ${entry.path}`) + } + return { entry, signerKind: 'preserved-upstream', expectedSigner } + }) +} + +export async function verifySshRelayRuntimeWindowsSignatures({ + runtimeRoot, + sourceIdentity, + finalIdentity, + selection, + platform = process.platform, + spawnSyncImpl = spawnSync +}) { + if (platform !== 'win32') { + throw new Error('Runtime Windows signature verification requires Windows') + } + assertSshRelayRuntimeNativeSigningSelection(sourceIdentity, selection) + assertIdentityTransition(sourceIdentity, finalIdentity, selection) + const rootMetadata = await lstat(resolve(runtimeRoot)) + if (rootMetadata.isSymbolicLink() || !rootMetadata.isDirectory()) { + throw new Error('Runtime Windows signature verification requires a real runtime root') + } + const physicalRoot = await realpath(resolve(runtimeRoot)) + // Why: native code is probed only after every byte in the final runtime matches its new identity. + await verifyRuntimeTree(physicalRoot, finalIdentity) + + const verifiedFiles = [] + for (const target of verificationTargets(finalIdentity, selection)) { + verifiedFiles.push( + await verifySignedFile({ + path: localPath(physicalRoot, target.entry.path), + ...target, + spawnSyncImpl + }) + ) + } + return { + tupleId: finalIdentity.tupleId, + sourceContentId: sourceIdentity.contentId, + finalContentId: finalIdentity.contentId, + verifiedFiles + } +} diff --git a/config/scripts/ssh-relay-runtime-windows-signature-verification.test.mjs b/config/scripts/ssh-relay-runtime-windows-signature-verification.test.mjs new file mode 100644 index 00000000000..44ceed0f942 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-windows-signature-verification.test.mjs @@ -0,0 +1,376 @@ +import { createHash } from 'node:crypto' +import { appendFileSync } from 'node:fs' +import { appendFile, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + +import { describe, expect, it } from 'vitest' + +import { expectedSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' +import { applySshRelayRuntimeNativeSigningReturn } from './ssh-relay-runtime-native-signing-apply.mjs' +import { buildSshRelayRuntimeNativeSigningPlan } from './ssh-relay-runtime-native-signing-plan.mjs' +import { buildSshRelayRuntimeNativeSigningSelection } from './ssh-relay-runtime-native-signing-selection.mjs' +import { + OFFICIAL_NODE_WINDOWS_SIGNER_SUBJECT, + ORCA_WINDOWS_SIGNER_SUBJECT, + verifySshRelayRuntimeWindowsSignatures +} from './ssh-relay-runtime-windows-signature-verification.mjs' + +const MICROSOFT_SUBJECT = + 'CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US' + +function digest(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +async function runtimeFixture() { + const root = await mkdtemp(join(tmpdir(), 'ssh-relay-windows-signature-')) + const runtimeRoot = join(root, 'runtime') + await mkdir(runtimeRoot) + const entries = [] + for (const entry of expectedSshRelayRuntimeClosureEntries('win32-x64')) { + if (entry.type === 'directory') { + await mkdir(join(runtimeRoot, ...entry.path.split('/')), { + recursive: true, + mode: entry.mode + }) + entries.push(entry) + continue + } + const bytes = Buffer.from(`fixture:${entry.path}`) + const path = join(runtimeRoot, ...entry.path.split('/')) + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, bytes, { mode: entry.mode }) + entries.push({ ...entry, size: bytes.length, sha256: digest(bytes) }) + } + const base = { + identitySchemaVersion: 1, + tupleId: 'win32-x64', + os: 'win32', + architecture: 'x64', + compatibility: sshRelayRuntimeCompatibility['win32-x64'], + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries + } + const files = entries.filter((entry) => entry.type === 'file') + return { + root, + runtimeRoot, + identity: { + ...base, + contentId: computeSshRelayRuntimeContentId(base), + fileCount: files.length, + expandedSize: files.reduce((total, entry) => total + entry.size, 0) + } + } +} + +function assessmentsFor(identity) { + return buildSshRelayRuntimeNativeSigningPlan(identity).signingCandidates.map((entry, index) => { + const preserved = entry.path.endsWith('/OpenConsole.exe') || entry.path.endsWith('/conpty.dll') + return preserved + ? { + path: entry.path, + sourceSha256: entry.sourceSha256, + status: 'valid-upstream', + signerSubject: MICROSOFT_SUBJECT, + signerThumbprint: `${index + 1}`.repeat(40) + } + : { path: entry.path, sourceSha256: entry.sourceSha256, status: 'unsigned' } + }) +} + +async function signedFixture() { + const fixture = await runtimeFixture() + const selection = buildSshRelayRuntimeNativeSigningSelection( + fixture.identity, + assessmentsFor(fixture.identity) + ) + const returnedRoot = join(fixture.root, 'returned') + for (const entry of selection.signingFiles) { + const path = join(returnedRoot, ...entry.path.split('/')) + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, `signed:${entry.path}`) + } + const finalRuntimeRoot = join(fixture.root, 'final-runtime') + const applied = await applySshRelayRuntimeNativeSigningReturn({ + sourceRuntimeRoot: fixture.runtimeRoot, + returnedRoot, + outputRuntimeRoot: finalRuntimeRoot, + identity: fixture.identity, + selection + }) + return { ...fixture, finalRuntimeRoot, selection, finalIdentity: applied.identity } +} + +function signatureJson(signerSubject, signerThumbprint) { + return JSON.stringify({ status: 'Valid', signerSubject, signerThumbprint }) +} + +function entryForPhysicalPath(selection, physicalPath) { + return [ + ...selection.immutableVendorFiles, + ...selection.signingFiles, + ...selection.preservedUpstreamFiles + ].find((entry) => physicalPath.endsWith(join(...entry.path.split('/')))) +} + +function successfulAuthenticode(selection, calls, override = () => undefined) { + return (command, args, options) => { + calls.push({ command, args, options }) + const physicalPath = options.env.ORCA_SSH_RELAY_AUTHENTICODE_FILE + const entry = entryForPhysicalPath(selection, physicalPath) + const overridden = override(entry) + if (overridden) { + return { status: 0, stdout: JSON.stringify(overridden), stderr: '' } + } + if (entry.action === 'preserve-exact-bytes') { + return { + status: 0, + stdout: signatureJson(OFFICIAL_NODE_WINDOWS_SIGNER_SUBJECT, 'A'.repeat(40)), + stderr: '' + } + } + if (entry.action === 'preserve-valid-upstream') { + return { + status: 0, + stdout: signatureJson(entry.signerSubject, entry.signerThumbprint), + stderr: '' + } + } + return { + status: 0, + stdout: signatureJson(ORCA_WINDOWS_SIGNER_SUBJECT, 'F'.repeat(40)), + stderr: '' + } + } +} + +describe('SSH relay runtime Windows signature verification', () => { + it('pins exact official Node and Orca SignPath signer subjects', () => { + expect(OFFICIAL_NODE_WINDOWS_SIGNER_SUBJECT).toBe( + 'CN=OpenJS Foundation, O=OpenJS Foundation, L=San Francisco, S=California, C=US' + ) + expect(ORCA_WINDOWS_SIGNER_SUBJECT).toBe( + 'CN=SignPath Foundation, O=SignPath Foundation, L=Lewes, S=Delaware, C=US' + ) + }) + + it('verifies the complete final tree before exact Node, SignPath, and preserved signer policy', async () => { + const fixture = await signedFixture() + const calls = [] + try { + const report = await verifySshRelayRuntimeWindowsSignatures({ + runtimeRoot: fixture.finalRuntimeRoot, + sourceIdentity: fixture.identity, + finalIdentity: fixture.finalIdentity, + selection: fixture.selection, + platform: 'win32', + spawnSyncImpl: successfulAuthenticode(fixture.selection, calls) + }) + + expect(report.tupleId).toBe('win32-x64') + expect(report.verifiedFiles).toHaveLength(6) + expect(report.verifiedFiles.find((entry) => entry.role === 'node')).toEqual( + expect.objectContaining({ + signerKind: 'official-node', + signerSubject: OFFICIAL_NODE_WINDOWS_SIGNER_SUBJECT + }) + ) + expect(calls).toHaveLength(6) + expect(calls.every((call) => call.command === 'pwsh')).toBe(true) + expect(calls.every((call) => call.options.timeout === 30_000)).toBe(true) + expect(calls.every((call) => call.options.maxBuffer === 64 * 1024)).toBe(true) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('rejects unexpected official Node, SignPath, and preserved signer identities', async () => { + const fixture = await signedFixture() + const common = { + runtimeRoot: fixture.finalRuntimeRoot, + sourceIdentity: fixture.identity, + finalIdentity: fixture.finalIdentity, + selection: fixture.selection, + platform: 'win32' + } + try { + await expect( + verifySshRelayRuntimeWindowsSignatures({ + ...common, + spawnSyncImpl: successfulAuthenticode(fixture.selection, [], (entry) => + entry.action === 'preserve-exact-bytes' + ? { + status: 'Valid', + signerSubject: 'CN=Unexpected Node Signer', + signerThumbprint: 'A'.repeat(40) + } + : undefined + ) + }) + ).rejects.toThrow(/official Node signer policy/i) + + await expect( + verifySshRelayRuntimeWindowsSignatures({ + ...common, + spawnSyncImpl: successfulAuthenticode(fixture.selection, [], (entry) => + entry.action === 'signpath-required' + ? { + status: 'Valid', + signerSubject: 'CN=Unexpected Orca Signer', + signerThumbprint: 'F'.repeat(40) + } + : undefined + ) + }) + ).rejects.toThrow(/Orca signer policy/i) + + await expect( + verifySshRelayRuntimeWindowsSignatures({ + ...common, + spawnSyncImpl: successfulAuthenticode(fixture.selection, [], (entry) => + entry.action === 'preserve-valid-upstream' + ? { + status: 'Valid', + signerSubject: entry.signerSubject, + signerThumbprint: '0'.repeat(40) + } + : undefined + ) + }) + ).rejects.toThrow(/preserved upstream signer policy/i) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('fails closed on invalid signature status and bounded probe failure', async () => { + const fixture = await signedFixture() + const common = { + runtimeRoot: fixture.finalRuntimeRoot, + sourceIdentity: fixture.identity, + finalIdentity: fixture.finalIdentity, + selection: fixture.selection, + platform: 'win32' + } + try { + await expect( + verifySshRelayRuntimeWindowsSignatures({ + ...common, + spawnSyncImpl: () => ({ + status: 0, + stdout: JSON.stringify({ + status: 'NotSigned', + signerSubject: null, + signerThumbprint: null + }), + stderr: '' + }) + }) + ).rejects.toThrow(/requires a valid Authenticode signature/i) + await expect( + verifySshRelayRuntimeWindowsSignatures({ + ...common, + spawnSyncImpl: () => ({ error: new Error('timed out') }) + }) + ).rejects.toThrow(/timed out/i) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('authenticates the tree before spawning and rejects mutation during native probes', async () => { + const fixture = await signedFixture() + let calls = 0 + try { + await appendFile(join(fixture.finalRuntimeRoot, 'relay.js'), ':mutated') + await expect( + verifySshRelayRuntimeWindowsSignatures({ + runtimeRoot: fixture.finalRuntimeRoot, + sourceIdentity: fixture.identity, + finalIdentity: fixture.finalIdentity, + selection: fixture.selection, + platform: 'win32', + spawnSyncImpl: () => { + calls += 1 + throw new Error('must not spawn') + } + }) + ).rejects.toThrow(/integrity mismatch/i) + expect(calls).toBe(0) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + + const raced = await signedFixture() + try { + const spawn = successfulAuthenticode(raced.selection, []) + let mutated = false + await expect( + verifySshRelayRuntimeWindowsSignatures({ + runtimeRoot: raced.finalRuntimeRoot, + sourceIdentity: raced.identity, + finalIdentity: raced.finalIdentity, + selection: raced.selection, + platform: 'win32', + spawnSyncImpl: (command, args, options) => { + if (!mutated) { + mutated = true + appendFileSync(options.env.ORCA_SSH_RELAY_AUTHENTICODE_FILE, ':raced') + } + return spawn(command, args, options) + } + }) + ).rejects.toThrow(/changed during signature verification/i) + } finally { + await rm(raced.root, { recursive: true, force: true }) + } + }) + + it('rejects cross-platform execution and stale selection or final identity before probes', async () => { + const fixture = await signedFixture() + let calls = 0 + const common = { + runtimeRoot: fixture.finalRuntimeRoot, + sourceIdentity: fixture.identity, + finalIdentity: fixture.finalIdentity, + selection: fixture.selection, + spawnSyncImpl: () => { + calls += 1 + throw new Error('must not spawn') + } + } + try { + await expect( + verifySshRelayRuntimeWindowsSignatures({ ...common, platform: 'darwin' }) + ).rejects.toThrow(/requires Windows/i) + + const staleSelection = structuredClone(fixture.selection) + staleSelection.signingFiles[0].sourceSha256 = `sha256:${'f'.repeat(64)}` + await expect( + verifySshRelayRuntimeWindowsSignatures({ + ...common, + selection: staleSelection, + platform: 'win32' + }) + ).rejects.toThrow(/wrong source hash|selection and identity disagree/i) + + const staleArchive = structuredClone(fixture.finalIdentity) + staleArchive.archive = { fileName: 'unsigned.zip' } + await expect( + verifySshRelayRuntimeWindowsSignatures({ + ...common, + finalIdentity: staleArchive, + platform: 'win32' + }) + ).rejects.toThrow(/does not match its unsigned source/i) + expect(calls).toBe(0) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) +}) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index aa6cacb5888..04a6aae1c9a 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -92,6 +92,9 @@ describe('SSH relay runtime artifact workflow', () => { expect(source.match(/ssh-relay-runtime-macos-signature-verification\.test\.mjs/g)).toHaveLength( 4 ) + expect( + source.match(/ssh-relay-runtime-windows-signature-verification\.test\.mjs/g) + ).toHaveLength(4) expect( source.match(/node --check config\/scripts\/ssh-relay-runtime-native-signing-plan\.mjs/g) ).toHaveLength(2) @@ -117,6 +120,11 @@ describe('SSH relay runtime artifact workflow', () => { /node --check config\/scripts\/ssh-relay-runtime-macos-signature-verification\.mjs/g ) ).toHaveLength(2) + expect( + source.match( + /node --check config\/scripts\/ssh-relay-runtime-windows-signature-verification\.mjs/g + ) + ).toHaveLength(2) expect( source.match(/node --check config\/scripts\/ssh-relay-runtime-closure\.mjs/g) ).toHaveLength(2) From cce37cd415600e9bd77093d250b5cb8e3ad9f3ce Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Tue, 14 Jul 2026 23:54:16 -0400 Subject: [PATCH 099/267] docs(ssh): record Windows signature policy proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 5 +- ...github-release-implementation-checklist.md | 103 +++++++++++++++++- 2 files changed, 105 insertions(+), 3 deletions(-) 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 3db90ccca3a..21bc3c5cafb 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 @@ -44,8 +44,9 @@ complete a box. [29386372366](https://github.com/stablyai/orca/actions/runs/29386372366): final-tree-first, bounded strict-codesign and exact Node/Orca Developer ID policy contracts run under Node 24 on all six native jobs. This is contract proof, not proof of real Orca signatures or native trust. - - Active next: credential-free Windows Authenticode policy verification; real signing and missing - oldest-floor snapshots remain externally gated. + - Proven locally: credential-free Windows final-tree-first Authenticode policy contracts under + `E-M3-WINDOWS-SIGNATURE-LOCAL-001`. Exact-head execution on all six native jobs is next. Real + signing and missing oldest-floor snapshots remain externally 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. 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 86004609a38..2a9feba9003 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps 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. Credential-free Windows Authenticode policy verification is the next bounded artifact-only package; 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.
      +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 are locally green under E-M3-WINDOWS-SIGNATURE-LOCAL-001. Exact-head execution on all six target-native jobs 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) @@ -8417,6 +8417,107 @@ diff --check`. credential-free Windows final-signature policy contract, and require real returned bytes plus target-native endpoint gates before recording native-trust evidence. +### E-M3-WINDOWS-SIGNATURE-LOCAL-RED-001 — Missing final Windows signature verifier fails focused contract + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: pre-implementation head `0efd19c9c3e82979554804d4e72b6fb5a3cbff7e` plus the new + uncommitted purpose-named test. +- Command: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-windows-signature-verification.test.mjs + ``` + +- Result: expected RED. The focused suite failed 1 file/0 tests in 463 ms because + `ssh-relay-runtime-windows-signature-verification.mjs` did not exist. No credential, signing + service, workflow, publication, tuple, SSH, or production behavior changed. +- Oracle proved: the existing pre-sign assessment and return-application modules did not already + authenticate the complete final Windows tree and enforce final official-Node, Orca SignPath, and + preserved-upstream signer policy. +- Does not prove: the implementation, real SignPath returned bytes, Windows trust, Defender/WDAC, + target-native behavior, SSH transfer/install, fallback/performance, or an enabled tuple. +- Correction: authenticate the complete final tree before native probes; require `Valid` on every + native file; pin the exact official Node and SignPath subjects; require preserved files to retain + their assessed subject and thumbprint; bound PowerShell; and hash every target before and after + probing. + +### E-M3-WINDOWS-SIGNATURE-LOCAL-001 — Final-tree-first Windows signer policy passes locally + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: implementation commit `072c0d434e91013fc29ce16059da82c1ac7bfc12`, based on local + evidence commit `0efd19c9c3e82979554804d4e72b6fb5a3cbff7e`. +- Runner/remote/network: local macOS 26.2 build 25C56 arm64, Node v26.0.0 and pnpm 10.24.0. No SSH + remote, credential, signing service, PowerShell trust execution, publication, or production path + was used. Synthetic `Get-AuthenticodeSignature` output was dependency-injected. Exact unpublished + win32 x64/arm64 artifacts were downloaded from run 29386372366 solely for authenticated embedded + certificate inspection. +- Commands: + + ```sh + node --check config/scripts/ssh-relay-runtime-windows-signature-verification.mjs + node --check config/scripts/ssh-relay-runtime-windows-signature-verification.test.mjs + pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-native-signing-selection.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs \ + config/scripts/ssh-relay-runtime-windows-authenticode-assessment.test.mjs \ + config/scripts/ssh-relay-runtime-windows-signature-verification.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + 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 + pnpm exec oxfmt --check \ + .github/workflows/ssh-relay-runtime-artifacts.yml \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-windows-signature-verification.mjs \ + config/scripts/ssh-relay-runtime-windows-signature-verification.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 + gh run download 29386372366 --repo stablyai/orca --pattern 'ssh-relay-runtime-win32-*' + unzip -p bin/node.exe | shasum -a 256 + unzip -p bin/node.exe | node -e | \ + openssl pkcs7 -inform DER -print_certs -noout + git diff --check + ``` + +- Result: PASS. The focused five-suite command passed 5 files/32 tests in 3.89 seconds. The complete + purpose-named SSH relay suite passed 30 files/158 tests in 5.39 seconds; the final post-format + verifier/workflow rerun passed 2 files/12 tests in 368 ms. Both syntax checks, typecheck, full lint + and reliability/localization/bundled-skill gates, max-lines (355 grandfathered suppressions and no + new bypass), focused formatting, and `git diff --check` passed. Lint emitted only existing + unrelated warnings; local Node 26 emitted the expected repository Node-24 engine warning. +- Verification oracle: the verifier accepts only Windows, reconstructs and authenticates the + signing selection, enforces the bounded source-to-final identity transition and final content ID, + and verifies the complete physical final tree before any native command. It probes all six native + files through the existing argument-array PowerShell contract bounded to 30 seconds and 64 KiB, + requires `Valid`, and hashes each file before and after probing. Official Node requires exact + subject `CN=OpenJS Foundation, O=OpenJS Foundation, L=San Francisco, S=California, C=US`; every + returned Orca file requires exact SignPath Foundation subject; preserved Microsoft files must + retain both their source-assessed subject and 40-hex thumbprint. Wrong/malformed status or signer, + stale selection/identity/archive metadata, unbounded growth, tree mutation, and probe-time mutation + fail closed. +- Exact official-Node certificate evidence: the arm64/x64 `bin/node.exe` bytes match their identities + at SHA-256 `c7225670c3f477778e18c43a55867f7a0d76468221245e5981ab80eb953c8102` (81,067,848 + bytes) and `9a4eb5f1c29c6a2e93852ead46b999e284a6a5ca8bab4d4e241d587d025a52de` (92,534,088 + bytes). Both PE certificate tables contain the OpenJS Foundation subject in San Francisco, + California, issued by Microsoft ID Verified CS AOC CA 04. This establishes the signer subject + encoded in the exact authenticated inputs; local OpenSSL inspection does not establish Windows + Authenticode `Valid` status or endpoint trust. +- Workflow oracle: both POSIX and Windows artifact job families syntax-check the verifier source and + test and run the suite. Four test-path and two source-check occurrences are locked. No job invokes + SignPath, fabricates final bytes, accepts native trust, publishes, or enables a tuple. +- Does not prove: real SignPath signatures on the three returned Orca files, target-native final-tree + execution, Windows Server 2022/Windows 11 trust, Defender/WDAC, the arm64 build-26100 floor, signing + approval/timeout behavior, release aggregation, SSH transfer/install, packaged desktop use, + fallback/performance, or an enabled tuple. +- Follow-up: commit and push the implementation/evidence separately, then require exact-head POSIX + and PowerShell logs to show the new suite under Node 24.18.0 on all six native jobs while every + prior artifact, baseline, PR-check, and Golden control retains its expected outcome. Keep native + signing/trust unchecked until real returned bytes pass target-native policy and endpoint gates. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From bd3e40dc52ede75e44ded2f5e84f5092be5b2d87 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 00:10:33 -0400 Subject: [PATCH 100/267] docs(ssh): record Windows signature policy CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 9 ++- ...github-release-implementation-checklist.md | 61 ++++++++++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) 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 21bc3c5cafb..ace78dc5881 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 @@ -44,9 +44,12 @@ complete a box. [29386372366](https://github.com/stablyai/orca/actions/runs/29386372366): final-tree-first, bounded strict-codesign and exact Node/Orca Developer ID policy contracts run under Node 24 on all six native jobs. This is contract proof, not proof of real Orca signatures or native trust. - - Proven locally: credential-free Windows final-tree-first Authenticode policy contracts under - `E-M3-WINDOWS-SIGNATURE-LOCAL-001`. Exact-head execution on all six native jobs is next. Real - signing and missing oldest-floor snapshots remain externally gated. + - Proven locally and in exact-head run + [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. + - Active next: target-native official-Node and preserved-upstream Windows source-signature policy; + real signing and missing oldest-floor snapshots remain externally 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. 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 2a9feba9003..82575364f97 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps 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 are locally green under E-M3-WINDOWS-SIGNATURE-LOCAL-001. Exact-head execution on all six target-native jobs 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.
      +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 Windows source-signature policy is the next credential-free 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) @@ -8518,6 +8518,65 @@ diff --check`. prior artifact, baseline, PR-check, and Golden control retains its expected outcome. Keep native signing/trust unchecked until real returned bytes pass target-native policy and endpoint gates. +### E-M3-WINDOWS-SIGNATURE-CI-001 — Windows signer-policy contracts pass on all six native jobs + +- Date: 2026-07-14 (run timestamps 2026-07-15 UTC) +- Owner: Codex implementation owner +- Source/run: exact draft-PR head `cce37cd415600e9bd77093d250b5cb8e3ad9f3ce`, containing + implementation `072c0d434e91013fc29ce16059da82c1ac7bfc12`; Actions run + [29387668264](https://github.com/stablyai/orca/actions/runs/29387668264). +- Commands: + + ```sh + gh run view 29387668264 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh run view 29387668264 --repo stablyai/orca --job 87264193814 --log | \ + awk -F '\t' '$2 == "Run runtime artifact contract tests"' | \ + rg 'windows-signature-verification|Test Files|Tests|Duration' + gh run view 29387668264 --repo stablyai/orca --job 87264193803 --log | \ + awk -F '\t' '$2 == "Run runtime artifact contract tests"' | \ + rg 'windows-signature-verification|Test Files|Tests|Duration' + gh run view 29387668264 --repo stablyai/orca --job 87264193809 --log | \ + awk -F '\t' '$2 == "Run runtime artifact contract tests"' | \ + rg 'windows-signature-verification|Test Files|Tests|Duration' + gh run view 29387668264 --repo stablyai/orca --job 87265321571 --log | \ + rg 'OsBuildNumber|26100|26200|osBuild' + gh run view 29387668227 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + gh run view 29387668205 --repo stablyai/orca \ + --json status,conclusion,headSha,createdAt,updatedAt,url,jobs + ``` + +- Contract result: PASS on Linux x64/arm64, macOS x64/arm64, and Windows x64/arm64. Every native + build job syntax-checked the source/test and executed the new suite under exact Node 24.18.0. The + macOS arm64 log records 6/6 in 372 ms and aggregate 29 files/154 tests in 4.86 seconds. Windows + x64 records 6/6 in 2.643 seconds and aggregate 30 files/158 tests (151 pass, 7 platform skips) in + 8.13 seconds; Windows arm64 records 6/6 in 3.722 seconds and the same aggregate in 11.35 seconds. +- Native build/regression controls: all six double-build, smoke, exact-equality, metadata, stage, + cleanup, and unpublished-upload jobs passed. Durations were Linux x64 4m08s, Linux arm64 4m20s, + macOS x64 9m42s, macOS arm64 2m58s, Windows x64 5m14s, and Windows arm64 9m13s. Linux x64/arm64 + digest-pinned glibc 2.28/libstdc++ 6.0.25 supplements passed in 52s/52s. Windows x64 exact + build-20348 passed in 1m17s. +- Expected aggregate status: the artifact workflow concluded failure only because the Windows arm64 + hosted image is build 10.0.26200 rather than the declared 10.0.26100 floor (`osBuild: false`) in + 4m41s. This retains E-M3-WINDOWS-ARM64-BASELINE-CI-RED-001 and is not a verifier regression. +- Other exact-head controls: PR Checks + [29387668227](https://github.com/stablyai/orca/actions/runs/29387668227) passed in about 13m27s, + including lint, reliability/max-lines, typecheck, Git 2.25 compatibility, full tests, unpacked app, + and packaged-CLI smoke. Golden E2E + [29387668205](https://github.com/stablyai/orca/actions/runs/29387668205) passed Linux/macOS in about + 4m49s/5m18s. +- Oracle proved: both native shell families parse and execute the exact final-tree/signer-policy + contract on every target-native runner while all prior artifact, bundled-Node, PTY/watcher, + clean-build equality, oldest-userland, packaging, and E2E controls retain their expected outcome. +- Does not prove: a real final signed Windows runtime, SignPath invocation or returned Orca bytes, + target-native `Valid` status for those returned bytes, Defender/WDAC, exact Windows arm64 build + 26100, Apple signing/trust, release aggregation, SSH transfer/install, packaged desktop use, + fallback/performance, or an enabled tuple. +- Follow-up: keep native signing/trust and every tuple unchecked. Prove exact official-Node and + preserved-upstream source signatures target-natively without credentials, then keep real returned + SignPath bytes plus Defender/WDAC and exact-floor execution as separate required gates. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 6ef2943dedd0165ae4b84d8295763aa06682d6cc Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 00:18:03 -0400 Subject: [PATCH 101/267] build(ssh): verify preserved Windows signatures Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 31 ++ ...runtime-windows-signature-verification.mjs | 10 +- ...-windows-source-signature-verification.mjs | 192 +++++++++++ ...ows-source-signature-verification.test.mjs | 326 ++++++++++++++++++ .../ssh-relay-runtime-workflow.test.mjs | 21 ++ 5 files changed, 578 insertions(+), 2 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-windows-source-signature-verification.mjs create mode 100644 config/scripts/ssh-relay-runtime-windows-source-signature-verification.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 5b595170296..4362b6ed781 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -141,6 +141,8 @@ jobs: node --check config/scripts/ssh-relay-runtime-macos-signature-verification.test.mjs node --check config/scripts/ssh-relay-runtime-windows-signature-verification.mjs 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 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 \ @@ -161,6 +163,7 @@ jobs: config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs \ 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-closure.test.mjs \ config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ @@ -472,6 +475,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime Windows signature verification script syntax check failed' } node --check config/scripts/ssh-relay-runtime-windows-signature-verification.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime Windows signature verification test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-windows-source-signature-verification.mjs + 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' } 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 ` @@ -492,6 +499,7 @@ jobs: config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs ` 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-closure.test.mjs ` config/scripts/ssh-relay-runtime-pty-smoke.test.mjs ` config/scripts/ssh-relay-runtime-reproducibility.test.mjs ` @@ -639,6 +647,28 @@ jobs: [System.Text.UTF8Encoding]::new($false) ) $reportJson + $sourceSignatureReport = Join-Path $outputRoot 'orca-ssh-relay-runtime-${{ matrix.tuple }}.source-signatures.json' + $sourceSignatureLines = & node config/scripts/ssh-relay-runtime-windows-source-signature-verification.mjs ` + --identity (Join-Path $firstOutput 'orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json') ` + --runtime-directory (Join-Path $firstOutput 'runtime') ` + --signing-report $signingReport + if ($LASTEXITCODE -ne 0) { throw 'runtime Windows source signature verification failed' } + $sourceSignatureJson = $sourceSignatureLines -join [Environment]::NewLine + $sourceSignature = $sourceSignatureJson | ConvertFrom-Json + if ( + $sourceSignature.tupleId -ne '${{ matrix.tuple }}' -or + @($sourceSignature.verifiedFiles).Count -ne 3 -or + @($sourceSignature.verifiedFiles | Where-Object { $_.signerKind -eq 'official-node' }).Count -ne 1 -or + @($sourceSignature.verifiedFiles | Where-Object { $_.signerKind -eq 'preserved-upstream' }).Count -ne 2 + ) { + throw 'Windows source signature report violates the immutable/preserved trust contract' + } + [System.IO.File]::WriteAllText( + $sourceSignatureReport, + "$sourceSignatureJson`n", + [System.Text.UTF8Encoding]::new($false) + ) + $sourceSignatureJson Remove-Item -LiteralPath $signingStage -Recurse -Force $evidence = Join-Path $env:GITHUB_WORKSPACE 'runtime-evidence/${{ matrix.tuple }}' New-Item -ItemType Directory -Path $evidence -Force | Out-Null @@ -648,6 +678,7 @@ jobs: Where-Object { $_.Name -match '\.(?:spdx|provenance|identity)\.json$' } | Copy-Item -Destination $evidence Copy-Item -LiteralPath $signingReport -Destination $evidence + Copy-Item -LiteralPath $sourceSignatureReport -Destination $evidence - name: Upload unpublished artifact evidence uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 diff --git a/config/scripts/ssh-relay-runtime-windows-signature-verification.mjs b/config/scripts/ssh-relay-runtime-windows-signature-verification.mjs index ae7f5f37d1b..0531ffa27b0 100644 --- a/config/scripts/ssh-relay-runtime-windows-signature-verification.mjs +++ b/config/scripts/ssh-relay-runtime-windows-signature-verification.mjs @@ -114,7 +114,13 @@ function assertSignerPolicy(signature, signerKind, expectedSigner) { return classified } -async function verifySignedFile({ path, entry, signerKind, expectedSigner, spawnSyncImpl }) { +export async function verifySshRelayRuntimeWindowsSignatureTarget({ + path, + entry, + signerKind, + expectedSigner, + spawnSyncImpl = spawnSync +}) { const metadata = await lstat(path) if (metadata.isSymbolicLink() || !metadata.isFile()) { throw new Error(`Runtime Windows signature target is not a regular file: ${entry.path}`) @@ -191,7 +197,7 @@ export async function verifySshRelayRuntimeWindowsSignatures({ const verifiedFiles = [] for (const target of verificationTargets(finalIdentity, selection)) { verifiedFiles.push( - await verifySignedFile({ + await verifySshRelayRuntimeWindowsSignatureTarget({ path: localPath(physicalRoot, target.entry.path), ...target, spawnSyncImpl diff --git a/config/scripts/ssh-relay-runtime-windows-source-signature-verification.mjs b/config/scripts/ssh-relay-runtime-windows-source-signature-verification.mjs new file mode 100644 index 00000000000..39eedb6cfdd --- /dev/null +++ b/config/scripts/ssh-relay-runtime-windows-source-signature-verification.mjs @@ -0,0 +1,192 @@ +import { spawnSync } from 'node:child_process' +import { lstat, readFile, realpath } from 'node:fs/promises' +import { resolve } from 'node:path' +import { pathToFileURL } from 'node:url' +import { isDeepStrictEqual } from 'node:util' + +import { readSshRelayRuntimeNativeSigningIdentity } from './ssh-relay-runtime-native-signing-plan.mjs' +import { + assertSshRelayRuntimeNativeSigningSelection, + buildSshRelayRuntimeNativeSigningSelection +} from './ssh-relay-runtime-native-signing-selection.mjs' +import { verifySshRelayRuntimeWindowsSignatureTarget } from './ssh-relay-runtime-windows-signature-verification.mjs' +import { verifyRuntimeTree } from './verify-ssh-relay-runtime.mjs' + +const MAX_SIGNING_REPORT_BYTES = 4 * 1024 * 1024 +const REPORT_FIELDS = [ + 'assessments', + 'immutableVendorFiles', + 'payload', + 'platform', + 'policy', + 'preservedUpstreamFiles', + 'signingFiles', + 'tupleId' +] +const ARGUMENT_FIELDS = new Map([ + ['--identity', 'identityPath'], + ['--runtime-directory', 'runtimeRoot'], + ['--signing-report', 'signingReportPath'] +]) + +function assertExactFields(value, expected, label) { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`Runtime Windows source signature ${label} must be an object`) + } + const actual = Object.keys(value).sort() + if ( + actual.length !== expected.length || + actual.some((field, index) => field !== expected[index]) + ) { + throw new Error(`Runtime Windows source signature ${label} has unexpected fields`) + } +} + +export function parseSshRelayRuntimeWindowsSourceSignatureArguments(argv) { + const result = {} + for (let index = 0; index < argv.length; index += 2) { + const flag = argv[index] + const field = ARGUMENT_FIELDS.get(flag) + const value = argv[index + 1] + if (!field) { + throw new Error(`Unknown runtime Windows source signature argument: ${flag}`) + } + if (result[field]) { + throw new Error(`Duplicate runtime Windows source signature argument: ${flag}`) + } + if (!value || value.startsWith('--')) { + throw new Error(`Runtime Windows source signature ${flag} requires a value`) + } + result[field] = resolve(value) + } + for (const field of ARGUMENT_FIELDS.values()) { + if (!result[field]) { + throw new Error(`Missing required runtime Windows source signature argument: ${field}`) + } + } + return result +} + +export async function readSshRelayRuntimeWindowsSigningStageReport(path) { + const metadata = await lstat(path) + if (metadata.isSymbolicLink() || !metadata.isFile() || metadata.size > MAX_SIGNING_REPORT_BYTES) { + throw new Error('Runtime Windows signing-stage report must be one bounded regular file') + } + const source = await readFile(path, 'utf8') + try { + return JSON.parse(source) + } catch (error) { + throw new Error(`Runtime Windows signing-stage report is not valid JSON: ${error.message}`) + } +} + +export function selectionFromSshRelayRuntimeWindowsSigningStageReport(identity, report) { + assertExactFields(report, REPORT_FIELDS, 'signing-stage report') + if (!report.payload || typeof report.payload !== 'object' || Array.isArray(report.payload)) { + throw new Error('Runtime Windows signing-stage report has malformed payload evidence') + } + const selection = buildSshRelayRuntimeNativeSigningSelection(identity, report.assessments) + const reportSelection = { + tupleId: report.tupleId, + platform: report.platform, + policy: report.policy, + immutableVendorFiles: report.immutableVendorFiles, + signingFiles: report.signingFiles, + preservedUpstreamFiles: report.preservedUpstreamFiles + } + const expected = { + tupleId: selection.tupleId, + platform: selection.platform, + policy: selection.policy, + immutableVendorFiles: selection.immutableVendorFiles, + signingFiles: selection.signingFiles, + preservedUpstreamFiles: selection.preservedUpstreamFiles + } + if (!isDeepStrictEqual(reportSelection, expected)) { + // Why: retained signature evidence must describe the same hash-bound selection used for staging. + throw new Error('Runtime Windows signing-stage report and authenticated selection disagree') + } + return selection +} + +function localPath(root, portablePath) { + return resolve(root, ...portablePath.split('/')) +} + +export async function verifySshRelayRuntimeWindowsSourceSignatures({ + runtimeRoot, + identity, + selection, + platform = process.platform, + spawnSyncImpl = spawnSync +}) { + if (platform !== 'win32') { + throw new Error('Runtime Windows source signature verification requires Windows') + } + assertSshRelayRuntimeNativeSigningSelection(identity, selection) + const rootMetadata = await lstat(resolve(runtimeRoot)) + if (rootMetadata.isSymbolicLink() || !rootMetadata.isDirectory()) { + throw new Error('Runtime Windows source signature verification requires a real runtime root') + } + const physicalRoot = await realpath(resolve(runtimeRoot)) + // Why: native trust probes run only after the entire unsigned source tree matches its identity. + await verifyRuntimeTree(physicalRoot, identity) + + const sourceFiles = new Map( + identity.entries.filter((entry) => entry.type === 'file').map((entry) => [entry.path, entry]) + ) + const targets = [ + ...selection.immutableVendorFiles.map((expectedSigner) => ({ + expectedSigner, + signerKind: 'official-node' + })), + ...selection.preservedUpstreamFiles.map((expectedSigner) => ({ + expectedSigner, + signerKind: 'preserved-upstream' + })) + ] + const verifiedFiles = [] + for (const target of targets) { + const entry = sourceFiles.get(target.expectedSigner.path) + if (!entry) { + throw new Error('Runtime Windows source signature target is missing from its identity') + } + verifiedFiles.push( + await verifySshRelayRuntimeWindowsSignatureTarget({ + path: localPath(physicalRoot, entry.path), + entry, + ...target, + spawnSyncImpl + }) + ) + } + return { + tupleId: identity.tupleId, + sourceContentId: identity.contentId, + verifiedFiles + } +} + +async function main() { + const options = parseSshRelayRuntimeWindowsSourceSignatureArguments(process.argv.slice(2)) + const [identity, report] = await Promise.all([ + readSshRelayRuntimeNativeSigningIdentity(options.identityPath), + readSshRelayRuntimeWindowsSigningStageReport(options.signingReportPath) + ]) + const selection = selectionFromSshRelayRuntimeWindowsSigningStageReport(identity, report) + const result = await verifySshRelayRuntimeWindowsSourceSignatures({ + runtimeRoot: options.runtimeRoot, + identity, + selection + }) + 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 Windows source signature verification failed: ${error.stack ?? error}\n` + ) + process.exitCode = 1 + }) +} diff --git a/config/scripts/ssh-relay-runtime-windows-source-signature-verification.test.mjs b/config/scripts/ssh-relay-runtime-windows-source-signature-verification.test.mjs new file mode 100644 index 00000000000..b31269cfb90 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-windows-source-signature-verification.test.mjs @@ -0,0 +1,326 @@ +import { createHash } from 'node:crypto' +import { appendFileSync } from 'node:fs' +import { appendFile, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + +import { describe, expect, it } from 'vitest' + +import { expectedSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' +import { buildSshRelayRuntimeNativeSigningPlan } from './ssh-relay-runtime-native-signing-plan.mjs' +import { buildSshRelayRuntimeNativeSigningSelection } from './ssh-relay-runtime-native-signing-selection.mjs' +import { + parseSshRelayRuntimeWindowsSourceSignatureArguments, + readSshRelayRuntimeWindowsSigningStageReport, + selectionFromSshRelayRuntimeWindowsSigningStageReport, + verifySshRelayRuntimeWindowsSourceSignatures +} from './ssh-relay-runtime-windows-source-signature-verification.mjs' +import { OFFICIAL_NODE_WINDOWS_SIGNER_SUBJECT } from './ssh-relay-runtime-windows-signature-verification.mjs' + +const MICROSOFT_SUBJECT = + 'CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US' + +function digest(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +async function sourceFixture() { + const root = await mkdtemp(join(tmpdir(), 'ssh-relay-windows-source-signature-')) + const runtimeRoot = join(root, 'runtime') + await mkdir(runtimeRoot) + const entries = [] + for (const entry of expectedSshRelayRuntimeClosureEntries('win32-x64')) { + if (entry.type === 'directory') { + await mkdir(join(runtimeRoot, ...entry.path.split('/')), { + recursive: true, + mode: entry.mode + }) + entries.push(entry) + continue + } + const bytes = Buffer.from(`fixture:${entry.path}`) + const path = join(runtimeRoot, ...entry.path.split('/')) + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, bytes, { mode: entry.mode }) + entries.push({ ...entry, size: bytes.length, sha256: digest(bytes) }) + } + const base = { + identitySchemaVersion: 1, + tupleId: 'win32-x64', + os: 'win32', + architecture: 'x64', + compatibility: sshRelayRuntimeCompatibility['win32-x64'], + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries + } + const files = entries.filter((entry) => entry.type === 'file') + const identity = { + ...base, + contentId: computeSshRelayRuntimeContentId(base), + fileCount: files.length, + expandedSize: files.reduce((total, entry) => total + entry.size, 0) + } + const assessments = buildSshRelayRuntimeNativeSigningPlan(identity).signingCandidates.map( + (entry, index) => { + const preserved = + entry.path.endsWith('/OpenConsole.exe') || entry.path.endsWith('/conpty.dll') + return preserved + ? { + path: entry.path, + sourceSha256: entry.sourceSha256, + status: 'valid-upstream', + signerSubject: MICROSOFT_SUBJECT, + signerThumbprint: `${index + 1}`.repeat(40) + } + : { path: entry.path, sourceSha256: entry.sourceSha256, status: 'unsigned' } + } + ) + const selection = buildSshRelayRuntimeNativeSigningSelection(identity, assessments) + const report = { + tupleId: selection.tupleId, + platform: selection.platform, + policy: selection.policy, + assessments, + immutableVendorFiles: selection.immutableVendorFiles, + signingFiles: selection.signingFiles, + preservedUpstreamFiles: selection.preservedUpstreamFiles, + payload: { stagingRequired: true } + } + const reportPath = join(root, 'signing-stage.json') + await writeFile(reportPath, JSON.stringify(report)) + return { root, runtimeRoot, identity, assessments, selection, report, reportPath } +} + +function entryForPath(selection, physicalPath) { + return [...selection.immutableVendorFiles, ...selection.preservedUpstreamFiles].find((entry) => + physicalPath.endsWith(join(...entry.path.split('/'))) + ) +} + +function signatureJson(subject, thumbprint) { + return JSON.stringify({ status: 'Valid', signerSubject: subject, signerThumbprint: thumbprint }) +} + +function successfulAuthenticode(selection, calls, override = () => undefined) { + return (command, args, options) => { + calls.push({ command, args, options }) + const entry = entryForPath(selection, options.env.ORCA_SSH_RELAY_AUTHENTICODE_FILE) + const overridden = override(entry) + if (overridden) { + return { status: 0, stdout: JSON.stringify(overridden), stderr: '' } + } + return { + status: 0, + stdout: + entry.action === 'preserve-exact-bytes' + ? signatureJson(OFFICIAL_NODE_WINDOWS_SIGNER_SUBJECT, 'A'.repeat(40)) + : signatureJson(entry.signerSubject, entry.signerThumbprint), + stderr: '' + } + } +} + +describe('SSH relay runtime Windows source signature verification', () => { + it('parses exact CLI arguments and authenticates the signing-stage report', async () => { + expect( + parseSshRelayRuntimeWindowsSourceSignatureArguments([ + '--identity', + 'identity.json', + '--runtime-directory', + 'runtime', + '--signing-report', + 'report.json' + ]) + ).toEqual({ + identityPath: expect.stringMatching(/identity\.json$/u), + runtimeRoot: expect.stringMatching(/runtime$/u), + signingReportPath: expect.stringMatching(/report\.json$/u) + }) + expect(() => + parseSshRelayRuntimeWindowsSourceSignatureArguments(['--identity', 'identity.json']) + ).toThrow(/missing required/i) + + const fixture = await sourceFixture() + try { + const report = await readSshRelayRuntimeWindowsSigningStageReport(fixture.reportPath) + expect( + selectionFromSshRelayRuntimeWindowsSigningStageReport(fixture.identity, report) + ).toEqual(fixture.selection) + + report.preservedUpstreamFiles[0].signerThumbprint = '0'.repeat(40) + expect(() => + selectionFromSshRelayRuntimeWindowsSigningStageReport(fixture.identity, report) + ).toThrow(/report and authenticated selection disagree/i) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('verifies the complete source tree before exact Node and preserved signer policy', async () => { + const fixture = await sourceFixture() + const calls = [] + try { + const result = await verifySshRelayRuntimeWindowsSourceSignatures({ + runtimeRoot: fixture.runtimeRoot, + identity: fixture.identity, + selection: fixture.selection, + platform: 'win32', + spawnSyncImpl: successfulAuthenticode(fixture.selection, calls) + }) + + expect(result.tupleId).toBe('win32-x64') + expect(result.verifiedFiles).toHaveLength(3) + expect(result.verifiedFiles.map((entry) => entry.signerKind).sort()).toEqual([ + 'official-node', + 'preserved-upstream', + 'preserved-upstream' + ]) + expect(calls).toHaveLength(3) + expect(calls.every((call) => call.command === 'pwsh')).toBe(true) + expect(calls.every((call) => call.options.timeout === 30_000)).toBe(true) + expect(calls.every((call) => call.options.maxBuffer === 64 * 1024)).toBe(true) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('rejects wrong Node or preserved signer policy and invalid status', async () => { + const fixture = await sourceFixture() + const common = { + runtimeRoot: fixture.runtimeRoot, + identity: fixture.identity, + selection: fixture.selection, + platform: 'win32' + } + try { + await expect( + verifySshRelayRuntimeWindowsSourceSignatures({ + ...common, + spawnSyncImpl: successfulAuthenticode(fixture.selection, [], (entry) => + entry.action === 'preserve-exact-bytes' + ? { + status: 'Valid', + signerSubject: 'CN=Unexpected Node', + signerThumbprint: 'A'.repeat(40) + } + : undefined + ) + }) + ).rejects.toThrow(/official Node signer policy/i) + await expect( + verifySshRelayRuntimeWindowsSourceSignatures({ + ...common, + spawnSyncImpl: successfulAuthenticode(fixture.selection, [], (entry) => + entry.action === 'preserve-valid-upstream' + ? { + status: 'Valid', + signerSubject: entry.signerSubject, + signerThumbprint: '0'.repeat(40) + } + : undefined + ) + }) + ).rejects.toThrow(/preserved upstream signer policy/i) + await expect( + verifySshRelayRuntimeWindowsSourceSignatures({ + ...common, + spawnSyncImpl: () => ({ + status: 0, + stdout: JSON.stringify({ + status: 'NotSigned', + signerSubject: null, + signerThumbprint: null + }), + stderr: '' + }) + }) + ).rejects.toThrow(/requires a valid Authenticode signature/i) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) + + it('rejects tree mutation before probing and source mutation during a probe', async () => { + const fixture = await sourceFixture() + let calls = 0 + try { + await appendFile(join(fixture.runtimeRoot, 'relay.js'), ':mutated') + await expect( + verifySshRelayRuntimeWindowsSourceSignatures({ + runtimeRoot: fixture.runtimeRoot, + identity: fixture.identity, + selection: fixture.selection, + platform: 'win32', + spawnSyncImpl: () => { + calls += 1 + throw new Error('must not spawn') + } + }) + ).rejects.toThrow(/integrity mismatch/i) + expect(calls).toBe(0) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + + const raced = await sourceFixture() + try { + const spawn = successfulAuthenticode(raced.selection, []) + let mutated = false + await expect( + verifySshRelayRuntimeWindowsSourceSignatures({ + runtimeRoot: raced.runtimeRoot, + identity: raced.identity, + selection: raced.selection, + platform: 'win32', + spawnSyncImpl: (command, args, options) => { + if (!mutated) { + mutated = true + appendFileSync(options.env.ORCA_SSH_RELAY_AUTHENTICODE_FILE, ':raced') + } + return spawn(command, args, options) + } + }) + ).rejects.toThrow(/changed during signature verification/i) + } finally { + await rm(raced.root, { recursive: true, force: true }) + } + }) + + it('rejects cross-platform execution and stale preserved-signer evidence', async () => { + const fixture = await sourceFixture() + let calls = 0 + try { + await expect( + verifySshRelayRuntimeWindowsSourceSignatures({ + runtimeRoot: fixture.runtimeRoot, + identity: fixture.identity, + selection: fixture.selection, + platform: 'linux', + spawnSyncImpl: () => { + calls += 1 + throw new Error('must not spawn') + } + }) + ).rejects.toThrow(/requires Windows/i) + expect(calls).toBe(0) + + const stale = structuredClone(fixture.selection) + stale.preservedUpstreamFiles[0].signerThumbprint = '0'.repeat(40) + await expect( + verifySshRelayRuntimeWindowsSourceSignatures({ + runtimeRoot: fixture.runtimeRoot, + identity: fixture.identity, + selection: stale, + platform: 'win32', + spawnSyncImpl: successfulAuthenticode(fixture.selection, []) + }) + ).rejects.toThrow(/preserved upstream signer policy/i) + expect(calls).toBe(0) + } finally { + await rm(fixture.root, { recursive: true, force: true }) + } + }) +}) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 04a6aae1c9a..cd6938bf512 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -95,6 +95,9 @@ 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) expect( source.match(/node --check config\/scripts\/ssh-relay-runtime-native-signing-plan\.mjs/g) ).toHaveLength(2) @@ -125,6 +128,11 @@ 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) @@ -219,7 +227,20 @@ describe('SSH relay runtime artifact workflow', () => { expect(windowsRun).toContain('@($report.preservedUpstreamFiles).Count -ne 2') expect(windowsRun).toContain('Required upstream signature was not preserved') expect(windowsRun).toContain("Join-Path $signingStage 'bin/node.exe'") + expect(windowsRun).toContain('ssh-relay-runtime-windows-source-signature-verification.mjs') + expect( + windowsRun.indexOf('ssh-relay-runtime-windows-source-signature-verification.mjs') + ).toBeGreaterThan(windowsRun.indexOf('ssh-relay-runtime-native-signing-stage.mjs')) + expect( + windowsRun.indexOf('ssh-relay-runtime-windows-source-signature-verification.mjs') + ).toBeLessThan(windowsRun.indexOf('Remove-Item -LiteralPath $signingStage')) + expect(windowsRun).toContain( + 'Windows source signature report violates the immutable/preserved trust contract' + ) + expect(windowsRun).toContain("$_.signerKind -eq 'official-node'") + expect(windowsRun).toContain("$_.signerKind -eq 'preserved-upstream'") expect(source.match(/\.signing-stage\.json/g)).toHaveLength(3) + expect(source.match(/\.source-signatures\.json/g)).toHaveLength(1) expect(source).not.toMatch(/SIGNPATH_|APPLE_(?:ID|KEY)|Developer ID Application/) }) From be32653a7b77eeaed168d91b790fc2e006b438db Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 00:19:15 -0400 Subject: [PATCH 102/267] docs(ssh): record Windows source signature proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 5 +- ...github-release-implementation-checklist.md | 87 ++++++++++++++++++- 2 files changed, 89 insertions(+), 3 deletions(-) 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 ace78dc5881..795025792f6 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 @@ -48,8 +48,9 @@ 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. - - Active next: target-native official-Node and preserved-upstream Windows source-signature policy; - real signing and missing oldest-floor snapshots remain externally gated. + - 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. - 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. 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 82575364f97..ad00982b5a4 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps 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 Windows source-signature policy is the next credential-free 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.
      +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.
      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) @@ -8577,6 +8577,91 @@ diff --check`. preserved-upstream source signatures target-natively without credentials, then keep real returned SignPath bytes plus Defender/WDAC and exact-floor execution as separate required gates. +### E-M3-WINDOWS-SOURCE-SIGNATURE-LOCAL-RED-001 — Missing Windows source-signature gate fails import contract + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: pre-implementation head `bd3e40dc52ede75e44ded2f5e84f5092be5b2d87` plus the new + uncommitted purpose-named test. +- Command: + + ```sh + node --input-type=module -e \ + "import('./config/scripts/ssh-relay-runtime-windows-source-signature-verification.test.mjs')" + ``` + +- Result: expected RED with `ERR_MODULE_NOT_FOUND` for + `ssh-relay-runtime-windows-source-signature-verification.mjs`. A concurrent focused Vitest launch + was stopped after unrelated workspace CPU contention prevented a bounded result; it is not used as + evidence. No workflow, credential, signing, publication, tuple, SSH, or production behavior + changed. +- Oracle proved: neither the pre-sign assessment nor final-runtime verifier already exposed a + reusable gate that authenticated the complete source tree and target-natively verified official + Node plus preserved upstream signer policy. +- Does not prove: implementation, Windows execution, a `Valid` signature, SignPath returns, + Defender/WDAC, SSH behavior, or an enabled tuple. +- Correction: consume the exact signing-stage assessment and selection, authenticate the complete + source tree, run three bounded native probes, require exact Node/preserved identity, re-hash each + target, and retain a separate report from each Windows runner. + +### E-M3-WINDOWS-SOURCE-SIGNATURE-LOCAL-001 — Immutable and preserved Windows source policy passes locally + +- Date: 2026-07-14 +- Owner: Codex implementation owner +- Source: implementation commit `6ef2943dedd0165ae4b84d8295763aa06682d6cc`, based on CI + evidence commit `bd3e40dc52ede75e44ded2f5e84f5092be5b2d87`. +- Runner/remote/network: local macOS 26.2 build 25C56 arm64, Node v26.0.0 and pnpm 10.24.0. No SSH + remote, Windows trust execution, credential, signing service, publication, or production path was + used. Native output was dependency-injected; exact target-native execution remains the next gate. +- Commands: + + ```sh + 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 + pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \ + config/scripts/ssh-relay-runtime-windows-source-signature-verification.test.mjs \ + config/scripts/ssh-relay-runtime-windows-signature-verification.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs + 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 + pnpm exec oxfmt --check \ + .github/workflows/ssh-relay-runtime-artifacts.yml \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + config/scripts/ssh-relay-runtime-windows-signature-verification.mjs \ + config/scripts/ssh-relay-runtime-windows-source-signature-verification.mjs \ + config/scripts/ssh-relay-runtime-windows-source-signature-verification.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 final focused command passed 3 files/17 tests in 4.98 seconds; the complete + purpose-named SSH relay suite passed 31 files/163 tests in 5.16 seconds. Both syntax checks, + typecheck, full lint and reliability/localization/bundled-skill gates, max-lines (355 + grandfathered suppressions and no new bypass), focused formatting, and `git diff --check` passed. + Lint emitted only existing unrelated warnings; local Node 26 emitted the expected repository + Node-24 engine warning. +- Verification oracle: a bounded regular signing-stage report must exactly reproduce the + hash-authenticated tuple/platform/policy, assessments, immutable file, signing set, and preserved + set. On Windows, the verifier authenticates the complete source runtime before any probe, then + reuses the final verifier's exact signer policy and 30-second/64-KiB PowerShell boundary for the + one official Node file and two source-preserved Microsoft files. Every file is hashed before and + after probing. Cross-platform execution, report/selection drift, wrong signer/status/thumbprint, + prior tree mutation, and probe-time mutation fail closed. +- Workflow oracle: both native shell families syntax-check and run the source contract. After the + real Windows first-build assessment, each native Windows job invokes the source verifier before + removing signing staging, requires exactly one `official-node` and two `preserved-upstream` + results, and retains a distinct `.source-signatures.json` evidence file. It has no credentials and + does not invoke SignPath or change runtime bytes. +- Does not prove: target-native x64/arm64 execution, actual `Valid` status or signer outputs, real + SignPath returned Orca files, final Windows trust, Defender/WDAC, build 26100, release aggregation, + SSH transfer/install, fallback/performance, or an enabled tuple. +- Follow-up: push the implementation and evidence, inspect both exact source-signature reports and + runner logs, and keep returned-byte/native-trust boxes unchecked until the separately gated real + SignPath and endpoint checks pass. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 6f13e94f8a7a315ac3da6bb2067b99da8e02a803 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 00:47:46 -0400 Subject: [PATCH 103/267] build(ssh): gate runtime release stages Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 16 + .../ssh-relay-runtime-draft-recovery.mjs | 89 ++++++ .../ssh-relay-runtime-draft-recovery.test.mjs | 80 +++++ .../ssh-relay-runtime-release-stage-gate.mjs | 288 ++++++++++++++++++ ...-relay-runtime-release-stage-gate.test.mjs | 228 ++++++++++++++ .../ssh-relay-runtime-workflow.test.mjs | 17 +- ...elease-implementation-checklist-summary.md | 19 +- ...github-release-implementation-checklist.md | 239 +++++++++++++-- 8 files changed, 937 insertions(+), 39 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-draft-recovery.mjs create mode 100644 config/scripts/ssh-relay-runtime-draft-recovery.test.mjs create mode 100644 config/scripts/ssh-relay-runtime-release-stage-gate.mjs create mode 100644 config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs 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 From 97164470cb9abad670634b9d4f04424bcc8662b5 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 00:53:37 -0400 Subject: [PATCH 104/267] docs(ssh): refresh release DAG checkpoint Co-authored-by: Orca --- ...thub-release-implementation-checklist-summary.md | 10 +++++++--- ...relay-github-release-implementation-checklist.md | 13 +++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) 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 9fe965f8892..d1f5aa262f8 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 @@ -18,9 +18,9 @@ complete a box. - [x] Integrity, security, and corruption failures are designed to fail closed, not fall back. - [x] Legacy removal or a default-on change requires a separate reviewed decision. -## Active work +## Current gates -- [ ] **WP2 — Prove oldest supported baselines and native trust.** +- [ ] **WP2 external gate — Prove oldest supported baselines and native trust.** - Proven: all-six target-native build/equality/smoke/metadata gates and direct payload audit. - Proven: exact-head run [29379227209](https://github.com/stablyai/orca/actions/runs/29379227209) builds both Linux tuples in @@ -60,6 +60,9 @@ complete a box. 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. +- [ ] **WP3 active implementation — Prove disconnected release/signing DAG behavior in exact-head + CI.** Local contracts pass at `6f13e94f8`, which is one commit ahead of PR #8741; no production + workflow, publication, desktop consumer, or tuple is connected. ## Work packages, in required order @@ -97,7 +100,8 @@ complete a box. - [ ] **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. + `E-M4-RELEASE-DAG-LOCAL-001` at local commit `6f13e94f8`; the commit is one ahead of the PR and + 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 3c6a88372f7..1cf08da951c 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 @@ -9,7 +9,7 @@ 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 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.
      +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — local implementation commit `6f13e94f8a7a315ac3da6bb2067b99da8e02a803` is one commit ahead of the PR head and closes the disconnected release-stage and draft-recovery contracts locally under E-M4-RELEASE-DAG-LOCAL-001. Exact-head CI for that commit is the active gate; push and GitHub-runner evidence collection are now authorized. The current PR head remains `be32653a7`: artifact jobs 87267322867 and 87267322870 in run [29388734922](https://github.com/stablyai/orca/actions/runs/29388734922) passed target-native Windows source-signature verification under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001, while 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. The artifact run's only failure is the retained Windows arm64 build-26200-versus-26100 floor 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) @@ -57,13 +57,14 @@ same change as the work it records. - Implementation status: Work Package 0 is committed at `c4259d94f` on current `main`, with refreshed focused, static, full-lint, live #8450 SSH/PTY, and GitHub Actions proof (E-M0-UNIT-002, E-M0-STATIC-002, E-M0-LIVE-002, E-M0-CI-001). Draft PR - [#8724](https://github.com/stablyai/orca/pull/8724) is open and CI-green at implementation head - `94e58d83e`. + [#8724](https://github.com/stablyai/orca/pull/8724) is open and CI-green at current PR head + `9a8f98fd9`; the implementation evidence remains anchored at `94e58d83e`. - Completed package: Work Package 1's disconnected manifest, content-identity, signature, release-asset, and conservative selector contracts are locally and CI-green under E-M2-RED-001, 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. + [#8728](https://github.com/stablyai/orca/pull/8728), which is CI-green at current PR head + `0c299fe18`; implementation evidence remains anchored at `b9d80a4cb`. No deploy/resolver call site + is connected and no tuple is enabled. - 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 @@ -8775,7 +8776,7 @@ diff --check`. - Date: 2026-07-15 - Owner: Codex implementation owner -- Source: uncommitted implementation based on exact head +- Source: exact implementation commit `6f13e94f8a7a315ac3da6bb2067b99da8e02a803`, based on `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 From e995186545d4b584840cb7ec8cd1403fbd045a7b Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 01:30:16 -0400 Subject: [PATCH 105/267] build(ssh): verify runtime release bytes Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 16 ++ .../ssh-relay-runtime-aggregate-input.mjs | 150 ++++++++++ ...ssh-relay-runtime-aggregate-input.test.mjs | 114 ++++++++ .../ssh-relay-runtime-draft-readback.mjs | 256 ++++++++++++++++++ .../ssh-relay-runtime-draft-readback.test.mjs | 212 +++++++++++++++ .../ssh-relay-runtime-workflow.test.mjs | 4 +- ...elease-implementation-checklist-summary.md | 20 +- ...github-release-implementation-checklist.md | 173 +++++++++++- 8 files changed, 923 insertions(+), 22 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-aggregate-input.mjs create mode 100644 config/scripts/ssh-relay-runtime-aggregate-input.test.mjs create mode 100644 config/scripts/ssh-relay-runtime-draft-readback.mjs create mode 100644 config/scripts/ssh-relay-runtime-draft-readback.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 2a522a034a2..09293f303d6 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -147,6 +147,10 @@ jobs: 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 + node --check config/scripts/ssh-relay-runtime-aggregate-input.mjs + node --check config/scripts/ssh-relay-runtime-aggregate-input.test.mjs + node --check config/scripts/ssh-relay-runtime-draft-readback.mjs + node --check config/scripts/ssh-relay-runtime-draft-readback.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 \ @@ -170,6 +174,8 @@ jobs: 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-aggregate-input.test.mjs \ + config/scripts/ssh-relay-runtime-draft-readback.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 \ @@ -493,6 +499,14 @@ jobs: 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' } + node --check config/scripts/ssh-relay-runtime-aggregate-input.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime aggregate input syntax check failed' } + node --check config/scripts/ssh-relay-runtime-aggregate-input.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime aggregate input test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-draft-readback.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime draft read-back syntax check failed' } + node --check config/scripts/ssh-relay-runtime-draft-readback.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime draft read-back 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 ` @@ -516,6 +530,8 @@ jobs: 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-aggregate-input.test.mjs ` + config/scripts/ssh-relay-runtime-draft-readback.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-aggregate-input.mjs b/config/scripts/ssh-relay-runtime-aggregate-input.mjs new file mode 100644 index 00000000000..f1410553d15 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-aggregate-input.mjs @@ -0,0 +1,150 @@ +import { createHash } from 'node:crypto' +import { createReadStream } from 'node:fs' +import { lstat, readdir } from 'node:fs/promises' +import { basename, join, resolve } from 'node:path' + +import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' + +const DIGEST_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 MAX_TUPLES = 8 +const AGGREGATE_TIMEOUT_MS = 15 * 60_000 +const ASSET_FIELDS = ['tupleId', 'name', 'contentId', 'sha256', 'size'] + +function assertObject(value, label) { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`SSH relay runtime aggregate ${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 aggregate ${label} has unexpected or missing fields`) + } +} + +function expectedArchiveName(tupleId, contentId) { + const digest = contentId.slice('sha256:'.length) + const extension = tupleId.startsWith('win32-') ? 'zip' : 'tar.xz' + return `orca-ssh-relay-runtime-v1-${tupleId}-${digest}.${extension}` +} + +function normalizeAssets(assets) { + if (!Array.isArray(assets) || assets.length === 0 || assets.length > MAX_TUPLES) { + throw new Error('SSH relay runtime aggregate assets must be a bounded non-empty array') + } + const tupleIds = new Set() + const names = new Set() + return assets.map((asset, index) => { + assertExactFields(asset, ASSET_FIELDS, `asset ${index}`) + if (!Object.hasOwn(sshRelayRuntimeCompatibility, asset.tupleId)) { + throw new Error(`SSH relay runtime aggregate has an unsupported tuple: ${asset.tupleId}`) + } + if (tupleIds.has(asset.tupleId)) { + throw new Error(`SSH relay runtime aggregate has a duplicate tuple: ${asset.tupleId}`) + } + tupleIds.add(asset.tupleId) + if (typeof asset.name !== 'string' || !ASSET_NAME_PATTERN.test(asset.name)) { + throw new Error('SSH relay runtime aggregate asset has an invalid name') + } + if (names.has(asset.name)) { + throw new Error(`SSH relay runtime aggregate has a duplicate asset: ${asset.name}`) + } + names.add(asset.name) + if (!DIGEST_PATTERN.test(asset.contentId)) { + throw new Error('SSH relay runtime aggregate asset has an invalid content identity') + } + if (!DIGEST_PATTERN.test(asset.sha256)) { + throw new Error('SSH relay runtime aggregate asset has an invalid SHA-256') + } + if (!Number.isSafeInteger(asset.size) || asset.size <= 0 || asset.size > MAX_ARCHIVE_BYTES) { + throw new Error('SSH relay runtime aggregate asset has an invalid size') + } + if (asset.name !== expectedArchiveName(asset.tupleId, asset.contentId)) { + throw new Error(`SSH relay runtime aggregate archive name is inconsistent: ${asset.name}`) + } + return { ...asset } + }) +} + +function sameFileState(before, after) { + return ( + before.dev === after.dev && + before.ino === after.ino && + before.size === after.size && + before.mtimeNs === after.mtimeNs && + before.ctimeNs === after.ctimeNs + ) +} + +async function hashArchive(path, asset, signal) { + signal?.throwIfAborted() + const before = await lstat(path, { bigint: true }) + if (!before.isFile() || before.isSymbolicLink()) { + throw new Error(`SSH relay runtime aggregate input is not a regular file: ${asset.name}`) + } + if (before.size !== BigInt(asset.size)) { + throw new Error(`SSH relay runtime aggregate input size mismatch: ${asset.name}`) + } + const hash = createHash('sha256') + let bytes = 0 + for await (const chunk of createReadStream(path, { signal })) { + signal?.throwIfAborted() + bytes += chunk.length + if (bytes > asset.size) { + throw new Error(`SSH relay runtime aggregate input exceeded its size: ${asset.name}`) + } + hash.update(chunk) + } + const after = await lstat(path, { bigint: true }) + // Why: aggregate identity must describe one stable file, not bytes swapped during hashing. + if (!sameFileState(before, after)) { + throw new Error(`SSH relay runtime aggregate input changed while hashing: ${asset.name}`) + } + if (bytes !== asset.size) { + throw new Error(`SSH relay runtime aggregate input size mismatch: ${asset.name}`) + } + const digest = `sha256:${hash.digest('hex')}` + if (digest !== asset.sha256) { + throw new Error(`SSH relay runtime aggregate input SHA-256 mismatch: ${asset.name}`) + } +} + +export async function verifySshRelayRuntimeAggregateInputs({ inputDirectory, assets, signal }) { + const effectiveSignal = signal + ? AbortSignal.any([signal, AbortSignal.timeout(AGGREGATE_TIMEOUT_MS)]) + : AbortSignal.timeout(AGGREGATE_TIMEOUT_MS) + effectiveSignal.throwIfAborted() + if (typeof inputDirectory !== 'string' || inputDirectory.length === 0) { + throw new Error('SSH relay runtime aggregate input directory is required') + } + const normalized = normalizeAssets(assets) + const root = resolve(inputDirectory) + const rootMetadata = await lstat(root) + if (!rootMetadata.isDirectory() || rootMetadata.isSymbolicLink()) { + throw new Error('SSH relay runtime aggregate input root must be a real directory') + } + const entries = await readdir(root, { withFileTypes: true }) + const actualNames = entries.map((entry) => entry.name).sort() + const expectedNames = normalized.map((asset) => asset.name).sort() + if (JSON.stringify(actualNames) !== JSON.stringify(expectedNames)) { + throw new Error('SSH relay runtime aggregate input directory has missing or unexpected files') + } + for (const entry of entries) { + if (!entry.isFile() || entry.isSymbolicLink()) { + throw new Error(`SSH relay runtime aggregate input is not a regular file: ${entry.name}`) + } + } + for (const asset of normalized) { + const path = join(root, asset.name) + if (basename(path) !== asset.name) { + throw new Error(`SSH relay runtime aggregate asset path is unsafe: ${asset.name}`) + } + await hashArchive(path, asset, effectiveSignal) + } + return normalized +} diff --git a/config/scripts/ssh-relay-runtime-aggregate-input.test.mjs b/config/scripts/ssh-relay-runtime-aggregate-input.test.mjs new file mode 100644 index 00000000000..7dded7d750d --- /dev/null +++ b/config/scripts/ssh-relay-runtime-aggregate-input.test.mjs @@ -0,0 +1,114 @@ +import { mkdtemp, rm, symlink, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { verifySshRelayRuntimeAggregateInputs } from './ssh-relay-runtime-aggregate-input.mjs' + +const temporaryDirectories = [] + +async function fixture() { + const inputDirectory = await mkdtemp(join(tmpdir(), 'orca-runtime-aggregate-')) + temporaryDirectories.push(inputDirectory) + const contentId = `sha256:${'a'.repeat(64)}` + const bytes = Buffer.from('verified runtime bytes') + const name = `orca-ssh-relay-runtime-v1-linux-x64-glibc-${contentId.slice(7)}.tar.xz` + await writeFile(join(inputDirectory, name), bytes) + return { + inputDirectory, + assets: [ + { + tupleId: 'linux-x64-glibc', + name, + contentId, + sha256: 'sha256:d4c28c942ea1505c048b33251b3afb8b7f1ce9c54d629c9e4c8923afd93d9f45', + size: bytes.length + } + ] + } +} + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })) + ) +}) + +describe('SSH relay runtime aggregate inputs', () => { + it('accepts only exact immutable runtime archive bytes', async () => { + const input = await fixture() + + await expect(verifySshRelayRuntimeAggregateInputs(input)).resolves.toEqual(input.assets) + }) + + it('rejects a hash or size mismatch', async () => { + const hash = await fixture() + hash.assets[0].sha256 = `sha256:${'f'.repeat(64)}` + await expect(verifySshRelayRuntimeAggregateInputs(hash)).rejects.toThrow(/sha-?256/i) + + const size = await fixture() + size.assets[0].size += 1 + await expect(verifySshRelayRuntimeAggregateInputs(size)).rejects.toThrow(/size/i) + }) + + it('rejects missing or extra inputs', async () => { + const missing = await fixture() + await rm(join(missing.inputDirectory, missing.assets[0].name)) + await expect(verifySshRelayRuntimeAggregateInputs(missing)).rejects.toThrow(/missing|exact/i) + + const extra = await fixture() + await writeFile(join(extra.inputDirectory, 'unexpected.bin'), 'extra') + await expect(verifySshRelayRuntimeAggregateInputs(extra)).rejects.toThrow(/unexpected|exact/i) + }) + + it.skipIf(process.platform === 'win32')('rejects a linked archive input', async () => { + const input = await fixture() + const path = join(input.inputDirectory, input.assets[0].name) + const target = join(input.inputDirectory, 'target') + await writeFile(target, 'target') + await rm(path) + await symlink(target, path) + + await expect(verifySshRelayRuntimeAggregateInputs(input)).rejects.toThrow(/unexpected|regular/i) + }) + + it('rejects duplicate tuples, names, and unsupported identities', async () => { + const duplicateTuple = await fixture() + duplicateTuple.assets.push({ ...duplicateTuple.assets[0], name: 'different.tar.xz' }) + await expect(verifySshRelayRuntimeAggregateInputs(duplicateTuple)).rejects.toThrow( + /duplicate tuple/i + ) + + const duplicateName = await fixture() + duplicateName.assets.push({ ...duplicateName.assets[0], tupleId: 'linux-arm64-glibc' }) + await expect(verifySshRelayRuntimeAggregateInputs(duplicateName)).rejects.toThrow( + /duplicate asset/i + ) + + const unsupported = await fixture() + unsupported.assets[0].tupleId = 'linux-riscv64-glibc' + await expect(verifySshRelayRuntimeAggregateInputs(unsupported)).rejects.toThrow( + /unsupported tuple/i + ) + }) + + it('derives the archive name from tuple and content identity', async () => { + const input = await fixture() + input.assets[0].name = 'latest.tar.xz' + + await expect(verifySshRelayRuntimeAggregateInputs(input)).rejects.toThrow(/archive name/i) + }) + + it('honors cancellation before reading an archive', async () => { + const input = await fixture() + const controller = new AbortController() + controller.abort(new Error('cancel aggregate')) + + await expect( + verifySshRelayRuntimeAggregateInputs({ ...input, signal: controller.signal }) + ).rejects.toThrow(/cancel aggregate/i) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-draft-readback.mjs b/config/scripts/ssh-relay-runtime-draft-readback.mjs new file mode 100644 index 00000000000..47b14ed2b9d --- /dev/null +++ b/config/scripts/ssh-relay-runtime-draft-readback.mjs @@ -0,0 +1,256 @@ +import { createHash } from 'node:crypto' + +const API_VERSION = '2022-11-28' +const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u +const TAG_PATTERN = /^v\d+\.\d+\.\d+(?:-rc\.\d+(?:\.[0-9A-Za-z]+)?)?$/u +const REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u +const ASSET_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,239}$/u +const MANAGED_ASSET_PREFIX = 'orca-ssh-relay-runtime-' +const RELEASE_ASSET_HOST = 'release-assets.githubusercontent.com' +const MAX_ASSET_BYTES = 100 * 1024 * 1024 +const MAX_TOTAL_BYTES = 1024 * 1024 * 1024 +const MAX_ASSETS = 26 +const READBACK_TIMEOUT_MS = 15 * 60_000 +const ASSET_FIELDS = ['name', 'sha256', 'size'] + +function assertObject(value, label) { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`SSH relay runtime draft read-back ${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 draft read-back ${label} has unexpected or missing fields`) + } +} + +function normalizeExpectedAssets(assets) { + if (!Array.isArray(assets) || assets.length === 0 || assets.length > MAX_ASSETS) { + throw new Error('SSH relay runtime draft read-back assets must be a bounded non-empty array') + } + const names = new Set() + let totalBytes = 0 + const normalized = assets.map((asset, index) => { + assertExactFields(asset, ASSET_FIELDS, `expected asset ${index}`) + if ( + typeof asset.name !== 'string' || + !ASSET_NAME_PATTERN.test(asset.name) || + !asset.name.startsWith(MANAGED_ASSET_PREFIX) + ) { + throw new Error('SSH relay runtime draft read-back expected an invalid managed asset') + } + if (names.has(asset.name)) { + throw new Error(`SSH relay runtime draft read-back has a duplicate asset: ${asset.name}`) + } + names.add(asset.name) + if (typeof asset.sha256 !== 'string' || !DIGEST_PATTERN.test(asset.sha256)) { + throw new Error('SSH relay runtime draft read-back expected an invalid SHA-256') + } + if (!Number.isSafeInteger(asset.size) || asset.size <= 0 || asset.size > MAX_ASSET_BYTES) { + throw new Error('SSH relay runtime draft read-back expected an invalid size') + } + totalBytes += asset.size + return { ...asset } + }) + if (!Number.isSafeInteger(totalBytes) || totalBytes > MAX_TOTAL_BYTES) { + throw new Error('SSH relay runtime draft read-back expected assets exceed the total size limit') + } + return normalized +} + +function authenticatedHeaders(token, accept = 'application/vnd.github+json') { + return { + Accept: accept, + Authorization: `Bearer ${token}`, + 'X-GitHub-Api-Version': API_VERSION + } +} + +async function responseError(response) { + const body = await response.text().catch(() => '') + return new Error( + `SSH relay runtime GitHub request failed ${response.status} ${response.statusText}: ${body.slice(0, 300)}` + ) +} + +async function fetchRelease({ repo, releaseId, token, fetchImpl, signal }) { + const response = await fetchImpl(`https://api.github.com/repos/${repo}/releases/${releaseId}`, { + headers: authenticatedHeaders(token), + redirect: 'error', + signal + }) + if (!response.ok) { + throw await responseError(response) + } + return response.json() +} + +function validateRelease(release, releaseId, tag, expectedAssets) { + assertObject(release, 'release') + if (release.id !== releaseId || release.draft !== true) { + throw new Error('SSH relay runtime release must remain the requested draft during read-back') + } + if (release.tag_name !== tag) { + throw new Error('SSH relay runtime draft read-back tag does not match') + } + if (!Array.isArray(release.assets)) { + throw new Error('SSH relay runtime draft read-back release assets must be an array') + } + const expectedByName = new Map(expectedAssets.map((asset) => [asset.name, asset])) + const releaseByName = new Map() + for (const asset of release.assets) { + assertObject(asset, 'release asset') + if (typeof asset.name !== 'string' || releaseByName.has(asset.name)) { + throw new Error('SSH relay runtime draft read-back has a duplicate or invalid release asset') + } + releaseByName.set(asset.name, asset) + if (asset.name.startsWith(MANAGED_ASSET_PREFIX) && !expectedByName.has(asset.name)) { + throw new Error(`SSH relay runtime draft has an unexpected managed asset: ${asset.name}`) + } + } + for (const expected of expectedAssets) { + const actual = releaseByName.get(expected.name) + if (!actual) { + throw new Error(`SSH relay runtime draft is missing managed asset: ${expected.name}`) + } + if (!Number.isSafeInteger(actual.id) || actual.id <= 0) { + throw new Error(`SSH relay runtime draft asset has an invalid ID: ${expected.name}`) + } + if (actual.state !== 'uploaded') { + throw new Error(`SSH relay runtime draft asset is not uploaded: ${expected.name}`) + } + if (actual.size !== expected.size) { + throw new Error(`SSH relay runtime draft asset size disagrees: ${expected.name}`) + } + } + return releaseByName +} + +function redirectedAssetUrl(response) { + const location = response.headers.get('location') + if (!location) { + throw new Error('SSH relay runtime draft read-back redirect is missing Location') + } + const url = new URL(location) + if (url.protocol !== 'https:') { + throw new Error('SSH relay runtime draft read-back redirect must use HTTPS') + } + if (url.hostname !== RELEASE_ASSET_HOST) { + throw new Error('SSH relay runtime draft read-back redirect has an unapproved origin') + } + return url.href +} + +async function fetchAsset({ repo, assetId, token, fetchImpl, signal }) { + const response = await fetchImpl( + `https://api.github.com/repos/${repo}/releases/assets/${assetId}`, + { + headers: authenticatedHeaders(token, 'application/octet-stream'), + redirect: 'manual', + signal + } + ) + if (response.status !== 302) { + // Why: partial or otherwise unusual successful responses cannot prove the exact release asset. + if (response.status !== 200) { + throw await responseError(response) + } + return response + } + const location = redirectedAssetUrl(response) + // Why: the API request is authenticated, but its signed CDN redirect must never receive the token. + const redirected = await fetchImpl(location, { + headers: { Accept: 'application/octet-stream' }, + redirect: 'error', + signal + }) + if (redirected.status !== 200) { + throw await responseError(redirected) + } + return redirected +} + +async function hashResponse(response, expected, signal) { + const contentLength = response.headers.get('content-length') + if (contentLength !== null) { + const parsed = Number(contentLength) + if (!Number.isSafeInteger(parsed) || parsed !== expected.size) { + throw new Error( + `SSH relay runtime draft read-back Content-Length disagrees: ${expected.name}` + ) + } + } + if (!response.body) { + throw new Error(`SSH relay runtime draft read-back body is missing: ${expected.name}`) + } + const hash = createHash('sha256') + let bytes = 0 + for await (const chunk of response.body) { + signal?.throwIfAborted() + const buffer = Buffer.from(chunk) + bytes += buffer.length + if (bytes > expected.size || bytes > MAX_ASSET_BYTES) { + throw new Error(`SSH relay runtime draft read-back size exceeded: ${expected.name}`) + } + hash.update(buffer) + } + if (bytes !== expected.size) { + throw new Error(`SSH relay runtime draft read-back size mismatch: ${expected.name}`) + } + const digest = `sha256:${hash.digest('hex')}` + if (digest !== expected.sha256) { + throw new Error(`SSH relay runtime draft read-back SHA-256 mismatch: ${expected.name}`) + } +} + +export async function verifySshRelayRuntimeDraftReadback({ + repo, + releaseId, + tag, + token, + expectedAssets, + fetchImpl = fetch, + signal +}) { + const effectiveSignal = signal + ? AbortSignal.any([signal, AbortSignal.timeout(READBACK_TIMEOUT_MS)]) + : AbortSignal.timeout(READBACK_TIMEOUT_MS) + effectiveSignal.throwIfAborted() + if (typeof repo !== 'string' || !REPOSITORY_PATTERN.test(repo)) { + throw new Error('SSH relay runtime draft read-back repository is invalid') + } + if (!Number.isSafeInteger(releaseId) || releaseId <= 0) { + throw new Error('SSH relay runtime draft read-back release ID is invalid') + } + if (typeof tag !== 'string' || !TAG_PATTERN.test(tag)) { + throw new Error('SSH relay runtime draft read-back tag is invalid') + } + if (typeof token !== 'string' || token.length === 0) { + throw new Error('SSH relay runtime draft read-back token is required') + } + const expected = normalizeExpectedAssets(expectedAssets) + const release = await fetchRelease({ + repo, + releaseId, + token, + fetchImpl, + signal: effectiveSignal + }) + const releaseAssets = validateRelease(release, releaseId, tag, expected) + for (const asset of expected) { + effectiveSignal.throwIfAborted() + const response = await fetchAsset({ + repo, + assetId: releaseAssets.get(asset.name).id, + token, + fetchImpl, + signal: effectiveSignal + }) + await hashResponse(response, asset, effectiveSignal) + } + return { releaseId, tag, downloadedAssets: expected } +} diff --git a/config/scripts/ssh-relay-runtime-draft-readback.test.mjs b/config/scripts/ssh-relay-runtime-draft-readback.test.mjs new file mode 100644 index 00000000000..5903acd5ef2 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-draft-readback.test.mjs @@ -0,0 +1,212 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { verifySshRelayRuntimeDraftReadback } from './ssh-relay-runtime-draft-readback.mjs' + +const TAG = 'v1.4.140-rc.1' +const REPO = 'stablyai/orca' +const TOKEN = 'secret-token' +const bytes = Buffer.from('immutable release bytes') +const expected = { + name: 'orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.xz', + sha256: 'sha256:5396515749878bd28c5dae110040b4fbae1c33f59318b9c23f446319d68e236a', + size: bytes.length +} + +function release(overrides = {}) { + return { + id: 42, + tag_name: TAG, + draft: true, + assets: [{ id: 101, name: expected.name, state: 'uploaded', size: expected.size }], + ...overrides + } +} + +function fetchFixture(assetResponse = new Response(bytes)) { + return vi + .fn() + .mockResolvedValueOnce(Response.json(release())) + .mockResolvedValueOnce(assetResponse) +} + +afterEach(() => vi.restoreAllMocks()) + +describe('SSH relay runtime draft read-back', () => { + it('downloads and hashes every exact managed asset from the same draft', async () => { + const fetchImpl = fetchFixture() + + await expect( + verifySshRelayRuntimeDraftReadback({ + repo: REPO, + releaseId: 42, + tag: TAG, + token: TOKEN, + expectedAssets: [expected], + fetchImpl + }) + ).resolves.toEqual({ releaseId: 42, tag: TAG, downloadedAssets: [expected] }) + }) + + it('follows only the approved asset redirect without forwarding authorization', async () => { + const location = 'https://release-assets.githubusercontent.com/example/runtime?sig=signed' + const fetchImpl = vi + .fn() + .mockResolvedValueOnce(Response.json(release())) + .mockResolvedValueOnce(new Response(null, { status: 302, headers: { location } })) + .mockResolvedValueOnce(new Response(bytes)) + + await verifySshRelayRuntimeDraftReadback({ + repo: REPO, + releaseId: 42, + tag: TAG, + token: TOKEN, + expectedAssets: [expected], + fetchImpl + }) + + expect(fetchImpl.mock.calls[1][1].headers.Authorization).toBe(`Bearer ${TOKEN}`) + expect(fetchImpl.mock.calls[2][0]).toBe(location) + expect(fetchImpl.mock.calls[2][1].headers).not.toHaveProperty('Authorization') + }) + + it.each([ + ['http://release-assets.githubusercontent.com/runtime', 'HTTPS'], + ['https://example.com/runtime', 'origin'] + ])('rejects an unsafe asset redirect %s', async (location, message) => { + const fetchImpl = vi + .fn() + .mockResolvedValueOnce(Response.json(release())) + .mockResolvedValueOnce(new Response(null, { status: 302, headers: { location } })) + + await expect( + verifySshRelayRuntimeDraftReadback({ + repo: REPO, + releaseId: 42, + tag: TAG, + token: TOKEN, + expectedAssets: [expected], + fetchImpl + }) + ).rejects.toThrow(new RegExp(message, 'i')) + }) + + it.each(['direct API response', 'redirected CDN response'])( + 'rejects an unexpected successful status from the %s', + async (responseKind) => { + const partialResponse = new Response(bytes, { + status: 206, + headers: { 'content-range': `bytes 0-${bytes.length - 1}/${bytes.length}` } + }) + const fetchImpl = + responseKind === 'direct API response' + ? fetchFixture(partialResponse) + : vi + .fn() + .mockResolvedValueOnce(Response.json(release())) + .mockResolvedValueOnce( + new Response(null, { + status: 302, + headers: { + location: 'https://release-assets.githubusercontent.com/example/runtime' + } + }) + ) + .mockResolvedValueOnce(partialResponse) + + await expect( + verifySshRelayRuntimeDraftReadback({ + repo: REPO, + releaseId: 42, + tag: TAG, + token: TOKEN, + expectedAssets: [expected], + fetchImpl + }) + ).rejects.toThrow(/failed 206/i) + } + ) + + it('rejects changed, truncated, or oversized returned bytes', async () => { + for (const body of [ + Buffer.from('changed release bytes'), + bytes.subarray(1), + Buffer.concat([bytes, bytes]) + ]) { + await expect( + verifySshRelayRuntimeDraftReadback({ + repo: REPO, + releaseId: 42, + tag: TAG, + token: TOKEN, + expectedAssets: [expected], + fetchImpl: fetchFixture(new Response(body)) + }) + ).rejects.toThrow(/size|sha-?256/i) + } + }) + + it('rejects a published, cross-tag, incomplete, or unexpected managed draft', async () => { + for (const changedRelease of [ + release({ draft: false }), + release({ tag_name: 'v1.4.140-rc.2' }), + release({ assets: [] }), + release({ + assets: [ + ...release().assets, + { id: 102, name: 'orca-ssh-relay-runtime-extra.zip', state: 'uploaded', size: 1 } + ] + }) + ]) { + const fetchImpl = vi.fn().mockResolvedValueOnce(Response.json(changedRelease)) + await expect( + verifySshRelayRuntimeDraftReadback({ + repo: REPO, + releaseId: 42, + tag: TAG, + token: TOKEN, + expectedAssets: [expected], + fetchImpl + }) + ).rejects.toThrow(/draft|tag|missing|unexpected/i) + } + }) + + it('rejects non-uploaded metadata or a mismatched declared size before download', async () => { + for (const asset of [ + { ...release().assets[0], state: 'new' }, + { ...release().assets[0], size: expected.size + 1 } + ]) { + const fetchImpl = vi.fn().mockResolvedValueOnce(Response.json(release({ assets: [asset] }))) + await expect( + verifySshRelayRuntimeDraftReadback({ + repo: REPO, + releaseId: 42, + tag: TAG, + token: TOKEN, + expectedAssets: [expected], + fetchImpl + }) + ).rejects.toThrow(/uploaded|size/i) + expect(fetchImpl).toHaveBeenCalledTimes(1) + } + }) + + it('honors cancellation before any GitHub request', async () => { + const controller = new AbortController() + controller.abort(new Error('cancel read-back')) + const fetchImpl = vi.fn() + + await expect( + verifySshRelayRuntimeDraftReadback({ + repo: REPO, + releaseId: 42, + tag: TAG, + token: TOKEN, + expectedAssets: [expected], + fetchImpl, + signal: controller.signal + }) + ).rejects.toThrow(/cancel read-back/i) + expect(fetchImpl).not.toHaveBeenCalled() + }) +}) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 93a773995d5..3eecea74467 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -98,7 +98,9 @@ describe('SSH relay runtime artifact workflow', () => { for (const moduleName of [ 'windows-source-signature-verification', 'release-stage-gate', - 'draft-recovery' + 'draft-recovery', + 'aggregate-input', + 'draft-readback' ]) { const script = `ssh-relay-runtime-${moduleName}` expect(source.split(`${script}.test.mjs`)).toHaveLength(5) diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md index d1f5aa262f8..1cfa41ea32e 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 @@ -60,9 +60,12 @@ complete a box. 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. -- [ ] **WP3 active implementation — Prove disconnected release/signing DAG behavior in exact-head - CI.** Local contracts pass at `6f13e94f8`, which is one commit ahead of PR #8741; no production - workflow, publication, desktop consumer, or tuple is connected. +- [ ] **WP3 active implementation — Add disconnected aggregate-input and authenticated draft + read-back verification.** Release-DAG contracts pass locally and on all six native build jobs in + exact-head run [29390079639](https://github.com/stablyai/orca/actions/runs/29390079639) under + `E-M4-RELEASE-DAG-CI-001`. Aggregate/read-back RED/GREEN, workflow, full SSH-relay, and static + gates pass locally under `E-M4-AGGREGATE-READBACK-LOCAL-001`; exact-head CI is next. No + production workflow, publication, desktop consumer, signing credential, or tuple is connected. ## Work packages, in required order @@ -97,11 +100,12 @@ 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` at local commit `6f13e94f8`; the commit is one ahead of the PR and - exact-head CI is next. +- [x] Release/signing DAG contracts pass locally and on all six native build jobs under + `E-M4-RELEASE-DAG-LOCAL-001` and `E-M4-RELEASE-DAG-CI-001`. +- [ ] **In progress — 2026-07-15, Codex implementation owner:** implement disconnected + aggregate-input and authenticated draft read-back verification before any production workflow, + publication, desktop consumer, or signing credential is connected. Local implementation and + verification pass under `E-M4-AGGREGATE-READBACK-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 1cf08da951c..72bce889119 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 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** — local implementation commit `6f13e94f8a7a315ac3da6bb2067b99da8e02a803` is one commit ahead of the PR head and closes the disconnected release-stage and draft-recovery contracts locally under E-M4-RELEASE-DAG-LOCAL-001. Exact-head CI for that commit is the active gate; push and GitHub-runner evidence collection are now authorized. The current PR head remains `be32653a7`: artifact jobs 87267322867 and 87267322870 in run [29388734922](https://github.com/stablyai/orca/actions/runs/29388734922) passed target-native Windows source-signature verification under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001, while 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. The artifact run's only failure is the retained Windows arm64 build-26200-versus-26100 floor 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.
      +Current phase: Milestone 4 / Work Package 3 credential-free aggregate-input and draft read-back verification — **In progress — 2026-07-15, Codex implementation owner**. The disconnected release/signing DAG failure contracts are closed locally and on all six native build jobs under E-M4-RELEASE-DAG-LOCAL-001 and E-M4-RELEASE-DAG-CI-001. Exact-floor gaps and real native signing/trust remain open external gates, so the active package may validate only immutable aggregate inputs and authenticated draft bytes; it may not publish, connect desktop consumers, use signing credentials, 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 run [29390079639](https://github.com/stablyai/orca/actions/runs/29390079639) executes the two release-DAG suites under Node 24 on all six target-native build jobs at `97164470c`; every build and supplemental userland job passes. The run remains red only because Windows arm64 runner build 26200 is not the required build 26100, after archive/tree/runtime smoke passes in 7,721.7682 ms with 48,504,832-byte RSS. Golden E2E [29390079632](https://github.com/stablyai/orca/actions/runs/29390079632) is green. PR Checks [29390079665](https://github.com/stablyai/orca/actions/runs/29390079665) attempt 1 passed lint, typecheck, Git compatibility, and tests, then hit a TCP reset downloading Electron; authorized attempt 2 job 87272898091 passes the complete verify/package/CLI-smoke job. The disconnected aggregate-input and authenticated draft read-back implementation now passes local RED/GREEN, workflow-wiring, full SSH-relay, type, lint, line-budget, formatting, and diff gates under E-M4-AGGREGATE-READBACK-LOCAL-RED-001 and E-M4-AGGREGATE-READBACK-LOCAL-001; exact-head native CI is next. 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) @@ -65,10 +65,11 @@ same change as the work it records. [#8728](https://github.com/stablyai/orca/pull/8728), which is CI-green at current PR head `0c299fe18`; implementation evidence remains anchored at `b9d80a4cb`. No deploy/resolver call site is connected and no tuple is enabled. -- 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. +- Active package: Work Package 3 credential-free aggregate-input and authenticated draft read-back + verification. It must bind every input and returned byte to exact name, size, SHA-256, tuple, + content identity, and source release without credentials, publication, production workflow wiring, + desktop consumers, or tuple enablement. No Milestone 4 box is checked until the corresponding + implementation 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 @@ -1459,7 +1460,7 @@ Update status and evidence as work begins. Do not combine these into one large b | 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 | +| 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; E-M4-RELEASE-DAG-CI-001 | | 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 | — | @@ -1527,6 +1528,8 @@ focused commands as their scripts/tests are introduced. - [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) +- [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-aggregate-input.test.mjs config/scripts/ssh-relay-runtime-draft-readback.test.mjs` (E-M4-AGGREGATE-READBACK-LOCAL-RED-001, E-M4-AGGREGATE-READBACK-LOCAL-001; local filesystem plus mocked GitHub responses only) +- [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-aggregate-input.test.mjs config/scripts/ssh-relay-runtime-draft-readback.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` (E-M4-AGGREGATE-READBACK-LOCAL-001; static native-job wiring only) ### Commands/scripts that must be added or formally identified @@ -8846,6 +8849,149 @@ diff --check`. credential-free aggregate-input/read-back implementation without connecting publication or a desktop consumer. +### E-M4-RELEASE-DAG-CI-001 — Release-DAG contracts pass all six native build jobs + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact PR head `97164470cb9abad670634b9d4f04424bcc8662b5`; implementation commit + `6f13e94f8a7a315ac3da6bb2067b99da8e02a803`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). +- Runner/network: GitHub-hosted native runners with normal artifact-build egress and no SSH remote, + signing service, release credential, approval environment, draft upload, publication, or desktop + consumer. Exact jobs and resolved images: + - linux-x64-glibc job 87271365779: `ubuntu-24.04`, `ubuntu24` image `20260705.232.1`, X64, + 04:53:59Z–04:58:13Z. + - linux-arm64-glibc job 87271365815: `ubuntu-24.04-arm`, `ubuntu24-arm64` image + `20260714.61.1`, ARM64, 04:54:03Z–04:59:11Z. + - darwin-x64 job 87271365782: `macos-15-intel`, `macos15` image `20260629.0276.1`, X64, + 04:53:59Z–04:59:21Z. + - darwin-arm64 job 87271365773: `macos-15`, `macos15` image `20260706.0213.1`, ARM64, + 04:53:59Z–04:57:05Z. + - win32-x64 job 87271365831: `windows-2022`, `win22` image `20260706.237.1`, X64, + 04:53:59Z–04:59:34Z. + - win32-arm64 job 87271365776: `windows-11-arm`, `win11-arm64` image `20260706.102.1`, ARM64, + 04:53:59Z–05:03:25Z. +- Command: the workflow's `Run runtime artifact contract tests` step syntax-checks both new modules + and tests, then includes + `ssh-relay-runtime-release-stage-gate.test.mjs` and + `ssh-relay-runtime-draft-recovery.test.mjs` in the purpose-named native Vitest command. Inspection + used `gh run view 29390079639 --repo stablyai/orca --job --log` for every job. +- Result: PASS for all six target-native build jobs. Each runner passes the release-stage suite's 15 + tests and draft-recovery suite's 6 tests under Node v24.18.0. The four POSIX jobs report 32 passing + contract files; the two Windows jobs report 33, including the Windows-only PE diagnostic suite. + Both Linux oldest-userland jobs and Windows x64 floor job also pass. Overall workflow conclusion is + the expected `failure` only because downstream Windows arm64 floor job 87272581856 observes build + 26200 instead of required 26100 after the exact runtime passes archive/tree verification, bundled + Node, PTY, watcher, and resource settlement in 7,721.7682 ms with 48,504,832-byte RSS. +- Concurrent regression: Golden E2E run + [29390079632](https://github.com/stablyai/orca/actions/runs/29390079632) passes both macOS job + 87271365717 and Linux job 87271365749. PR Checks run 29390079665 attempt 1 job 87271365846 passes + lint, typecheck, Git compatibility, and tests, then flakes during `Build unpacked app` when a + 125-MB Electron GitHub asset download receives `read: connection reset by peer`. Authorized + attempt 2 job 87272898091 runs from 05:05:54Z to 05:21:00Z and passes the complete verify job, + including unpacked packaging and packaged CLI smoke. +- Oracle proved: the disconnected build → signing → aggregate → upload → read-back failure contract + and same-draft immutable recovery contract parse and execute identically on all six native runner + families, including Windows PowerShell workflow wiring. No platform can omit its required signing + stage, invent a Linux signing stage, exceed the bounded retry/time policy, or accept missing, + duplicate, drifted, failed, cancelled, approval-rejected, or recovered cross-release state. +- Does not prove: real Apple/SignPath signing or approval, target-native trust of returned Orca bytes, + aggregate/manifest generation, manifest signing, GitHub draft upload/read-back, packaged manifest + embedding, SSH transfer/install, fallback, performance, or an enabled tuple. +- Follow-up: implement disconnected aggregate-input and authenticated draft read-back verification, + then execute it on all six native job families. Retain every production/default and tuple state. + +### E-M4-AGGREGATE-READBACK-LOCAL-RED-001 — Aggregate and draft byte verifiers are absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact implementation base `97164470cb9abad670634b9d4f04424bcc8662b5` 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-aggregate-input.test.mjs \ + config/scripts/ssh-relay-runtime-draft-readback.test.mjs + ``` + +- Result: expected RED in 284 ms. Both suites fail before collecting tests with + `ERR_MODULE_NOT_FOUND` for `ssh-relay-runtime-aggregate-input.mjs` and + `ssh-relay-runtime-draft-readback.mjs`. +- Oracle proved: the exact head had only declarative stage/recovery contracts; it had no filesystem + implementation binding aggregate input declarations to stable archive bytes and no authenticated, + bounded draft asset read-back implementation. +- Does not prove: implementation, GitHub API behavior, actual release assets, native runners, + manifest generation/signing, publication, desktop embedding, SSH behavior, or an enabled tuple. +- Correction: implement both byte boundaries as disconnected modules with bounded streaming, + cancellation, exact identity/asset checks, and credential-safe redirect handling. + +### E-M4-AGGREGATE-READBACK-LOCAL-001 — Immutable aggregate and draft read-back bytes pass locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted implementation based on exact head + `97164470cb9abad670634b9d4f04424bcc8662b5`. +- Runner/network: local macOS 26.2 build 25C56 arm64, Node v26.0.0 and pnpm 10.24.0. Aggregate tests + use exclusive local temporary directories. Draft tests use injected WHATWG `Response` objects and + make no network, GitHub API, release, credential, signing-service, SSH, publication, or production + call. The repository emits its expected Node-24 engine warning. +- Commands: + + ```sh + node --check config/scripts/ssh-relay-runtime-aggregate-input.mjs + node --check config/scripts/ssh-relay-runtime-aggregate-input.test.mjs + node --check config/scripts/ssh-relay-runtime-draft-readback.mjs + node --check config/scripts/ssh-relay-runtime-draft-readback.test.mjs + /usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \ + config/scripts/ssh-relay-runtime-aggregate-input.test.mjs \ + config/scripts/ssh-relay-runtime-draft-readback.test.mjs + pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \ + config/scripts/ssh-relay-runtime-aggregate-input.test.mjs \ + config/scripts/ssh-relay-runtime-draft-readback.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 + git diff --check + ``` + +- Result: PASS. Four syntax checks pass. After exact-HTTP-200 hardening, the two purpose-named suites + pass 2 files/17 tests in 295 ms; `/usr/bin/time -l` records 0.89 seconds wall time, + 132,104,192-byte maximum RSS, zero swaps, and 96,277,880-byte peak memory footprint. + Workflow-inclusive focus passes 3 files/23 tests in 410 ms. The complete SSH-relay suite passes 35 + files/201 tests in 6.63 seconds. Typecheck, full + lint/reliability/localization/bundled-skill gates, max-lines (355 grandfathered suppressions and no + new bypass), focused formatting, and `git diff --check` all pass; lint emits only existing unrelated + warnings. +- Aggregate oracle: accepts at most eight declared supported tuples and derives each immutable + archive name from its tuple/content identity. The exclusive input directory must contain exactly + those regular files, with no extras or links; each file is streamed under a 100-MiB/file and + 15-minute bound, and exact size, SHA-256, and stable pre/post file metadata must match before the + declared asset chain is returned. Duplicate tuple/name, unsupported tuple, malformed identity, + cancellation, missing/extra input, or byte drift fails closed. +- Draft read-back oracle: requires one exact draft ID and stable/RC/perf tag, exact managed asset + names, uploaded state, nonzero declared size, at most 100 MiB per asset/1 GiB total, and a 15-minute + bound. It manually handles the authenticated GitHub asset API redirect, accepts only HTTPS + `release-assets.githubusercontent.com`, never forwards authorization to the CDN, requires exact + HTTP 200 from either direct or redirected downloads, streams every body, and requires exact size + and SHA-256. Published/cross-tag/incomplete/unexpected drafts, unsafe redirects, unexpected 2xx + statuses, changed/truncated/oversized bytes, and cancellation fail closed. +- Workflow oracle: both POSIX and Windows artifact jobs syntax-check and execute the two 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 GitHub API/CDN redirect, a real draft or full-size asset, aggregate manifest + generation, Ed25519 signing, native signing/trust, upload/recovery execution, desktop + prerequisites/embedding, SSH transfer/install, fallback, performance, or an enabled tuple. The + local symlink fixture is skipped on Windows, although the shared implementation rejects all + non-regular directory entries and exact-head Windows execution remains required. +- Follow-up: execute both suites on all six native jobs at the exact implementation head, then add + credential-free canonical manifest assembly/signature handoff without production credentials, + publication, or a desktop consumer. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -8903,12 +9049,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -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. +Commit and run the disconnected aggregate-input and authenticated draft read-back verifiers on all +six native jobs at the exact implementation head. If green, record exact job evidence and implement +the next credential-free canonical-manifest assembly/signature-handoff slice without release +publication, production signing credentials, 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 From 87a8e4dc3461ee1c118c223918f36c672749a60b Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 01:31:10 -0400 Subject: [PATCH 106/267] docs(ssh): anchor release byte verification Co-authored-by: Orca --- ...lay-github-release-implementation-checklist-summary.md | 8 +++++--- ...4-ssh-relay-github-release-implementation-checklist.md | 7 ++++--- 2 files changed, 9 insertions(+), 6 deletions(-) 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 1cfa41ea32e..e0d0261fe26 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 @@ -64,8 +64,9 @@ complete a box. read-back verification.** Release-DAG contracts pass locally and on all six native build jobs in exact-head run [29390079639](https://github.com/stablyai/orca/actions/runs/29390079639) under `E-M4-RELEASE-DAG-CI-001`. Aggregate/read-back RED/GREEN, workflow, full SSH-relay, and static - gates pass locally under `E-M4-AGGREGATE-READBACK-LOCAL-001`; exact-head CI is next. No - production workflow, publication, desktop consumer, signing credential, or tuple is connected. + gates pass locally at implementation commit `e99518654` under + `E-M4-AGGREGATE-READBACK-LOCAL-001`; exact-head CI is next. No production workflow, + publication, desktop consumer, signing credential, or tuple is connected. ## Work packages, in required order @@ -105,7 +106,8 @@ complete a box. - [ ] **In progress — 2026-07-15, Codex implementation owner:** implement disconnected aggregate-input and authenticated draft read-back verification before any production workflow, publication, desktop consumer, or signing credential is connected. Local implementation and - verification pass under `E-M4-AGGREGATE-READBACK-LOCAL-001`; exact-head CI is next. + verification pass at `e99518654` under `E-M4-AGGREGATE-READBACK-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 72bce889119..c3c5484fed4 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 @@ -9,7 +9,7 @@ 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 credential-free aggregate-input and draft read-back verification — **In progress — 2026-07-15, Codex implementation owner**. The disconnected release/signing DAG failure contracts are closed locally and on all six native build jobs under E-M4-RELEASE-DAG-LOCAL-001 and E-M4-RELEASE-DAG-CI-001. Exact-floor gaps and real native signing/trust remain open external gates, so the active package may validate only immutable aggregate inputs and authenticated draft bytes; it may not publish, connect desktop consumers, use signing credentials, 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 run [29390079639](https://github.com/stablyai/orca/actions/runs/29390079639) executes the two release-DAG suites under Node 24 on all six target-native build jobs at `97164470c`; every build and supplemental userland job passes. The run remains red only because Windows arm64 runner build 26200 is not the required build 26100, after archive/tree/runtime smoke passes in 7,721.7682 ms with 48,504,832-byte RSS. Golden E2E [29390079632](https://github.com/stablyai/orca/actions/runs/29390079632) is green. PR Checks [29390079665](https://github.com/stablyai/orca/actions/runs/29390079665) attempt 1 passed lint, typecheck, Git compatibility, and tests, then hit a TCP reset downloading Electron; authorized attempt 2 job 87272898091 passes the complete verify/package/CLI-smoke job. The disconnected aggregate-input and authenticated draft read-back implementation now passes local RED/GREEN, workflow-wiring, full SSH-relay, type, lint, line-budget, formatting, and diff gates under E-M4-AGGREGATE-READBACK-LOCAL-RED-001 and E-M4-AGGREGATE-READBACK-LOCAL-001; exact-head native CI is next. 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.
      +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact-head artifact run [29390079639](https://github.com/stablyai/orca/actions/runs/29390079639) executes the two release-DAG suites under Node 24 on all six target-native build jobs at `97164470c`; every build and supplemental userland job passes. The run remains red only because Windows arm64 runner build 26200 is not the required build 26100, after archive/tree/runtime smoke passes in 7,721.7682 ms with 48,504,832-byte RSS. Golden E2E [29390079632](https://github.com/stablyai/orca/actions/runs/29390079632) is green. PR Checks [29390079665](https://github.com/stablyai/orca/actions/runs/29390079665) attempt 1 passed lint, typecheck, Git compatibility, and tests, then hit a TCP reset downloading Electron; authorized attempt 2 job 87272898091 passes the complete verify/package/CLI-smoke job. The disconnected aggregate-input and authenticated draft read-back implementation is committed at `e995186545d4b584840cb7ec8cd1403fbd045a7b` and passes local RED/GREEN, workflow-wiring, full SSH-relay, type, lint, line-budget, formatting, and diff gates under E-M4-AGGREGATE-READBACK-LOCAL-RED-001 and E-M4-AGGREGATE-READBACK-LOCAL-001; push and exact-head native CI are next. 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) @@ -8930,8 +8930,9 @@ diff --check`. - Date: 2026-07-15 - Owner: Codex implementation owner -- Source: uncommitted implementation based on exact head - `97164470cb9abad670634b9d4f04424bcc8662b5`. +- Source: implementation commit `e995186545d4b584840cb7ec8cd1403fbd045a7b`, based on exact prior + head `97164470cb9abad670634b9d4f04424bcc8662b5`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). - Runner/network: local macOS 26.2 build 25C56 arm64, Node v26.0.0 and pnpm 10.24.0. Aggregate tests use exclusive local temporary directories. Draft tests use injected WHATWG `Response` objects and make no network, GitHub API, release, credential, signing-service, SSH, publication, or production From 90da7e14b0383fb5be76f508e59f5582cce5ad3f Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 01:49:37 -0400 Subject: [PATCH 107/267] docs(ssh): record release byte verification CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 23 +++--- ...github-release-implementation-checklist.md | 80 ++++++++++++++++--- 2 files changed, 82 insertions(+), 21 deletions(-) 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 e0d0261fe26..2be123d4f56 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 @@ -60,13 +60,11 @@ complete a box. 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. -- [ ] **WP3 active implementation — Add disconnected aggregate-input and authenticated draft - read-back verification.** Release-DAG contracts pass locally and on all six native build jobs in - exact-head run [29390079639](https://github.com/stablyai/orca/actions/runs/29390079639) under - `E-M4-RELEASE-DAG-CI-001`. Aggregate/read-back RED/GREEN, workflow, full SSH-relay, and static - gates pass locally at implementation commit `e99518654` under - `E-M4-AGGREGATE-READBACK-LOCAL-001`; exact-head CI is next. No production workflow, - publication, desktop consumer, signing credential, or tuple is connected. +- [ ] **WP3 active implementation — Add credential-free canonical manifest assembly and signature + handoff.** Release-DAG and aggregate/read-back contracts pass locally and on all six native + build jobs under `E-M4-RELEASE-DAG-CI-001` and `E-M4-AGGREGATE-READBACK-CI-001`. Exact-head PR + Checks and Golden E2E are green. No production workflow, publication, desktop consumer, signing + credential, or tuple is connected. ## Work packages, in required order @@ -103,11 +101,12 @@ complete a box. - [x] Release/signing DAG contracts pass locally and on all six native build jobs under `E-M4-RELEASE-DAG-LOCAL-001` and `E-M4-RELEASE-DAG-CI-001`. -- [ ] **In progress — 2026-07-15, Codex implementation owner:** implement disconnected - aggregate-input and authenticated draft read-back verification before any production workflow, - publication, desktop consumer, or signing credential is connected. Local implementation and - verification pass at `e99518654` under `E-M4-AGGREGATE-READBACK-LOCAL-001`; exact-head CI is - next. +- [x] Aggregate-input and authenticated draft read-back verification pass locally and on all six + native build jobs under `E-M4-AGGREGATE-READBACK-LOCAL-001` and + `E-M4-AGGREGATE-READBACK-CI-001`. +- [ ] **In progress — 2026-07-15, Codex implementation owner:** add disconnected canonical manifest + assembly and credential-free signing-request/returned-signature verification without production + workflow wiring, publication, signing credentials, desktop consumers, or tuple enablement. - [ ] 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 c3c5484fed4..24dcdf14cf2 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 credential-free aggregate-input and draft read-back verification — **In progress — 2026-07-15, Codex implementation owner**. The disconnected release/signing DAG failure contracts are closed locally and on all six native build jobs under E-M4-RELEASE-DAG-LOCAL-001 and E-M4-RELEASE-DAG-CI-001. Exact-floor gaps and real native signing/trust remain open external gates, so the active package may validate only immutable aggregate inputs and authenticated draft bytes; it may not publish, connect desktop consumers, use signing credentials, 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 run [29390079639](https://github.com/stablyai/orca/actions/runs/29390079639) executes the two release-DAG suites under Node 24 on all six target-native build jobs at `97164470c`; every build and supplemental userland job passes. The run remains red only because Windows arm64 runner build 26200 is not the required build 26100, after archive/tree/runtime smoke passes in 7,721.7682 ms with 48,504,832-byte RSS. Golden E2E [29390079632](https://github.com/stablyai/orca/actions/runs/29390079632) is green. PR Checks [29390079665](https://github.com/stablyai/orca/actions/runs/29390079665) attempt 1 passed lint, typecheck, Git compatibility, and tests, then hit a TCP reset downloading Electron; authorized attempt 2 job 87272898091 passes the complete verify/package/CLI-smoke job. The disconnected aggregate-input and authenticated draft read-back implementation is committed at `e995186545d4b584840cb7ec8cd1403fbd045a7b` and passes local RED/GREEN, workflow-wiring, full SSH-relay, type, lint, line-budget, formatting, and diff gates under E-M4-AGGREGATE-READBACK-LOCAL-RED-001 and E-M4-AGGREGATE-READBACK-LOCAL-001; push and exact-head native CI are next. 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.
      +Current phase: Milestone 4 / Work Package 3 credential-free canonical manifest assembly and signature handoff — **In progress — 2026-07-15, Codex implementation owner**. The disconnected release-DAG and aggregate/read-back byte boundaries are closed locally and on all six native build jobs under E-M4-RELEASE-DAG-LOCAL-001, E-M4-RELEASE-DAG-CI-001, E-M4-AGGREGATE-READBACK-LOCAL-001, and E-M4-AGGREGATE-READBACK-CI-001. Exact-floor gaps and real native signing/trust remain open external gates, so the active package may add only canonical unsigned-manifest assembly plus credential-free signing-request and returned-signature verification; it may not publish, connect desktop consumers, use signing credentials, 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 run [29391666358](https://github.com/stablyai/orca/actions/runs/29391666358) executes aggregate-input and authenticated draft read-back suites under Node 24 on all six target-native build jobs at `87a8e4dc3`; every native build and both Linux supplemental userland jobs pass under E-M4-AGGREGATE-READBACK-CI-001. The run remains red only because Windows arm64 runner build 26200 is not the required build 26100, after archive/tree/Node/PTY/watcher smoke settles in 8,032.7284 ms with 49,922,048-byte RSS. Golden E2E [29391666362](https://github.com/stablyai/orca/actions/runs/29391666362) and PR Checks [29391666360](https://github.com/stablyai/orca/actions/runs/29391666360) are green. The next disconnected package is canonical unsigned-manifest assembly plus credential-free signing-request/returned-signature verification. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, 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) @@ -65,11 +65,15 @@ same change as the work it records. [#8728](https://github.com/stablyai/orca/pull/8728), which is CI-green at current PR head `0c299fe18`; implementation evidence remains anchored at `b9d80a4cb`. No deploy/resolver call site is connected and no tuple is enabled. -- Active package: Work Package 3 credential-free aggregate-input and authenticated draft read-back - verification. It must bind every input and returned byte to exact name, size, SHA-256, tuple, - content identity, and source release without credentials, publication, production workflow wiring, - desktop consumers, or tuple enablement. No Milestone 4 box is checked until the corresponding - implementation and exact tests exist. +- Completed Work Package 3 gate: credential-free aggregate-input and authenticated draft read-back + verification binds each input and returned byte to exact name, size, SHA-256, tuple, content + identity, source draft, approved HTTPS asset origin, and exact HTTP 200 under + E-M4-AGGREGATE-READBACK-LOCAL-001 and E-M4-AGGREGATE-READBACK-CI-001. +- Active package: Work Package 3 credential-free canonical unsigned-manifest assembly and signing + handoff. It may build and validate canonical bytes, construct a bounded signing request, and verify + injected returned signatures without credentials, protected-environment access, publication, + production workflow wiring, desktop consumers, or tuple enablement. No broader Milestone 4 box is + checked until the corresponding implementation 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 @@ -1460,7 +1464,7 @@ Update status and evidence as work begins. Do not combine these into one large b | 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; E-M4-RELEASE-DAG-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-CI-001; E-M4-AGGREGATE-READBACK-CI-001 | | 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 | — | @@ -8989,10 +8993,68 @@ diff --check`. prerequisites/embedding, SSH transfer/install, fallback, performance, or an enabled tuple. The local symlink fixture is skipped on Windows, although the shared implementation rejects all non-regular directory entries and exact-head Windows execution remains required. -- Follow-up: execute both suites on all six native jobs at the exact implementation head, then add +- Follow-up: exact-head native execution is closed by E-M4-AGGREGATE-READBACK-CI-001. Add credential-free canonical manifest assembly/signature handoff without production credentials, publication, or a desktop consumer. +### E-M4-AGGREGATE-READBACK-CI-001 — Aggregate and draft byte verifiers pass all six native jobs + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact PR head `87a8e4dc3461ee1c118c223918f36c672749a60b`; implementation commit + `e995186545d4b584840cb7ec8cd1403fbd045a7b`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). +- Runner/network: GitHub-hosted native runners with normal artifact-build egress. The aggregate tests + use runner-local exclusive temporary directories. Draft read-back tests inject WHATWG `Response` + objects and make no GitHub API, CDN, release, signing-service, SSH, publication, or production call. + Exact jobs, resolved images, and full contract-suite durations: + - linux-x64-glibc job 87276316322: `ubuntu-24.04`, `ubuntu24` image `20260705.232.1`, X64, + Node v24.18.0; 34 files/197 tests in 3.64 s; job 05:31:27Z–05:35:54Z. + - linux-arm64-glibc job 87276316371: `ubuntu-24.04-arm`, `ubuntu24-arm64` image + `20260706.52.2`, ARM64, Node v24.18.0; 34 files/197 tests in 3.67 s; job + 05:31:30Z–05:43:37Z. The digest-pinned builder-image pull took 8m20s; the subsequent two-build + runtime step passed in 2m33s. + - darwin-x64 job 87276316387: `macos-15-intel`, `macos15` image `20260629.0276.1`, X64, + Node v24.18.0; 34 files/197 tests in 17.99 s; job 05:31:28Z–05:37:13Z. + - darwin-arm64 job 87276316336: `macos-15`, `macos15` image `20260706.0213.1`, ARM64, + Node v24.18.0; 34 files/197 tests in 5.08 s; job 05:31:27Z–05:34:29Z. + - win32-x64 job 87276316353: `windows-2022`, `win22` image `20260706.237.1`, X64, + Node v24.18.0; 35 files/193 passing and 8 skipped tests in 9.35 s; job + 05:31:27Z–05:36:57Z. + - win32-arm64 job 87276316340: `windows-11-arm`, `win11-arm64` image `20260706.102.1`, + ARM64, Node v24.18.0; 35 files/193 passing and 8 skipped tests in 10.03 s; job + 05:31:27Z–05:39:57Z. +- Commands: the workflow syntax-checks both modules and tests, then includes + `ssh-relay-runtime-aggregate-input.test.mjs` and + `ssh-relay-runtime-draft-readback.test.mjs` in `Run runtime artifact contract tests`. Evidence + inspection used `gh run view 29391666358 --repo stablyai/orca --log`, purpose-named `rg` filters, + and `gh run view 29391666358 --repo stablyai/orca --job --log`. +- Result: PASS on all six target-native build jobs. Draft read-back passes 10 tests on each runner; + aggregate input passes 7 on each POSIX runner and 6 with the one POSIX-symlink fixture skipped on + each Windows runner. Purpose-suite timings were draft/aggregate: linux x64 66/36 ms, Linux arm64 + 52/37 ms, macOS x64 258/54 ms, macOS arm64 46/22 ms, Windows x64 70/62 ms, and Windows arm64 + 86/49 ms. Shared implementation still rejects non-regular entries on Windows. +- Concurrent regression: both Linux oldest-userland jobs 87277915889 and 87277915838 pass. Windows + x64 oldest-floor job 87277415457 passes. Overall artifact run conclusion is the expected `failure` + only because Windows arm64 oldest-floor job 87277415503 observes build 26200 instead of the exact + required build 26100; before that gate, archive/tree/Node/PTY/watcher smoke settles in 8,032.7284 + ms with 49,922,048-byte RSS. PR Checks run + [29391666360](https://github.com/stablyai/orca/actions/runs/29391666360) job 87276316191 passes the + complete verify/package/CLI-smoke job from 05:31:28Z to 05:46:00Z. Golden E2E run + [29391666362](https://github.com/stablyai/orca/actions/runs/29391666362) passes Linux job + 87276316058 and macOS job 87276316073. +- Oracle proved: exact immutable archive declarations are bound to stable runner-local bytes on all + six native families; draft metadata, authenticated redirect handling, exact HTTP 200, bounded + streaming, cancellation, size, and SHA-256 policies execute consistently under Node 24. The + workflow remains read-only and cannot publish, access a signing environment, or connect a desktop + consumer. +- Does not prove: real GitHub API/CDN behavior, a real/full-size draft asset, canonical manifest + generation, Ed25519 signing, native signing/trust, upload/recovery execution, desktop + prerequisites/embedding, SSH transfer/install, fallback, performance, or an enabled tuple. +- Follow-up: implement credential-free canonical unsigned-manifest assembly plus bounded signing + request and returned-signature verification. Do not connect production credentials, publication, + desktop consumers, or tuple enablement. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 707a9da23e3955bed6ed00ad3afc24b5ab808243 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 02:00:46 -0400 Subject: [PATCH 108/267] build(ssh): align Windows runtime manifest identity Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 8 ++ .../ssh-relay-runtime-compatibility.mjs | 5 +- .../ssh-relay-runtime-compatibility.test.mjs | 27 +++++ .../ssh-relay-runtime-workflow.test.mjs | 1 + config/scripts/ssh-relay-runtime-zip.test.mjs | 2 +- ...elease-implementation-checklist-summary.md | 18 +-- ...github-release-implementation-checklist.md | 107 ++++++++++++++++-- .../ssh/ssh-relay-artifact-consistency.ts | 3 +- .../ssh/ssh-relay-artifact-schema.test.ts | 40 +++++++ 9 files changed, 192 insertions(+), 19 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-compatibility.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 09293f303d6..5f0d8dabf7c 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -108,6 +108,8 @@ jobs: shell: bash run: | set -euo pipefail + node --check config/scripts/ssh-relay-runtime-compatibility.mjs + node --check config/scripts/ssh-relay-runtime-compatibility.test.mjs node --check config/scripts/ssh-relay-runtime-reproducibility.mjs node --check config/scripts/ssh-relay-runtime-reproducibility.test.mjs node --check config/scripts/ssh-relay-runtime-baseline.mjs @@ -152,6 +154,7 @@ jobs: node --check config/scripts/ssh-relay-runtime-draft-readback.mjs node --check config/scripts/ssh-relay-runtime-draft-readback.test.mjs pnpm exec vitest run --config config/vitest.config.ts \ + config/scripts/ssh-relay-runtime-compatibility.test.mjs \ config/scripts/ssh-relay-node-release-verification.test.mjs \ config/scripts/ssh-relay-node-tar-inspection.test.mjs \ config/scripts/ssh-relay-linux-node-gyp-compiler.test.mjs \ @@ -417,6 +420,10 @@ jobs: shell: pwsh run: | $ErrorActionPreference = 'Stop' + node --check config/scripts/ssh-relay-runtime-compatibility.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime compatibility syntax check failed' } + node --check config/scripts/ssh-relay-runtime-compatibility.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime compatibility test syntax check failed' } node --check config/scripts/ssh-relay-runtime-reproducibility.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime reproducibility script syntax check failed' } node --check config/scripts/ssh-relay-runtime-reproducibility.test.mjs @@ -508,6 +515,7 @@ jobs: node --check config/scripts/ssh-relay-runtime-draft-readback.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime draft read-back test syntax check failed' } pnpm exec vitest run --config config/vitest.config.ts ` + config/scripts/ssh-relay-runtime-compatibility.test.mjs ` config/scripts/ssh-relay-node-release-verification.test.mjs ` config/scripts/ssh-relay-node-tar-inspection.test.mjs ` config/scripts/ssh-relay-linux-node-gyp-compiler.test.mjs ` diff --git a/config/scripts/ssh-relay-runtime-compatibility.mjs b/config/scripts/ssh-relay-runtime-compatibility.mjs index 2645a816a18..95002f3e9ae 100644 --- a/config/scripts/ssh-relay-runtime-compatibility.mjs +++ b/config/scripts/ssh-relay-runtime-compatibility.mjs @@ -22,14 +22,15 @@ export const sshRelayRuntimeCompatibility = Object.freeze({ 'darwin-x64': { kind: 'darwin', minimumVersion: '13.5' }, 'darwin-arm64': { kind: 'darwin', minimumVersion: '13.5' }, 'win32-x64': { - kind: 'win32', + // Why: tuple OS uses `win32`, but the signed manifest's compatibility union uses `windows`. + kind: 'windows', minimumBuild: 19045, minimumOpenSshVersion: '8.1p1', minimumPowerShellVersion: '5.1', minimumDotNetFrameworkRelease: 528040 }, 'win32-arm64': { - kind: 'win32', + kind: 'windows', minimumBuild: 26100, minimumOpenSshVersion: '8.1p1', minimumPowerShellVersion: '5.1', diff --git a/config/scripts/ssh-relay-runtime-compatibility.test.mjs b/config/scripts/ssh-relay-runtime-compatibility.test.mjs new file mode 100644 index 00000000000..ed79634b878 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-compatibility.test.mjs @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest' + +import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' + +describe('SSH relay runtime compatibility contract', () => { + it('uses the manifest compatibility discriminator for both Windows tuples', () => { + expect(sshRelayRuntimeCompatibility['win32-x64'].kind).toBe('windows') + expect(sshRelayRuntimeCompatibility['win32-arm64'].kind).toBe('windows') + }) + + it('matches a fixed Windows canonical content-identity vector', () => { + const identity = { + tupleId: 'win32-x64', + os: 'win32', + architecture: 'x64', + compatibility: sshRelayRuntimeCompatibility['win32-x64'], + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries: [] + } + + expect(computeSshRelayRuntimeContentId(identity)).toBe( + 'sha256:b3eb5c89f079ed735cb83cf2595102fe010b8dd78d3096ddf592109b2ac222b0' + ) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 3eecea74467..e91accd2153 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -96,6 +96,7 @@ describe('SSH relay runtime artifact workflow', () => { source.match(/ssh-relay-runtime-windows-signature-verification\.test\.mjs/g) ).toHaveLength(4) for (const moduleName of [ + 'compatibility', 'windows-source-signature-verification', 'release-stage-gate', 'draft-recovery', diff --git a/config/scripts/ssh-relay-runtime-zip.test.mjs b/config/scripts/ssh-relay-runtime-zip.test.mjs index 00dcab442c8..e65b1734d90 100644 --- a/config/scripts/ssh-relay-runtime-zip.test.mjs +++ b/config/scripts/ssh-relay-runtime-zip.test.mjs @@ -33,7 +33,7 @@ async function fixture() { os: 'win32', architecture: 'x64', compatibility: { - kind: 'win32', + kind: 'windows', minimumBuild: 19045, minimumOpenSshVersion: '8.1p1', minimumPowerShellVersion: '5.1', 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 2be123d4f56..4092150bd6b 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 @@ -60,11 +60,12 @@ complete a box. 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. -- [ ] **WP3 active implementation — Add credential-free canonical manifest assembly and signature - handoff.** Release-DAG and aggregate/read-back contracts pass locally and on all six native - build jobs under `E-M4-RELEASE-DAG-CI-001` and `E-M4-AGGREGATE-READBACK-CI-001`. Exact-head PR - Checks and Golden E2E are green. No production workflow, publication, desktop consumer, signing - credential, or tuple is connected. +- [ ] **WP3 active implementation — Correct Windows compatibility-kind parity before canonical + manifest assembly.** Native build identities currently emit `kind: "win32"`, but the reviewed + manifest schema requires `kind: "windows"`. Add a narrow RED/GREEN contract correction before + the signing-handoff slice. Local RED/GREEN, full SSH-relay, type, lint, line-budget, and + workflow-wiring gates pass; exact-head native CI is next. No production workflow, publication, + desktop consumer, signing credential, or tuple is connected. ## Work packages, in required order @@ -104,9 +105,10 @@ complete a box. - [x] Aggregate-input and authenticated draft read-back verification pass locally and on all six native build jobs under `E-M4-AGGREGATE-READBACK-LOCAL-001` and `E-M4-AGGREGATE-READBACK-CI-001`. -- [ ] **In progress — 2026-07-15, Codex implementation owner:** add disconnected canonical manifest - assembly and credential-free signing-request/returned-signature verification without production - workflow wiring, publication, signing credentials, desktop consumers, or tuple enablement. +- [ ] **In progress — 2026-07-15, Codex implementation owner:** correct the Windows runtime + compatibility discriminator from `win32` to the manifest-contract `windows`, prove both Windows + tuples and full regressions, then resume disconnected canonical manifest/signing handoff. Local + proof passes under `E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001`; exact-head native 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 24dcdf14cf2..ef1c0f1318b 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 credential-free canonical manifest assembly and signature handoff — **In progress — 2026-07-15, Codex implementation owner**. The disconnected release-DAG and aggregate/read-back byte boundaries are closed locally and on all six native build jobs under E-M4-RELEASE-DAG-LOCAL-001, E-M4-RELEASE-DAG-CI-001, E-M4-AGGREGATE-READBACK-LOCAL-001, and E-M4-AGGREGATE-READBACK-CI-001. Exact-floor gaps and real native signing/trust remain open external gates, so the active package may add only canonical unsigned-manifest assembly plus credential-free signing-request and returned-signature verification; it may not publish, connect desktop consumers, use signing credentials, 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 run [29391666358](https://github.com/stablyai/orca/actions/runs/29391666358) executes aggregate-input and authenticated draft read-back suites under Node 24 on all six target-native build jobs at `87a8e4dc3`; every native build and both Linux supplemental userland jobs pass under E-M4-AGGREGATE-READBACK-CI-001. The run remains red only because Windows arm64 runner build 26200 is not the required build 26100, after archive/tree/Node/PTY/watcher smoke settles in 8,032.7284 ms with 49,922,048-byte RSS. Golden E2E [29391666362](https://github.com/stablyai/orca/actions/runs/29391666362) and PR Checks [29391666360](https://github.com/stablyai/orca/actions/runs/29391666360) are green. The next disconnected package is canonical unsigned-manifest assembly plus credential-free signing-request/returned-signature verification. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, and native trust remain separately gated. Nothing is published or enabled, and legacy remains the production default.
      +Current phase: Milestone 4 / Work Package 3 Windows compatibility-kind parity prerequisite — **In progress — 2026-07-15, Codex implementation owner**. The disconnected release-DAG and aggregate/read-back byte boundaries are closed locally and on all six native build jobs under E-M4-RELEASE-DAG-LOCAL-001, E-M4-RELEASE-DAG-CI-001, E-M4-AGGREGATE-READBACK-LOCAL-001, and E-M4-AGGREGATE-READBACK-CI-001. Pre-assembly inspection found that target-native Windows build identities use compatibility `kind: "win32"` while the reviewed manifest schema and desktop consistency verifier require `kind: "windows"`. The active package is limited to a RED/GREEN parity correction and exact regressions before canonical manifest/signature-handoff work resumes; it may not publish, connect desktop consumers, use signing credentials, 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 run [29391666358](https://github.com/stablyai/orca/actions/runs/29391666358) executes aggregate-input and authenticated draft read-back suites under Node 24 on all six target-native build jobs at `87a8e4dc3`; every native build and both Linux supplemental userland jobs pass under E-M4-AGGREGATE-READBACK-CI-001. The run remains red only because Windows arm64 runner build 26200 is not the required build 26100, after archive/tree/Node/PTY/watcher smoke settles in 8,032.7284 ms with 49,922,048-byte RSS. Golden E2E [29391666362](https://github.com/stablyai/orca/actions/runs/29391666362) and PR Checks [29391666360](https://github.com/stablyai/orca/actions/runs/29391666360) are green. Inspection for the next manifest slice exposed the unpublished Windows `win32`/`windows` compatibility-kind and root-`node.exe`/`bin/node.exe` verifier mismatches. The narrow correction now passes local RED/GREEN, purpose-named workflow wiring, 36-file/203-test full SSH-relay, type, lint, line-budget, formatting, and diff gates under E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-RED-001 and E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001; exact-head native CI is next. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, 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) @@ -69,11 +69,11 @@ same change as the work it records. verification binds each input and returned byte to exact name, size, SHA-256, tuple, content identity, source draft, approved HTTPS asset origin, and exact HTTP 200 under E-M4-AGGREGATE-READBACK-LOCAL-001 and E-M4-AGGREGATE-READBACK-CI-001. -- Active package: Work Package 3 credential-free canonical unsigned-manifest assembly and signing - handoff. It may build and validate canonical bytes, construct a bounded signing request, and verify - injected returned signatures without credentials, protected-environment access, publication, - production workflow wiring, desktop consumers, or tuple enablement. No broader Milestone 4 box is - checked until the corresponding implementation and exact tests exist. +- Active package: Work Package 3 Windows compatibility-kind parity prerequisite. Correct the + artifact builder's compatibility discriminator from `win32` to the manifest contract's `windows`, + prove both Windows tuples and content-identity behavior, and run exact regressions before resuming + canonical unsigned-manifest/signing-handoff work. No broader Milestone 4 box is checked until the + corresponding implementation 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 @@ -1534,6 +1534,8 @@ focused commands as their scripts/tests are introduced. - [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) - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-aggregate-input.test.mjs config/scripts/ssh-relay-runtime-draft-readback.test.mjs` (E-M4-AGGREGATE-READBACK-LOCAL-RED-001, E-M4-AGGREGATE-READBACK-LOCAL-001; local filesystem plus mocked GitHub responses only) - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-aggregate-input.test.mjs config/scripts/ssh-relay-runtime-draft-readback.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` (E-M4-AGGREGATE-READBACK-LOCAL-001; static native-job wiring only) +- [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-compatibility.test.mjs` (E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-RED-001, E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001; exact Windows compatibility discriminator and canonical vector) +- [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-compatibility.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs src/main/ssh/ssh-relay-artifact-schema.test.ts` (E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-RED-001, E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001; build/desktop contract parity and static native-job wiring) ### Commands/scripts that must be added or formally identified @@ -9055,6 +9057,97 @@ diff --check`. request and returned-signature verification. Do not connect production credentials, publication, desktop consumers, or tuple enablement. +### E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-RED-001 — Windows artifact/manifest identity mismatch + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact base `90da7e14b` plus uncommitted purpose-named tests; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). +- Runner/network: local macOS 26.2 build 25C56 arm64, Node v26.0.0 and pnpm 10.24.0; no network, + runner, signing, release, SSH, publication, desktop consumer, or production call. +- Commands: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \ + config/scripts/ssh-relay-runtime-compatibility.test.mjs + pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \ + src/main/ssh/ssh-relay-artifact-schema.test.ts + ``` + +- Result: expected RED. The compatibility suite fails 2/2 tests in 144 ms: both Windows tuple + contracts return `kind: "win32"` rather than manifest-compatible `kind: "windows"`, and the actual + canonical content ID `sha256:704e9381bc99c97dfbdc4cfc902101879ab6990aa202114120855a5eb02aa6f5` + disagrees with the fixed `windows` vector + `sha256:b3eb5c89f079ed735cb83cf2595102fe010b8dd78d3096ddf592109b2ac222b0`. + After correcting only that discriminator locally, the schema suite's new Windows fixture fails + 1/30 tests in 196 ms because the desktop consistency verifier expects root `node.exe`; every + target-native Windows artifact and the detailed plan use `bin/node.exe`. +- Oracle proved: the artifact builder's Windows identities could not be consumed by the reviewed + signed-manifest schema even though both sides were individually tested. The failure was caught + before canonical assembly, publication, desktop embedding, or tuple enablement. +- Does not prove: the correction, target-native regeneration, archive equality after the intentional + content-ID change, native signing/trust, manifest assembly/signing, SSH behavior, or an enabled + tuple. +- Correction: use `windows` only as the compatibility-union discriminator while retaining `win32` + as the tuple OS, and make the desktop verifier accept the already-reviewed `bin/node.exe` archive + layout. Add both contracts to every native artifact job. + +### E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001 — Windows artifact and manifest identities align locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted correction based on exact head `90da7e14b`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). +- Runner/network: local macOS 26.2 build 25C56 arm64, Node v26.0.0 and pnpm 10.24.0; no network, + native runner, signing, release, SSH, publication, desktop consumer, or production call. The + repository emits its expected Node-24 engine warning. +- Commands: + + ```sh + node --check config/scripts/ssh-relay-runtime-compatibility.mjs + node --check config/scripts/ssh-relay-runtime-compatibility.test.mjs + /usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \ + config/scripts/ssh-relay-runtime-compatibility.test.mjs \ + config/scripts/ssh-relay-runtime-workflow.test.mjs \ + src/main/ssh/ssh-relay-artifact-schema.test.ts + pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \ + config/scripts/ssh-relay-runtime-compatibility.test.mjs \ + config/scripts/ssh-relay-runtime-zip.test.mjs \ + config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-native-signing-apply.test.mjs \ + config/scripts/ssh-relay-runtime-windows-signature-verification.test.mjs \ + config/scripts/ssh-relay-runtime-windows-source-signature-verification.test.mjs \ + src/main/ssh/ssh-relay-artifact-schema.test.ts \ + src/main/ssh/ssh-relay-runtime-identity.test.ts + 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 + git diff --check + ``` + +- Result: PASS. Both syntax checks pass. The compatibility/schema/workflow focus passes 3 files/38 + tests in 1.59 s; `/usr/bin/time -l` records 3.41 seconds wall time, 133,152,768-byte maximum RSS, + zero swaps, and 96,572,816-byte peak memory footprint. The eight affected contract files pass 62 + tests in 5.46 s. The complete SSH-relay suite passes 36 files/203 tests in 14.88 s. 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. +- Oracle proved: both target-native Windows tuple definitions now use the manifest schema's + `windows` compatibility discriminator while preserving `win32` tuple IDs/OS values. The MJS + canonical identity matches the fixed Windows vector, the TypeScript schema accepts the exact + `bin/node.exe` archive layout, and both POSIX and Windows workflow families syntax-check and run + the parity suite. The intentional content-ID/archive-name change occurs before publication or + enablement. +- Does not prove: target-native Windows rebuild/reproducibility with the new content IDs, macOS/Linux + non-regression on native runners, native signing/trust, canonical manifest assembly/signing, + publication, desktop embedding, SSH transfer/install, fallback, performance, or an enabled tuple. +- Follow-up: run the parity suite and regenerate all six native artifacts at the exact implementation + head. If green, record the new Windows content IDs and resume credential-free canonical manifest + assembly/signature handoff. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/src/main/ssh/ssh-relay-artifact-consistency.ts b/src/main/ssh/ssh-relay-artifact-consistency.ts index 4272beef7f1..36bfd023e4b 100644 --- a/src/main/ssh/ssh-relay-artifact-consistency.ts +++ b/src/main/ssh/ssh-relay-artifact-consistency.ts @@ -16,7 +16,8 @@ function expectedTupleId(tuple: SshRelayRuntimeTuple): string { function assertRequiredRuntimeEntries(tuple: SshRelayRuntimeTuple): void { const files = tuple.entries.filter((entry) => entry.type === 'file') const expectedPaths = new Map([ - ['node', tuple.os === 'win32' ? 'node.exe' : 'bin/node'], + // Why: every archive keeps bundled Node under `bin`, including the Windows ZIP. + ['node', tuple.os === 'win32' ? 'bin/node.exe' : 'bin/node'], ['relay', 'relay.js'], ['relay-watcher', 'relay-watcher.js'] ]) diff --git a/src/main/ssh/ssh-relay-artifact-schema.test.ts b/src/main/ssh/ssh-relay-artifact-schema.test.ts index c057c627818..d52ad42cafe 100644 --- a/src/main/ssh/ssh-relay-artifact-schema.test.ts +++ b/src/main/ssh/ssh-relay-artifact-schema.test.ts @@ -2,6 +2,37 @@ import { describe, expect, it } from 'vitest' import { createSshRelayArtifactTestManifest } from './ssh-relay-artifact-test-manifest' import { parseSshRelayArtifactManifest } from './ssh-relay-artifact-schema' +import { sshRelayRuntimeArchiveName } from './ssh-relay-release-asset' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity' + +function createWindowsManifest() { + const manifest = createSshRelayArtifactTestManifest() + const tuple = manifest.tuples[0] + tuple.tupleId = 'win32-x64' + tuple.os = 'win32' + tuple.architecture = 'x64' + tuple.compatibility = { + kind: 'windows', + minimumBuild: 19045, + minimumOpenSshVersion: '8.1p1', + minimumPowerShellVersion: '5.1', + minimumDotNetFrameworkRelease: 528040 + } + for (const entry of tuple.entries) { + entry.path = entry.path + .replace('bin/node', 'bin/node.exe') + .replace('watcher-linux-x64-glibc', 'watcher-win32-x64') + } + tuple.nativeVerification.policy = 'signpath-authenticode-v1' + for (const file of tuple.nativeVerification.files) { + file.path = file.path + .replace('bin/node', 'bin/node.exe') + .replace('watcher-linux-x64-glibc', 'watcher-win32-x64') + } + tuple.contentId = computeSshRelayRuntimeContentId(tuple) + tuple.archive.name = sshRelayRuntimeArchiveName(tuple.tupleId, tuple.contentId) + return manifest +} describe('SSH relay artifact manifest schema', () => { it('accepts a complete internally consistent manifest', () => { @@ -11,6 +42,15 @@ describe('SSH relay artifact manifest schema', () => { expect(parsed.tuples[0].tupleId).toBe('linux-x64-glibc') }) + it('accepts the target-native Windows Node path and compatibility discriminator', () => { + const parsed = parseSshRelayArtifactManifest(createWindowsManifest()) + + expect(parsed.tuples[0].compatibility.kind).toBe('windows') + expect(parsed.tuples[0].entries).toContainEqual( + expect.objectContaining({ path: 'bin/node.exe', role: 'node' }) + ) + }) + it('rejects unsupported schema versions, extra fields, and non-canonical timestamps', () => { const unsupported = createSshRelayArtifactTestManifest() as unknown as Record unsupported.schemaVersion = 2 From c68a99039e4633d41e21fb30afdfeaa06f776369 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 02:01:27 -0400 Subject: [PATCH 109/267] docs(ssh): anchor Windows manifest parity proof Co-authored-by: Orca --- ...relay-github-release-implementation-checklist-summary.md | 3 ++- ...-14-ssh-relay-github-release-implementation-checklist.md | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) 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 4092150bd6b..2227082b19c 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 @@ -108,7 +108,8 @@ complete a box. - [ ] **In progress — 2026-07-15, Codex implementation owner:** correct the Windows runtime compatibility discriminator from `win32` to the manifest-contract `windows`, prove both Windows tuples and full regressions, then resume disconnected canonical manifest/signing handoff. Local - proof passes under `E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001`; exact-head native CI is next. + proof passes at `707a9da23` under `E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001`; exact-head native 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 ef1c0f1318b..c622fc52306 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 @@ -9,7 +9,7 @@ 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 Windows compatibility-kind parity prerequisite — **In progress — 2026-07-15, Codex implementation owner**. The disconnected release-DAG and aggregate/read-back byte boundaries are closed locally and on all six native build jobs under E-M4-RELEASE-DAG-LOCAL-001, E-M4-RELEASE-DAG-CI-001, E-M4-AGGREGATE-READBACK-LOCAL-001, and E-M4-AGGREGATE-READBACK-CI-001. Pre-assembly inspection found that target-native Windows build identities use compatibility `kind: "win32"` while the reviewed manifest schema and desktop consistency verifier require `kind: "windows"`. The active package is limited to a RED/GREEN parity correction and exact regressions before canonical manifest/signature-handoff work resumes; it may not publish, connect desktop consumers, use signing credentials, 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 run [29391666358](https://github.com/stablyai/orca/actions/runs/29391666358) executes aggregate-input and authenticated draft read-back suites under Node 24 on all six target-native build jobs at `87a8e4dc3`; every native build and both Linux supplemental userland jobs pass under E-M4-AGGREGATE-READBACK-CI-001. The run remains red only because Windows arm64 runner build 26200 is not the required build 26100, after archive/tree/Node/PTY/watcher smoke settles in 8,032.7284 ms with 49,922,048-byte RSS. Golden E2E [29391666362](https://github.com/stablyai/orca/actions/runs/29391666362) and PR Checks [29391666360](https://github.com/stablyai/orca/actions/runs/29391666360) are green. Inspection for the next manifest slice exposed the unpublished Windows `win32`/`windows` compatibility-kind and root-`node.exe`/`bin/node.exe` verifier mismatches. The narrow correction now passes local RED/GREEN, purpose-named workflow wiring, 36-file/203-test full SSH-relay, type, lint, line-budget, formatting, and diff gates under E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-RED-001 and E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001; exact-head native CI is next. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, and native trust remain separately gated. Nothing is published or enabled, and legacy remains the production default.
      +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact-head artifact run [29391666358](https://github.com/stablyai/orca/actions/runs/29391666358) executes aggregate-input and authenticated draft read-back suites under Node 24 on all six target-native build jobs at `87a8e4dc3`; every native build and both Linux supplemental userland jobs pass under E-M4-AGGREGATE-READBACK-CI-001. The run remains red only because Windows arm64 runner build 26200 is not the required build 26100, after archive/tree/Node/PTY/watcher smoke settles in 8,032.7284 ms with 49,922,048-byte RSS. Golden E2E [29391666362](https://github.com/stablyai/orca/actions/runs/29391666362) and PR Checks [29391666360](https://github.com/stablyai/orca/actions/runs/29391666360) are green. Inspection for the next manifest slice exposed the unpublished Windows `win32`/`windows` compatibility-kind and root-`node.exe`/`bin/node.exe` verifier mismatches. The narrow correction is committed at `707a9da23e3955bed6ed00ad3afc24b5ab808243` and passes local RED/GREEN, purpose-named workflow wiring, 36-file/203-test full SSH-relay, type, lint, line-budget, formatting, and diff gates under E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-RED-001 and E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001; push and exact-head native CI are next. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, 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) @@ -9096,8 +9096,8 @@ diff --check`. - Date: 2026-07-15 - Owner: Codex implementation owner -- Source: uncommitted correction based on exact head `90da7e14b`; draft PR - [#8741](https://github.com/stablyai/orca/pull/8741). +- Source: implementation commit `707a9da23e3955bed6ed00ad3afc24b5ab808243`, based on exact prior + head `90da7e14b`; draft PR [#8741](https://github.com/stablyai/orca/pull/8741). - Runner/network: local macOS 26.2 build 25C56 arm64, Node v26.0.0 and pnpm 10.24.0; no network, native runner, signing, release, SSH, publication, desktop consumer, or production call. The repository emits its expected Node-24 engine warning. From 63899c182f3a831d61fb1f6566a0d0318bca4192 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 02:19:21 -0400 Subject: [PATCH 110/267] docs(ssh): record Windows manifest parity CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 26 +++-- ...github-release-implementation-checklist.md | 106 +++++++++++++++--- 2 files changed, 103 insertions(+), 29 deletions(-) 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 2227082b19c..e0fc39ba30f 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 @@ -60,12 +60,14 @@ complete a box. 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. -- [ ] **WP3 active implementation — Correct Windows compatibility-kind parity before canonical - manifest assembly.** Native build identities currently emit `kind: "win32"`, but the reviewed - manifest schema requires `kind: "windows"`. Add a narrow RED/GREEN contract correction before - the signing-handoff slice. Local RED/GREEN, full SSH-relay, type, lint, line-budget, and - workflow-wiring gates pass; exact-head native CI is next. No production workflow, publication, - desktop consumer, signing credential, or tuple is connected. +- [ ] **WP3 active implementation — Build the credential-free canonical manifest/signing handoff.** + Windows compatibility-kind parity is closed locally and on all six target-native jobs in + exact-head run + [29393022768](https://github.com/stablyai/orca/actions/runs/29393022768) under + `E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001` and `E-M4-WINDOWS-MANIFEST-PARITY-CI-001`. Next, + canonicalize only validated unsigned manifest bytes, create the bounded signing request, and + verify returned signatures without production credentials. No production workflow, + publication, desktop consumer, signing credential, or tuple is connected. ## Work packages, in required order @@ -105,11 +107,13 @@ complete a box. - [x] Aggregate-input and authenticated draft read-back verification pass locally and on all six native build jobs under `E-M4-AGGREGATE-READBACK-LOCAL-001` and `E-M4-AGGREGATE-READBACK-CI-001`. -- [ ] **In progress — 2026-07-15, Codex implementation owner:** correct the Windows runtime - compatibility discriminator from `win32` to the manifest-contract `windows`, prove both Windows - tuples and full regressions, then resume disconnected canonical manifest/signing handoff. Local - proof passes at `707a9da23` under `E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001`; exact-head native CI - is next. +- [x] Correct the Windows compatibility discriminator and `bin/node.exe` manifest parity; prove both + regenerated Windows identities and all-six native regressions + (`E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001`, `E-M4-WINDOWS-MANIFEST-PARITY-CI-001`). +- [ ] **In progress — 2026-07-15, Codex implementation owner:** add disconnected canonical unsigned + manifest assembly, a bounded signing request, and returned-signature verification. Keep final + detached-signature asset encoding, production credentials, publication, desktop consumers, and + every tuple outside this slice until their contracts and gates are explicit. - [ ] 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 c622fc52306..2514413fc4f 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 Windows compatibility-kind parity prerequisite — **In progress — 2026-07-15, Codex implementation owner**. The disconnected release-DAG and aggregate/read-back byte boundaries are closed locally and on all six native build jobs under E-M4-RELEASE-DAG-LOCAL-001, E-M4-RELEASE-DAG-CI-001, E-M4-AGGREGATE-READBACK-LOCAL-001, and E-M4-AGGREGATE-READBACK-CI-001. Pre-assembly inspection found that target-native Windows build identities use compatibility `kind: "win32"` while the reviewed manifest schema and desktop consistency verifier require `kind: "windows"`. The active package is limited to a RED/GREEN parity correction and exact regressions before canonical manifest/signature-handoff work resumes; it may not publish, connect desktop consumers, use signing credentials, 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 run [29391666358](https://github.com/stablyai/orca/actions/runs/29391666358) executes aggregate-input and authenticated draft read-back suites under Node 24 on all six target-native build jobs at `87a8e4dc3`; every native build and both Linux supplemental userland jobs pass under E-M4-AGGREGATE-READBACK-CI-001. The run remains red only because Windows arm64 runner build 26200 is not the required build 26100, after archive/tree/Node/PTY/watcher smoke settles in 8,032.7284 ms with 49,922,048-byte RSS. Golden E2E [29391666362](https://github.com/stablyai/orca/actions/runs/29391666362) and PR Checks [29391666360](https://github.com/stablyai/orca/actions/runs/29391666360) are green. Inspection for the next manifest slice exposed the unpublished Windows `win32`/`windows` compatibility-kind and root-`node.exe`/`bin/node.exe` verifier mismatches. The narrow correction is committed at `707a9da23e3955bed6ed00ad3afc24b5ab808243` and passes local RED/GREEN, purpose-named workflow wiring, 36-file/203-test full SSH-relay, type, lint, line-budget, formatting, and diff gates under E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-RED-001 and E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001; push and exact-head native CI are next. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, and native trust remain separately gated. Nothing is published or enabled, and legacy remains the production default.
      +Current phase: Milestone 4 / Work Package 3 credential-free canonical manifest/signing handoff — **In progress — 2026-07-15, Codex implementation owner**. The disconnected release-DAG, aggregate/read-back byte boundaries, and Windows artifact/manifest parity prerequisite are closed locally and on all six native build jobs under E-M4-RELEASE-DAG-LOCAL-001, E-M4-RELEASE-DAG-CI-001, E-M4-AGGREGATE-READBACK-LOCAL-001, E-M4-AGGREGATE-READBACK-CI-001, E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001, and E-M4-WINDOWS-MANIFEST-PARITY-CI-001. The active package is limited to canonical unsigned-manifest bytes, a bounded signing request, and returned-signature verification; it may not choose final detached-signature asset encoding, publish, connect desktop consumers, use production signing credentials, 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 run [29393022768](https://github.com/stablyai/orca/actions/runs/29393022768) at `c68a99039e4633d41e21fb30afdfeaa06f776369` executes the corrected compatibility/schema/workflow contracts under Node 24.18.0 and rebuilds, verifies, smokes, compares, and uploads all six unpublished target-native artifacts. Both regenerated Windows identities use compatibility `kind: "windows"`; x64 content ID is `sha256:4224aee6de820a94263f05ebfc58edd421921431e8cbdf1d5ec6ddcf783afc26` and arm64 content ID is `sha256:02edf462be83c2864a89546d5344d348f9e07ce10964660342608a8c614e47db`. Both Linux supplemental userland jobs and the Windows x64 floor job pass. The workflow remains red only because Windows arm64 runner build 26200 is not the required build 26100, after complete runtime smoke settles in 8,382.8952 ms with 49,811,456-byte RSS. Golden E2E [29393022784](https://github.com/stablyai/orca/actions/runs/29393022784) and PR Checks [29393022761](https://github.com/stablyai/orca/actions/runs/29393022761) are green. The parity prerequisite is closed under E-M4-WINDOWS-MANIFEST-PARITY-CI-001; canonical manifest/signature-handoff RED tests are next. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, 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) @@ -69,10 +69,11 @@ same change as the work it records. verification binds each input and returned byte to exact name, size, SHA-256, tuple, content identity, source draft, approved HTTPS asset origin, and exact HTTP 200 under E-M4-AGGREGATE-READBACK-LOCAL-001 and E-M4-AGGREGATE-READBACK-CI-001. -- Active package: Work Package 3 Windows compatibility-kind parity prerequisite. Correct the - artifact builder's compatibility discriminator from `win32` to the manifest contract's `windows`, - prove both Windows tuples and content-identity behavior, and run exact regressions before resuming - canonical unsigned-manifest/signing-handoff work. No broader Milestone 4 box is checked until the +- Active package: Work Package 3 disconnected canonical unsigned-manifest/signing handoff. Assemble + canonical bytes only from fully validated inputs, create a bounded credential-free signing request, + and reconstruct and verify the returned Ed25519 signature before emitting a final manifest. Final + detached-signature asset encoding, production credentials, publication, desktop consumers, and + tuple enablement remain outside this slice. No broader Milestone 4 box is checked until the corresponding implementation 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 @@ -191,11 +192,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 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. +- Next required action: add RED tests and implement disconnected canonical unsigned-manifest + assembly, a bounded credential-free signing request, and returned-signature reconstruction and + verification. Keep final detached-signature asset encoding, real native/manifest 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 @@ -9148,6 +9149,76 @@ diff --check`. head. If green, record the new Windows content IDs and resume credential-free canonical manifest assembly/signature handoff. +### E-M4-WINDOWS-MANIFEST-PARITY-CI-001 — Corrected parity passes all six native artifact jobs + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact head `c68a99039e4633d41e21fb30afdfeaa06f776369`, containing implementation + commit `707a9da23e3955bed6ed00ad3afc24b5ab808243`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). +- Runner/network: GitHub-hosted target-native jobs under Node 24.18.0 in artifact run + [29393022768](https://github.com/stablyai/orca/actions/runs/29393022768): + - Windows arm64 job 87280402068, `windows-11-arm64` image `20260706.102.1`; + - Linux x64 job 87280402087, `ubuntu-24.04` image `20260705.232.1`; + - macOS arm64 job 87280402091, `macos-15-arm64` image `20260706.0213.1`; + - macOS x64 job 87280402092, `macos-15` image `20260629.0276.1`; + - Windows x64 job 87280402093, `windows-2022` image `20260706.237.1`; + - Linux arm64 job 87280402113, `ubuntu-24.04-arm` image `20260714.61.1`. +- Commands/evidence inspection: + + ```sh + gh run watch 29393022768 --repo stablyai/orca --interval 15 --exit-status + gh run view 29393022768 --repo stablyai/orca --json status,conclusion,headSha,url,jobs + gh run view 29393022768 --repo stablyai/orca --job --log + gh run download 29393022768 --repo stablyai/orca \ + -n ssh-relay-runtime-win32-x64 -D + gh run download 29393022768 --repo stablyai/orca \ + -n ssh-relay-runtime-win32-arm64 -D + gh run view 29393022761 --repo stablyai/orca --json status,conclusion,headSha,url,jobs + gh run view 29393022784 --repo stablyai/orca --json status,conclusion,headSha,url,jobs + ``` + +- Result: PASS on all six target-native build jobs. The complete contract suite passes 35 + files/199 tests on each POSIX runner and 36 files/195 passed plus 8 platform-skipped tests on each + Windows runner. Total Vitest durations are Linux x64 4.41 s, Linux arm64 3.37 s, macOS arm64 + 6.96 s, macOS x64 29.25 s, Windows x64 8.16 s, and Windows arm64 11.13 s. The purpose-named + compatibility suite passes 2/2 tests in 3–8 ms on every runner; the workflow suite passes 6/6 in + 77–499 ms. Every job then builds twice, verifies archive/tree/Node/native modules, smokes PTY and + watcher behavior, compares exact outputs, and uploads unpublished evidence. +- Downloaded Windows read-back identities: + - `win32-x64`: compatibility `kind: "windows"`, content ID + `sha256:4224aee6de820a94263f05ebfc58edd421921431e8cbdf1d5ec6ddcf783afc26`, archive + `orca-ssh-relay-runtime-v1-win32-x64-4224aee6de820a94263f05ebfc58edd421921431e8cbdf1d5ec6ddcf783afc26.zip`, + 37,168,778 bytes, archive SHA-256 + `sha256:473aff5d97d08b0a1deaa5a74940975eb44ab303f88fc2a94d8b9b2f3d594e7b`; + - `win32-arm64`: compatibility `kind: "windows"`, content ID + `sha256:02edf462be83c2864a89546d5344d348f9e07ce10964660342608a8c614e47db`, archive + `orca-ssh-relay-runtime-v1-win32-arm64-02edf462be83c2864a89546d5344d348f9e07ce10964660342608a8c614e47db.zip`, + 33,204,738 bytes, archive SHA-256 + `sha256:5f5ffc89d3a1a4fee50ad641c0c9abd063a113269851936eb30b01ab1527fdcd`. +- Concurrent regression: Linux supplemental userland jobs 87281853967 and 87281853918 pass, as does + Windows x64 oldest-floor job 87281706480. Artifact-run conclusion is the expected `failure` only + because Windows arm64 floor job 87281706438 observes build 26200 rather than required build 26100; + before failing that exact-floor gate, complete runtime execution settles in 8,382.8952 ms, its + smoke stage reports 6,055.8004 ms and 49,811,456-byte RSS, and post-observation resources contain + only the expected parent `PipeWrap`s. PR Checks run + [29393022761](https://github.com/stablyai/orca/actions/runs/29393022761) job 87280377607 and both + Golden E2E jobs 87280377578/87280377611 in run + [29393022784](https://github.com/stablyai/orca/actions/runs/29393022784) are green at the same head. +- Oracle proved: the corrected Windows compatibility discriminator and `bin/node.exe` layout are + accepted consistently by the build and desktop contracts; both Windows architectures regenerate + stable, internally consistent identities/archive names; and no POSIX or Windows native artifact + regression appears in the all-six build/equality/smoke gates. +- Does not prove: Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, real native signing/trust, + canonical manifest assembly/signing, publication, desktop embedding, SSH transfer/install, + fallback, performance, or an enabled tuple. +- Worktree visibility residual: `orca status --json` reports app not running and runtime state + `stale_bootstrap`; the required best-effort `orca worktree set --worktree active --comment ... +--json` retry fails with `runtime_unavailable`. This does not affect repository or CI evidence. +- Follow-up: begin RED tests for credential-free canonical unsigned-manifest assembly, bounded signing + request, and returned-signature verification. Keep final detached-signature asset encoding, + production credentials, publication, desktop consumers, and tuple enablement disconnected. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -9205,13 +9276,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and run the disconnected aggregate-input and authenticated draft read-back verifiers on all -six native jobs at the exact implementation head. If green, record exact job evidence and implement -the next credential-free canonical-manifest assembly/signature-handoff slice without release -publication, production signing credentials, 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. +Add RED tests and implement credential-free canonical unsigned-manifest assembly, a bounded signing +request, and returned-signature reconstruction and verification. Do not choose final detached- +signature asset encoding, connect release publication, use production signing credentials, or add a +desktop consumer in this slice. 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 From 14355dfe0583f634a6e86ada9e1afcf7abe7a8fb Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 02:48:38 -0400 Subject: [PATCH 111/267] build(ssh): add manifest signing handoff contracts Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 19 + .../ssh-relay-runtime-manifest-assembly.mjs | 152 ++++++++ ...h-relay-runtime-manifest-assembly.test.mjs | 137 +++++++ ...relay-runtime-manifest-signing-handoff.mjs | 173 ++++++++ ...-runtime-manifest-signing-handoff.test.mjs | 168 ++++++++ .../ssh-relay-runtime-manifest-validation.mjs | 369 ++++++++++++++++++ .../ssh-relay-runtime-workflow.test.mjs | 8 + ...elease-implementation-checklist-summary.md | 14 +- ...github-release-implementation-checklist.md | 135 ++++++- 9 files changed, 1156 insertions(+), 19 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-manifest-assembly.mjs create mode 100644 config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs create mode 100644 config/scripts/ssh-relay-runtime-manifest-signing-handoff.mjs create mode 100644 config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs create mode 100644 config/scripts/ssh-relay-runtime-manifest-validation.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 5f0d8dabf7c..6f8cde69090 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -153,6 +153,11 @@ jobs: node --check config/scripts/ssh-relay-runtime-aggregate-input.test.mjs node --check config/scripts/ssh-relay-runtime-draft-readback.mjs node --check config/scripts/ssh-relay-runtime-draft-readback.test.mjs + node --check config/scripts/ssh-relay-runtime-manifest-validation.mjs + node --check config/scripts/ssh-relay-runtime-manifest-assembly.mjs + node --check config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs + node --check config/scripts/ssh-relay-runtime-manifest-signing-handoff.mjs + node --check config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs pnpm exec vitest run --config config/vitest.config.ts \ config/scripts/ssh-relay-runtime-compatibility.test.mjs \ config/scripts/ssh-relay-node-release-verification.test.mjs \ @@ -179,6 +184,8 @@ jobs: config/scripts/ssh-relay-runtime-draft-recovery.test.mjs \ config/scripts/ssh-relay-runtime-aggregate-input.test.mjs \ config/scripts/ssh-relay-runtime-draft-readback.test.mjs \ + config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs \ + config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs \ config/scripts/ssh-relay-runtime-closure.test.mjs \ config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ config/scripts/ssh-relay-runtime-reproducibility.test.mjs \ @@ -514,6 +521,16 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime draft read-back syntax check failed' } node --check config/scripts/ssh-relay-runtime-draft-readback.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime draft read-back test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-manifest-validation.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime manifest validation syntax check failed' } + node --check config/scripts/ssh-relay-runtime-manifest-assembly.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime manifest assembly syntax check failed' } + node --check config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime manifest assembly test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-manifest-signing-handoff.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime manifest signing handoff syntax check failed' } + node --check config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime manifest signing handoff test syntax check failed' } pnpm exec vitest run --config config/vitest.config.ts ` config/scripts/ssh-relay-runtime-compatibility.test.mjs ` config/scripts/ssh-relay-node-release-verification.test.mjs ` @@ -540,6 +557,8 @@ jobs: config/scripts/ssh-relay-runtime-draft-recovery.test.mjs ` config/scripts/ssh-relay-runtime-aggregate-input.test.mjs ` config/scripts/ssh-relay-runtime-draft-readback.test.mjs ` + config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs ` + config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs ` config/scripts/ssh-relay-runtime-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-manifest-assembly.mjs b/config/scripts/ssh-relay-runtime-manifest-assembly.mjs new file mode 100644 index 00000000000..a92eaa60419 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-manifest-assembly.mjs @@ -0,0 +1,152 @@ +import { createHash } from 'node:crypto' + +import { parseSshRelayRuntimeUnsignedManifest } from './ssh-relay-runtime-manifest-validation.mjs' + +// Why: this exceeds the eight-tuple entry/attestation maximum while bounding signer input memory. +const MAX_CANONICAL_BYTES = 32 * 1024 * 1024 + +function compareAscii(left, right) { + return left < right ? -1 : left > right ? 1 : 0 +} + +function canonicalCompatibility(compatibility) { + if (compatibility.kind === 'linux') { + return { + kind: compatibility.kind, + minimumKernelVersion: compatibility.minimumKernelVersion, + libc: { + family: compatibility.libc.family, + minimumVersion: compatibility.libc.minimumVersion, + minimumLibstdcxxVersion: compatibility.libc.minimumLibstdcxxVersion, + minimumGlibcxxVersion: compatibility.libc.minimumGlibcxxVersion + } + } + } + if (compatibility.kind === 'darwin') { + return { kind: compatibility.kind, minimumVersion: compatibility.minimumVersion } + } + return { + kind: compatibility.kind, + minimumBuild: compatibility.minimumBuild, + minimumOpenSshVersion: compatibility.minimumOpenSshVersion, + minimumPowerShellVersion: compatibility.minimumPowerShellVersion, + minimumDotNetFrameworkRelease: compatibility.minimumDotNetFrameworkRelease + } +} + +function canonicalEntry(entry) { + return entry.type === 'directory' + ? { path: entry.path, type: entry.type, mode: entry.mode } + : { + path: entry.path, + type: entry.type, + role: entry.role, + size: entry.size, + mode: entry.mode, + sha256: entry.sha256 + } +} + +function canonicalTuple(tuple) { + return { + tupleId: tuple.tupleId, + os: tuple.os, + architecture: tuple.architecture, + compatibility: canonicalCompatibility(tuple.compatibility), + nodeVersion: tuple.nodeVersion, + dependencies: { + nodePtyVersion: tuple.dependencies.nodePtyVersion, + parcelWatcherVersion: tuple.dependencies.parcelWatcherVersion + }, + entries: [...tuple.entries] + .sort((left, right) => compareAscii(left.path, right.path)) + .map(canonicalEntry), + contentId: tuple.contentId, + archive: { + name: tuple.archive.name, + size: tuple.archive.size, + expandedSize: tuple.archive.expandedSize, + fileCount: tuple.archive.fileCount, + sha256: tuple.archive.sha256 + }, + metadataAssets: { + sbom: { + name: tuple.metadataAssets.sbom.name, + size: tuple.metadataAssets.sbom.size, + sha256: tuple.metadataAssets.sbom.sha256 + }, + provenance: { + name: tuple.metadataAssets.provenance.name, + size: tuple.metadataAssets.provenance.size, + sha256: tuple.metadataAssets.provenance.sha256 + } + }, + nativeVerification: { + policy: tuple.nativeVerification.policy, + tool: { + name: tuple.nativeVerification.tool.name, + version: tuple.nativeVerification.tool.version + }, + verifiedAt: tuple.nativeVerification.verifiedAt, + files: [...tuple.nativeVerification.files] + .sort((left, right) => compareAscii(left.path, right.path)) + .map((file) => ({ path: file.path, sha256: file.sha256 })) + } + } +} + +export function assembleCanonicalSshRelayRuntimeManifest(input) { + const parsed = parseSshRelayRuntimeUnsignedManifest(input) + // Why: release and desktop verification must authenticate one fixed projection across runners. + const manifest = { + schemaVersion: parsed.schemaVersion, + build: { + tag: parsed.build.tag, + channel: parsed.build.channel, + version: parsed.build.version, + relayProtocolVersion: parsed.build.relayProtocolVersion + }, + createdAt: parsed.createdAt, + tuples: [...parsed.tuples] + .sort((left, right) => compareAscii(left.tupleId, right.tupleId)) + .map(canonicalTuple) + } + const canonicalBytes = Buffer.from(JSON.stringify(manifest), 'utf8') + if (canonicalBytes.length === 0 || canonicalBytes.length > MAX_CANONICAL_BYTES) { + throw new Error('SSH relay runtime canonical manifest exceeds its size limit') + } + return { + manifest, + canonicalBytes, + sha256: `sha256:${createHash('sha256').update(canonicalBytes).digest('hex')}` + } +} + +export function parseCanonicalSshRelayRuntimeManifestBytes(input) { + if (!Buffer.isBuffer(input) && !(input instanceof Uint8Array)) { + throw new Error('SSH relay runtime canonical manifest must be bytes') + } + const bytes = Buffer.from(input) + if (bytes.length === 0 || bytes.length > MAX_CANONICAL_BYTES) { + throw new Error('SSH relay runtime canonical manifest exceeds its size limit') + } + let source + try { + source = new TextDecoder('utf-8', { fatal: true }).decode(bytes) + } catch { + throw new Error('SSH relay runtime canonical manifest must be valid UTF-8') + } + let inputManifest + try { + inputManifest = JSON.parse(source) + } catch { + throw new Error('SSH relay runtime canonical manifest must be valid JSON') + } + const assembled = assembleCanonicalSshRelayRuntimeManifest(inputManifest) + if (!assembled.canonicalBytes.equals(bytes)) { + throw new Error('SSH relay runtime manifest bytes are not the canonical unsigned projection') + } + return assembled.manifest +} + +export { MAX_CANONICAL_BYTES as SSH_RELAY_RUNTIME_MAX_CANONICAL_MANIFEST_BYTES } diff --git a/config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs b/config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs new file mode 100644 index 00000000000..c5b2bce8f9c --- /dev/null +++ b/config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs @@ -0,0 +1,137 @@ +import { createHash } from 'node:crypto' + +import { describe, expect, it } from 'vitest' + +import { createSshRelayArtifactTestManifest } from '../../src/main/ssh/ssh-relay-artifact-test-manifest.ts' +import { canonicalUnsignedSshRelayManifestBytes } from '../../src/main/ssh/ssh-relay-manifest-signature.ts' +import { + assembleCanonicalSshRelayRuntimeManifest, + parseCanonicalSshRelayRuntimeManifestBytes +} from './ssh-relay-runtime-manifest-assembly.mjs' +import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' + +function unsignedManifest() { + const { signatures: _signatures, ...unsigned } = createSshRelayArtifactTestManifest() + return unsigned +} + +function arm64TupleFrom(source) { + const tuple = structuredClone(source) + tuple.tupleId = 'linux-arm64-glibc' + tuple.architecture = 'arm64' + tuple.compatibility = structuredClone(sshRelayRuntimeCompatibility[tuple.tupleId]) + for (const entry of tuple.entries) { + entry.path = entry.path.replaceAll('watcher-linux-x64-glibc', 'watcher-linux-arm64-glibc') + } + for (const file of tuple.nativeVerification.files) { + file.path = file.path.replaceAll('watcher-linux-x64-glibc', 'watcher-linux-arm64-glibc') + } + tuple.metadataAssets.sbom.name = 'orca-ssh-relay-runtime-linux-arm64-glibc.spdx.json' + tuple.metadataAssets.provenance.name = 'orca-ssh-relay-runtime-linux-arm64-glibc.provenance.json' + tuple.contentId = computeSshRelayRuntimeContentId(tuple) + tuple.archive.name = `orca-ssh-relay-runtime-v1-${tuple.tupleId}-${tuple.contentId.slice('sha256:'.length)}.tar.xz` + return tuple +} + +describe('SSH relay runtime canonical manifest assembly', () => { + it('matches the desktop canonical unsigned test vector exactly', () => { + const assembled = assembleCanonicalSshRelayRuntimeManifest(unsignedManifest()) + + expect(assembled.canonicalBytes.at(-1)).not.toBe('\n'.charCodeAt(0)) + expect(createHash('sha256').update(assembled.canonicalBytes).digest('hex')).toBe( + 'e78bf4416628a91055035dc7926035cbf633f29d3618be34e041c6dc5e0794fb' + ) + expect(assembled.sha256).toBe( + 'sha256:e78bf4416628a91055035dc7926035cbf633f29d3618be34e041c6dc5e0794fb' + ) + expect(parseCanonicalSshRelayRuntimeManifestBytes(assembled.canonicalBytes)).toEqual( + assembled.manifest + ) + }) + + it('sorts tuples, entries, and native attestations by portable ASCII identity', () => { + const input = unsignedManifest() + const arm64 = arm64TupleFrom(input.tuples[0]) + input.tuples = [input.tuples[0], arm64].toReversed() + for (const tuple of input.tuples) { + tuple.entries = tuple.entries.toReversed() + tuple.nativeVerification.files = tuple.nativeVerification.files.toReversed() + } + + const first = assembleCanonicalSshRelayRuntimeManifest(input) + input.tuples = input.tuples.toReversed() + for (const tuple of input.tuples) { + tuple.entries = tuple.entries.toReversed() + tuple.nativeVerification.files = tuple.nativeVerification.files.toReversed() + } + const second = assembleCanonicalSshRelayRuntimeManifest(input) + + expect(first.canonicalBytes).toEqual(second.canonicalBytes) + expect(second.canonicalBytes).toEqual(canonicalUnsignedSshRelayManifestBytes(input)) + expect(first.manifest.tuples.map((tuple) => tuple.tupleId)).toEqual([ + 'linux-arm64-glibc', + 'linux-x64-glibc' + ]) + for (const tuple of first.manifest.tuples) { + expect(tuple.entries.map((entry) => entry.path)).toEqual( + tuple.entries.map((entry) => entry.path).toSorted() + ) + expect(tuple.nativeVerification.files.map((entry) => entry.path)).toEqual( + tuple.nativeVerification.files.map((entry) => entry.path).toSorted() + ) + } + }) + + it('rejects signatures, unknown fields, and non-canonical byte encodings', () => { + expect(() => + assembleCanonicalSshRelayRuntimeManifest(createSshRelayArtifactTestManifest()) + ).toThrow(/unsigned|signature|field/i) + + const extra = unsignedManifest() + extra.latest = true + expect(() => assembleCanonicalSshRelayRuntimeManifest(extra)).toThrow(/unrecognized|field/i) + + const canonical = assembleCanonicalSshRelayRuntimeManifest(unsignedManifest()).canonicalBytes + expect(() => + parseCanonicalSshRelayRuntimeManifestBytes(Buffer.concat([canonical, Buffer.from('\n')])) + ).toThrow(/canonical/i) + expect(() => parseCanonicalSshRelayRuntimeManifestBytes(Buffer.from([0xff]))).toThrow(/utf-?8/i) + }) + + it('fails closed on inconsistent identity, archive, metadata, and native trust content', () => { + const mutations = [ + (manifest) => { + manifest.tuples[0].contentId = `sha256:${'f'.repeat(64)}` + }, + (manifest) => { + manifest.tuples[0].archive.fileCount += 1 + }, + (manifest) => { + manifest.tuples[0].metadataAssets.sbom.name = 'latest.json' + }, + (manifest) => { + manifest.tuples[0].nativeVerification.files[0].sha256 = `sha256:${'f'.repeat(64)}` + } + ] + for (const mutate of mutations) { + const manifest = unsignedManifest() + mutate(manifest) + expect(() => assembleCanonicalSshRelayRuntimeManifest(manifest)).toThrow() + } + }) + + it('rejects duplicate tuples and invalid release/timestamp identities', () => { + const duplicate = unsignedManifest() + duplicate.tuples.push(structuredClone(duplicate.tuples[0])) + expect(() => assembleCanonicalSshRelayRuntimeManifest(duplicate)).toThrow(/duplicate tuple/i) + + const channel = unsignedManifest() + channel.build.channel = 'stable' + expect(() => assembleCanonicalSshRelayRuntimeManifest(channel)).toThrow(/release|build/i) + + const timestamp = unsignedManifest() + timestamp.createdAt = '2026-07-14T00:00:00Z' + expect(() => assembleCanonicalSshRelayRuntimeManifest(timestamp)).toThrow(/timestamp/i) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-manifest-signing-handoff.mjs b/config/scripts/ssh-relay-runtime-manifest-signing-handoff.mjs new file mode 100644 index 00000000000..3fe2cc39431 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-manifest-signing-handoff.mjs @@ -0,0 +1,173 @@ +import { createHash } from 'node:crypto' + +import nacl from 'tweetnacl' + +import { + parseCanonicalSshRelayRuntimeManifestBytes, + SSH_RELAY_RUNTIME_MAX_CANONICAL_MANIFEST_BYTES +} from './ssh-relay-runtime-manifest-assembly.mjs' + +const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u +const SIGNATURE_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/u +const MAX_SIGNATURES = 4 + +export const SSH_RELAY_RUNTIME_MANIFEST_SIGNING_LIMITS = Object.freeze({ + maximumCanonicalBytes: SSH_RELAY_RUNTIME_MAX_CANONICAL_MANIFEST_BYTES +}) + +function assertObject(value, label) { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`SSH relay runtime manifest signing ${label} must be an object`) + } +} + +function assertExactFields(value, fields, label) { + assertObject(value, label) + const actual = Object.keys(value).sort() + const expected = [...fields].sort() + if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) { + throw new Error(`SSH relay runtime manifest signing ${label} has unexpected or missing fields`) + } +} + +function sha256(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +function canonicalSignatureBytes(value) { + if (typeof value !== 'string' || !SIGNATURE_PATTERN.test(value)) { + throw new Error('SSH relay runtime manifest returned signature is not canonical base64') + } + const bytes = Buffer.from(value, 'base64') + if (bytes.length !== nacl.sign.signatureLength || bytes.toString('base64') !== value) { + throw new Error('SSH relay runtime manifest returned signature must contain exactly 64 bytes') + } + return bytes +} + +function validateRequest(request) { + assertExactFields( + request, + ['algorithm', 'canonicalBytes', 'payloadSha256', 'payloadSize'], + 'request' + ) + if (request.algorithm !== 'ed25519-v1') { + throw new Error('SSH relay runtime manifest signing request algorithm is unsupported') + } + if (!Buffer.isBuffer(request.canonicalBytes) && !(request.canonicalBytes instanceof Uint8Array)) { + throw new Error('SSH relay runtime manifest signing request payload must be bytes') + } + const canonicalBytes = Buffer.from(request.canonicalBytes) + if ( + !Number.isSafeInteger(request.payloadSize) || + request.payloadSize <= 0 || + request.payloadSize > SSH_RELAY_RUNTIME_MANIFEST_SIGNING_LIMITS.maximumCanonicalBytes || + request.payloadSize !== canonicalBytes.length + ) { + throw new Error('SSH relay runtime manifest signing request payload size is inconsistent') + } + if ( + !DIGEST_PATTERN.test(request.payloadSha256) || + request.payloadSha256 !== sha256(canonicalBytes) + ) { + throw new Error('SSH relay runtime manifest signing request payload SHA-256 is inconsistent') + } + const manifest = parseCanonicalSshRelayRuntimeManifestBytes(canonicalBytes) + return { canonicalBytes, manifest } +} + +export function sshRelayRuntimeManifestKeyId(publicKey) { + if (!(publicKey instanceof Uint8Array) || publicKey.byteLength !== nacl.sign.publicKeyLength) { + throw new Error('SSH relay runtime manifest Ed25519 public key must contain exactly 32 bytes') + } + return sha256(publicKey) +} + +export function createSshRelayRuntimeManifestSigningRequest(input) { + if (!Buffer.isBuffer(input) && !(input instanceof Uint8Array)) { + throw new Error('SSH relay runtime manifest signing payload must be bytes') + } + const canonicalBytes = Buffer.from(input) + if ( + canonicalBytes.length === 0 || + canonicalBytes.length > SSH_RELAY_RUNTIME_MANIFEST_SIGNING_LIMITS.maximumCanonicalBytes + ) { + throw new Error('SSH relay runtime manifest signing payload exceeds its size limit') + } + parseCanonicalSshRelayRuntimeManifestBytes(canonicalBytes) + return { + algorithm: 'ed25519-v1', + canonicalBytes, + payloadSha256: sha256(canonicalBytes), + payloadSize: canonicalBytes.length + } +} + +function acceptedKeyMap(input) { + if (!Array.isArray(input) || input.length === 0 || input.length > MAX_SIGNATURES) { + throw new Error( + 'SSH relay runtime manifest signing accepted keys must be a bounded non-empty array' + ) + } + const keys = new Map() + for (const [index, key] of input.entries()) { + assertExactFields(key, ['keyId', 'publicKey'], `accepted key ${index}`) + const derivedKeyId = sshRelayRuntimeManifestKeyId(key.publicKey) + if (key.keyId !== derivedKeyId) { + throw new Error('SSH relay runtime manifest signing accepted key ID disagrees with its bytes') + } + if (keys.has(key.keyId)) { + throw new Error( + `SSH relay runtime manifest signing has a duplicate accepted key: ${key.keyId}` + ) + } + keys.set(key.keyId, key.publicKey) + } + return keys +} + +function verifySigningResults(results, keys, canonicalBytes) { + if (!Array.isArray(results) || results.length === 0 || results.length > MAX_SIGNATURES) { + throw new Error('SSH relay runtime manifest signing results must be a bounded non-empty array') + } + const seen = new Set() + return results.map((result, index) => { + // Why: the protected signer may return only the authenticated key identity and signature bytes. + assertExactFields(result, ['keyId', 'signature'], `result ${index}`) + if (!DIGEST_PATTERN.test(result.keyId)) { + throw new Error('SSH relay runtime manifest signing result key ID is malformed') + } + if (seen.has(result.keyId)) { + throw new Error( + `SSH relay runtime manifest signing has a duplicate result key: ${result.keyId}` + ) + } + seen.add(result.keyId) + const publicKey = keys.get(result.keyId) + if (!publicKey) { + throw new Error(`SSH relay runtime manifest signing returned an unknown key: ${result.keyId}`) + } + const signature = canonicalSignatureBytes(result.signature) + if (!nacl.sign.detached.verify(canonicalBytes, signature, publicKey)) { + throw new Error( + `SSH relay runtime manifest signing returned an invalid signature: ${result.keyId}` + ) + } + return { algorithm: 'ed25519-v1', keyId: result.keyId, signature: result.signature } + }) +} + +export function finalizeSshRelayRuntimeManifestSigningHandoff({ + request, + signingResults, + acceptedKeys +}) { + const { canonicalBytes, manifest: unsignedManifest } = validateRequest(request) + const keys = acceptedKeyMap(acceptedKeys) + const signatures = verifySigningResults(signingResults, keys, canonicalBytes).sort( + (left, right) => (left.keyId < right.keyId ? -1 : left.keyId > right.keyId ? 1 : 0) + ) + const manifest = { ...unsignedManifest, signatures } + const bytes = Buffer.from(JSON.stringify(manifest), 'utf8') + return { manifest, bytes, sha256: sha256(bytes) } +} diff --git a/config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs b/config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs new file mode 100644 index 00000000000..91b93ad487a --- /dev/null +++ b/config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs @@ -0,0 +1,168 @@ +import nacl from 'tweetnacl' +import { describe, expect, it } from 'vitest' + +import { createSshRelayArtifactTestManifest } from '../../src/main/ssh/ssh-relay-artifact-test-manifest.ts' +import { verifySshRelayArtifactManifest } from '../../src/main/ssh/ssh-relay-manifest-signature.ts' +import { assembleCanonicalSshRelayRuntimeManifest } from './ssh-relay-runtime-manifest-assembly.mjs' +import { + SSH_RELAY_RUNTIME_MANIFEST_SIGNING_LIMITS, + createSshRelayRuntimeManifestSigningRequest, + finalizeSshRelayRuntimeManifestSigningHandoff, + sshRelayRuntimeManifestKeyId +} from './ssh-relay-runtime-manifest-signing-handoff.mjs' + +const keyPair = nacl.sign.keyPair.fromSeed(Uint8Array.from({ length: 32 }, (_, index) => index)) +const nextKeyPair = nacl.sign.keyPair.fromSeed( + Uint8Array.from({ length: 32 }, (_, index) => 31 - index) +) + +function assembly() { + const { signatures: _signatures, ...unsigned } = createSshRelayArtifactTestManifest() + return assembleCanonicalSshRelayRuntimeManifest(unsigned) +} + +function signingResult(request, pair = keyPair) { + return { + keyId: sshRelayRuntimeManifestKeyId(pair.publicKey), + signature: Buffer.from(nacl.sign.detached(request.canonicalBytes, pair.secretKey)).toString( + 'base64' + ) + } +} + +function acceptedKey(pair = keyPair) { + return { keyId: sshRelayRuntimeManifestKeyId(pair.publicKey), publicKey: pair.publicKey } +} + +describe('SSH relay runtime manifest signing handoff', () => { + it('creates a bounded request over the exact canonical unsigned bytes', () => { + const canonical = assembly().canonicalBytes + const request = createSshRelayRuntimeManifestSigningRequest(canonical) + + expect(SSH_RELAY_RUNTIME_MANIFEST_SIGNING_LIMITS).toEqual({ + maximumCanonicalBytes: 32 * 1024 * 1024 + }) + expect(Object.keys(request).sort()).toEqual([ + 'algorithm', + 'canonicalBytes', + 'payloadSha256', + 'payloadSize' + ]) + expect(request).toMatchObject({ + algorithm: 'ed25519-v1', + payloadSha256: 'sha256:e78bf4416628a91055035dc7926035cbf633f29d3618be34e041c6dc5e0794fb', + payloadSize: canonical.length + }) + expect(request.canonicalBytes).toEqual(canonical) + }) + + it('accepts only key ID plus a verified returned signature', () => { + const request = createSshRelayRuntimeManifestSigningRequest(assembly().canonicalBytes) + const result = signingResult(request) + expect(Object.keys(result).sort()).toEqual(['keyId', 'signature']) + + const finalized = finalizeSshRelayRuntimeManifestSigningHandoff({ + request, + signingResults: [result], + acceptedKeys: [acceptedKey()] + }) + + expect(finalized.manifest.signatures).toEqual([{ algorithm: 'ed25519-v1', ...result }]) + expect(finalized.bytes.at(-1)).not.toBe('\n'.charCodeAt(0)) + expect(JSON.parse(finalized.bytes.toString('utf8'))).toEqual(finalized.manifest) + expect(verifySshRelayArtifactManifest(finalized.manifest, [acceptedKey()])).toEqual( + finalized.manifest + ) + }) + + it('reconstructs canonical content and rejects request drift', () => { + const request = createSshRelayRuntimeManifestSigningRequest(assembly().canonicalBytes) + const result = signingResult(request) + + for (const mutate of [ + (copy) => { + copy.payloadSize += 1 + }, + (copy) => { + copy.payloadSha256 = `sha256:${'f'.repeat(64)}` + }, + (copy) => { + copy.algorithm = 'rsa-v1' + }, + (copy) => { + copy.canonicalBytes = Buffer.concat([copy.canonicalBytes, Buffer.from('\n')]) + } + ]) { + const copy = { ...request, canonicalBytes: Buffer.from(request.canonicalBytes) } + mutate(copy) + expect(() => + finalizeSshRelayRuntimeManifestSigningHandoff({ + request: copy, + signingResults: [result], + acceptedKeys: [acceptedKey()] + }) + ).toThrow() + } + }) + + it('fails closed on malformed, unknown, duplicate, or invalid returned signatures', () => { + const request = createSshRelayRuntimeManifestSigningRequest(assembly().canonicalBytes) + const result = signingResult(request) + const cases = [ + { signingResults: [], acceptedKeys: [acceptedKey()] }, + { signingResults: [{ ...result, extra: true }], acceptedKeys: [acceptedKey()] }, + { + signingResults: [{ ...result, signature: Buffer.alloc(63).toString('base64') }], + acceptedKeys: [acceptedKey()] + }, + { signingResults: [result], acceptedKeys: [acceptedKey(nextKeyPair)] }, + { + signingResults: [{ ...result, signature: Buffer.alloc(64).toString('base64') }], + acceptedKeys: [acceptedKey()] + }, + { signingResults: [result, structuredClone(result)], acceptedKeys: [acceptedKey()] } + ] + for (const testCase of cases) { + expect(() => + finalizeSshRelayRuntimeManifestSigningHandoff({ request, ...testCase }) + ).toThrow() + } + }) + + it('sorts verified dual signatures by key ID for deterministic final bytes', () => { + const request = createSshRelayRuntimeManifestSigningRequest(assembly().canonicalBytes) + const results = [ + signingResult(request, nextKeyPair), + signingResult(request, keyPair) + ].toReversed() + const acceptedKeys = [acceptedKey(nextKeyPair), acceptedKey(keyPair)].toReversed() + + const first = finalizeSshRelayRuntimeManifestSigningHandoff({ + request, + signingResults: results, + acceptedKeys + }) + const second = finalizeSshRelayRuntimeManifestSigningHandoff({ + request, + signingResults: results.toReversed(), + acceptedKeys: acceptedKeys.toReversed() + }) + + expect(first.bytes).toEqual(second.bytes) + expect(first.manifest.signatures.map((entry) => entry.keyId)).toEqual( + first.manifest.signatures.map((entry) => entry.keyId).toSorted() + ) + }) + + it('rejects empty, non-byte, and oversized signing payloads before handoff', () => { + expect(() => createSshRelayRuntimeManifestSigningRequest(Buffer.alloc(0))).toThrow( + /empty|size/i + ) + expect(() => createSshRelayRuntimeManifestSigningRequest('{}')).toThrow(/bytes/i) + expect(() => + createSshRelayRuntimeManifestSigningRequest( + Buffer.alloc(SSH_RELAY_RUNTIME_MANIFEST_SIGNING_LIMITS.maximumCanonicalBytes + 1) + ) + ).toThrow(/size|limit/i) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-manifest-validation.mjs b/config/scripts/ssh-relay-runtime-manifest-validation.mjs new file mode 100644 index 00000000000..575f9be7073 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-manifest-validation.mjs @@ -0,0 +1,369 @@ +import { z } from 'zod' + +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' + +const MAX_ARCHIVE_SIZE = 100 * 1024 * 1024 +const MAX_EXPANDED_SIZE = 350 * 1024 * 1024 +const MAX_FILE_SIZE = 250 * 1024 * 1024 +const MAX_ENTRIES = 5_000 +const MAX_PATH_BYTES = 240 +const MAX_PATH_DEPTH = 32 +const VERSION = /^\d+\.\d+(?:\.\d+)?(?:[-+][0-9A-Za-z.-]+)?$/u +const NUMERIC_VERSION = /^\d+\.\d+(?:\.\d+)?$/u +const OPENSSH_VERSION = /^\d+\.\d+p\d+$/u +const ASSET_NAME = /^[A-Za-z0-9._-]+$/u +const PORTABLE_PATH = /^[A-Za-z0-9._@+/-]+$/u +const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu +const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u + +const digestSchema = z.string().regex(/^sha256:[0-9a-f]{64}$/u) +const safeSizeSchema = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER) +const versionSchema = z.string().regex(VERSION).max(64) +const numericVersionSchema = z.string().regex(NUMERIC_VERSION).max(64) +const timestampSchema = z + .string() + .regex(TIMESTAMP) + .refine( + (value) => { + const milliseconds = Date.parse(value) + return !Number.isNaN(milliseconds) && new Date(milliseconds).toISOString() === value + }, + { message: 'invalid canonical UTC timestamp' } + ) + +const directoryEntrySchema = z + .object({ path: z.string(), type: z.literal('directory'), mode: z.literal(0o755) }) + .strict() +const fileEntrySchema = z + .object({ + path: z.string(), + type: z.literal('file'), + role: z.enum([ + 'node', + 'relay', + 'relay-watcher', + 'node-pty-native', + 'parcel-watcher-native', + 'native-runtime', + 'runtime-javascript', + 'license' + ]), + size: safeSizeSchema.max(MAX_FILE_SIZE), + mode: z.union([z.literal(0o644), z.literal(0o755)]), + sha256: digestSchema + }) + .strict() +const entrySchema = z.discriminatedUnion('type', [directoryEntrySchema, fileEntrySchema]) + +const glibcSchema = z + .object({ + family: z.literal('glibc'), + minimumVersion: numericVersionSchema, + minimumLibstdcxxVersion: numericVersionSchema, + minimumGlibcxxVersion: numericVersionSchema + }) + .strict() +const muslSchema = z + .object({ + family: z.literal('musl'), + minimumVersion: numericVersionSchema, + minimumLibstdcxxVersion: z.null(), + minimumGlibcxxVersion: z.null() + }) + .strict() +const compatibilitySchema = z.discriminatedUnion('kind', [ + z + .object({ + kind: z.literal('linux'), + minimumKernelVersion: numericVersionSchema, + libc: z.discriminatedUnion('family', [glibcSchema, muslSchema]) + }) + .strict(), + z.object({ kind: z.literal('darwin'), minimumVersion: numericVersionSchema }).strict(), + z + .object({ + kind: z.literal('windows'), + minimumBuild: safeSizeSchema, + minimumOpenSshVersion: z.string().regex(OPENSSH_VERSION).max(64), + minimumPowerShellVersion: numericVersionSchema, + minimumDotNetFrameworkRelease: safeSizeSchema + }) + .strict() +]) + +const metadataAssetSchema = z + .object({ name: z.string().regex(ASSET_NAME), size: safeSizeSchema, sha256: digestSchema }) + .strict() +const nativeVerificationSchema = z + .object({ + policy: z.enum(['linux-hash-only-v1', 'apple-developer-id-v1', 'signpath-authenticode-v1']), + tool: z + .object({ + name: z.string().regex(ASSET_NAME), + version: z + .string() + .regex(/^[\x20-\x7e]+$/u) + .max(64) + }) + .strict(), + verifiedAt: timestampSchema, + files: z + .array(z.object({ path: z.string(), sha256: digestSchema }).strict()) + .min(1) + .max(MAX_ENTRIES) + }) + .strict() + +const tupleSchema = z + .object({ + tupleId: z.enum([ + 'linux-x64-glibc', + 'linux-arm64-glibc', + 'linux-x64-musl', + 'linux-arm64-musl', + 'darwin-x64', + 'darwin-arm64', + 'win32-x64', + 'win32-arm64' + ]), + os: z.enum(['linux', 'darwin', 'win32']), + architecture: z.enum(['x64', 'arm64']), + compatibility: compatibilitySchema, + nodeVersion: versionSchema, + dependencies: z + .object({ nodePtyVersion: versionSchema, parcelWatcherVersion: versionSchema }) + .strict(), + entries: z.array(entrySchema).min(1).max(MAX_ENTRIES), + contentId: digestSchema, + archive: z + .object({ + name: z.string().regex(ASSET_NAME), + size: safeSizeSchema.max(MAX_ARCHIVE_SIZE), + expandedSize: safeSizeSchema.max(MAX_EXPANDED_SIZE), + fileCount: safeSizeSchema.max(MAX_ENTRIES), + sha256: digestSchema + }) + .strict(), + metadataAssets: z + .object({ sbom: metadataAssetSchema, provenance: metadataAssetSchema }) + .strict(), + nativeVerification: nativeVerificationSchema + }) + .strict() + +const unsignedManifestSchema = z + .object({ + schemaVersion: z.literal(1), + build: z + .object({ + tag: z.string().max(64), + channel: z.enum(['stable', 'rc', 'perf']), + version: z.string().max(64), + relayProtocolVersion: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER) + }) + .strict(), + createdAt: timestampSchema, + tuples: z.array(tupleSchema).min(1).max(8) + }) + .strict() + +function assertSafePath(path) { + if (!path || Buffer.byteLength(path, 'utf8') > MAX_PATH_BYTES || !PORTABLE_PATH.test(path)) { + throw new Error(`SSH relay runtime manifest contains an unsafe artifact path: ${path}`) + } + const segments = path.split('/') + if (path.startsWith('/') || segments.length > MAX_PATH_DEPTH) { + throw new Error(`SSH relay runtime manifest contains an unsafe artifact path: ${path}`) + } + for (const segment of segments) { + if ( + !segment || + segment === '.' || + segment === '..' || + segment.endsWith('.') || + segment.endsWith(' ') || + WINDOWS_DEVICE_NAME.test(segment) + ) { + throw new Error(`SSH relay runtime manifest contains an unsafe artifact path: ${path}`) + } + } +} + +function expectedTupleId(tuple) { + if (tuple.os === 'linux' && tuple.compatibility.kind === 'linux') { + return `linux-${tuple.architecture}-${tuple.compatibility.libc.family}` + } + return `${tuple.os}-${tuple.architecture}` +} + +function expectedArchiveName(tuple) { + const extension = tuple.os === 'win32' ? 'zip' : 'tar.xz' + return `orca-ssh-relay-runtime-v1-${tuple.tupleId}-${tuple.contentId.slice(7)}.${extension}` +} + +function assertTupleEntries(tuple) { + const exactPaths = new Set() + const foldedPaths = new Set() + const directories = new Set() + for (const entry of tuple.entries) { + assertSafePath(entry.path) + const folded = entry.path.toLowerCase() + if (exactPaths.has(entry.path) || foldedPaths.has(folded)) { + throw new Error(`SSH relay runtime manifest has a colliding path: ${entry.path}`) + } + exactPaths.add(entry.path) + foldedPaths.add(folded) + if (entry.type === 'directory') { + directories.add(entry.path) + } + } + for (const entry of tuple.entries) { + const separator = entry.path.lastIndexOf('/') + if (separator > 0 && !directories.has(entry.path.slice(0, separator))) { + throw new Error(`SSH relay runtime manifest has an undeclared parent: ${entry.path}`) + } + } + + const files = tuple.entries.filter((entry) => entry.type === 'file') + const expectedPaths = new Map([ + ['node', tuple.os === 'win32' ? 'bin/node.exe' : 'bin/node'], + ['relay', 'relay.js'], + ['relay-watcher', 'relay-watcher.js'] + ]) + for (const role of [ + 'node', + 'relay', + 'relay-watcher', + 'node-pty-native', + 'parcel-watcher-native' + ]) { + const matching = files.filter((entry) => entry.role === role) + if ( + matching.length !== 1 || + (expectedPaths.has(role) && matching[0].path !== expectedPaths.get(role)) + ) { + throw new Error(`SSH relay runtime manifest requires one valid ${role} entry`) + } + } + if ( + !files.some((entry) => entry.path === 'node_modules/node-pty/package.json') || + !files.some((entry) => entry.path === 'node_modules/@parcel/watcher/package.json') || + !files.some((entry) => entry.role === 'license') + ) { + throw new Error('SSH relay runtime manifest is missing required package or license content') + } + if (files.find((entry) => entry.role === 'node').mode !== 0o755) { + throw new Error('SSH relay runtime manifest bundled Node must be executable') + } + return files +} + +function assertNativeVerification(tuple, files) { + const watcherPackage = + tuple.os === 'linux' && tuple.compatibility.kind === 'linux' + ? `node_modules/@parcel/watcher-linux-${tuple.architecture}-${tuple.compatibility.libc.family}` + : `node_modules/@parcel/watcher-${tuple.os}-${tuple.architecture}` + for (const entry of tuple.entries) { + if ( + entry.path.startsWith('node_modules/@parcel/watcher-') && + entry.path !== watcherPackage && + !entry.path.startsWith(`${watcherPackage}/`) + ) { + throw new Error('SSH relay runtime manifest contains an extra native watcher package') + } + } + const expectedPolicy = + tuple.os === 'linux' + ? 'linux-hash-only-v1' + : tuple.os === 'darwin' + ? 'apple-developer-id-v1' + : 'signpath-authenticode-v1' + if (tuple.nativeVerification.policy !== expectedPolicy) { + throw new Error('SSH relay runtime manifest has the wrong native verification policy') + } + + const attested = new Map() + for (const file of tuple.nativeVerification.files) { + assertSafePath(file.path) + if (attested.has(file.path)) { + throw new Error(`SSH relay runtime manifest has duplicate native attestation: ${file.path}`) + } + attested.set(file.path, file.sha256) + } + const nativeRoles = new Set([ + 'node', + 'node-pty-native', + 'parcel-watcher-native', + 'native-runtime' + ]) + for (const file of files) { + if (nativeRoles.has(file.role) && attested.get(file.path) !== file.sha256) { + throw new Error(`SSH relay runtime manifest has an invalid attested hash: ${file.path}`) + } + } + for (const [path, sha256] of attested) { + const file = files.find((entry) => entry.path === path) + if (!file || file.sha256 !== sha256) { + throw new Error(`SSH relay runtime manifest attests missing or mismatched content: ${path}`) + } + } +} + +function assertTupleConsistency(tuple) { + const compatibilityKind = tuple.os === 'win32' ? 'windows' : tuple.os + if (expectedTupleId(tuple) !== tuple.tupleId || tuple.compatibility.kind !== compatibilityKind) { + throw new Error(`SSH relay runtime manifest tuple identity is inconsistent: ${tuple.tupleId}`) + } + const files = assertTupleEntries(tuple) + assertNativeVerification(tuple, files) + const expandedSize = files.reduce((total, entry) => total + entry.size, 0) + if (tuple.archive.fileCount !== files.length || tuple.archive.expandedSize !== expandedSize) { + throw new Error(`SSH relay runtime manifest archive metadata is inconsistent: ${tuple.tupleId}`) + } + if (tuple.contentId !== computeSshRelayRuntimeContentId(tuple)) { + throw new Error(`SSH relay runtime manifest content identity is inconsistent: ${tuple.tupleId}`) + } + if (tuple.archive.name !== expectedArchiveName(tuple)) { + throw new Error(`SSH relay runtime manifest archive name is inconsistent: ${tuple.tupleId}`) + } + const metadataPrefix = `orca-ssh-relay-runtime-${tuple.tupleId}` + if ( + tuple.metadataAssets.sbom.name !== `${metadataPrefix}.spdx.json` || + tuple.metadataAssets.provenance.name !== `${metadataPrefix}.provenance.json` + ) { + throw new Error( + `SSH relay runtime manifest metadata asset name is inconsistent: ${tuple.tupleId}` + ) + } +} + +function parseReleaseTag(tag) { + for (const [pattern, channel] of [ + [/^v(\d+\.\d+\.\d+)$/u, 'stable'], + [/^v(\d+\.\d+\.\d+-rc\.\d+)$/u, 'rc'], + [/^v(\d+\.\d+\.\d+-rc\.\d+\.perf)$/u, 'perf'] + ]) { + const match = pattern.exec(tag) + if (match) { + return { channel, version: match[1] } + } + } + throw new Error('SSH relay runtime manifest release tag is invalid') +} + +export function parseSshRelayRuntimeUnsignedManifest(input) { + const parsed = unsignedManifestSchema.parse(input) + const release = parseReleaseTag(parsed.build.tag) + if (release.channel !== parsed.build.channel || release.version !== parsed.build.version) { + throw new Error('SSH relay runtime manifest build identity does not match its exact release') + } + const tupleIds = new Set() + for (const tuple of parsed.tuples) { + if (tupleIds.has(tuple.tupleId)) { + throw new Error(`SSH relay runtime manifest has a duplicate tuple: ${tuple.tupleId}`) + } + tupleIds.add(tuple.tupleId) + assertTupleConsistency(tuple) + } + return parsed +} diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index e91accd2153..20bf57a7d90 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -107,6 +107,14 @@ describe('SSH relay runtime artifact workflow', () => { 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-manifest-validation\.mjs/g) + ).toHaveLength(2) + for (const moduleName of ['manifest-assembly', 'manifest-signing-handoff']) { + 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) 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 e0fc39ba30f..eea4e72b1c7 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 @@ -64,10 +64,11 @@ complete a box. Windows compatibility-kind parity is closed locally and on all six target-native jobs in exact-head run [29393022768](https://github.com/stablyai/orca/actions/runs/29393022768) under - `E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001` and `E-M4-WINDOWS-MANIFEST-PARITY-CI-001`. Next, - canonicalize only validated unsigned manifest bytes, create the bounded signing request, and - verify returned signatures without production credentials. No production workflow, - publication, desktop consumer, signing credential, or tuple is connected. + `E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001` and `E-M4-WINDOWS-MANIFEST-PARITY-CI-001`. + Disconnected canonical assembly and signing-handoff modules are implemented locally. Focused + and broad local gates pass under `E-M4-MANIFEST-HANDOFF-LOCAL-001` and + `E-M4-MANIFEST-HANDOFF-LOCAL-002`; exact-head native CI remains pending. No production + workflow, publication, desktop consumer, signing credential, or tuple is connected. ## Work packages, in required order @@ -113,7 +114,10 @@ complete a box. - [ ] **In progress — 2026-07-15, Codex implementation owner:** add disconnected canonical unsigned manifest assembly, a bounded signing request, and returned-signature verification. Keep final detached-signature asset encoding, production credentials, publication, desktop consumers, and - every tuple outside this slice until their contracts and gates are explicit. + every tuple outside this slice until their contracts and gates are explicit. Purpose-named RED + and GREEN suites plus broad local regressions are recorded under + `E-M4-MANIFEST-HANDOFF-LOCAL-RED-001`, `E-M4-MANIFEST-HANDOFF-LOCAL-001`, and + `E-M4-MANIFEST-HANDOFF-LOCAL-002`; exact-head native 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 2514413fc4f..08af19f089a 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 @@ -9,7 +9,7 @@ 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 credential-free canonical manifest/signing handoff — **In progress — 2026-07-15, Codex implementation owner**. The disconnected release-DAG, aggregate/read-back byte boundaries, and Windows artifact/manifest parity prerequisite are closed locally and on all six native build jobs under E-M4-RELEASE-DAG-LOCAL-001, E-M4-RELEASE-DAG-CI-001, E-M4-AGGREGATE-READBACK-LOCAL-001, E-M4-AGGREGATE-READBACK-CI-001, E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001, and E-M4-WINDOWS-MANIFEST-PARITY-CI-001. The active package is limited to canonical unsigned-manifest bytes, a bounded signing request, and returned-signature verification; it may not choose final detached-signature asset encoding, publish, connect desktop consumers, use production signing credentials, 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 run [29393022768](https://github.com/stablyai/orca/actions/runs/29393022768) at `c68a99039e4633d41e21fb30afdfeaa06f776369` executes the corrected compatibility/schema/workflow contracts under Node 24.18.0 and rebuilds, verifies, smokes, compares, and uploads all six unpublished target-native artifacts. Both regenerated Windows identities use compatibility `kind: "windows"`; x64 content ID is `sha256:4224aee6de820a94263f05ebfc58edd421921431e8cbdf1d5ec6ddcf783afc26` and arm64 content ID is `sha256:02edf462be83c2864a89546d5344d348f9e07ce10964660342608a8c614e47db`. Both Linux supplemental userland jobs and the Windows x64 floor job pass. The workflow remains red only because Windows arm64 runner build 26200 is not the required build 26100, after complete runtime smoke settles in 8,382.8952 ms with 49,811,456-byte RSS. Golden E2E [29393022784](https://github.com/stablyai/orca/actions/runs/29393022784) and PR Checks [29393022761](https://github.com/stablyai/orca/actions/runs/29393022761) are green. The parity prerequisite is closed under E-M4-WINDOWS-MANIFEST-PARITY-CI-001; canonical manifest/signature-handoff RED tests are next. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, and native trust remain separately gated. Nothing is published or enabled, and legacy remains the production default.
      +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact-head artifact run [29393022768](https://github.com/stablyai/orca/actions/runs/29393022768) at `c68a99039e4633d41e21fb30afdfeaa06f776369` executes the corrected compatibility/schema/workflow contracts under Node 24.18.0 and rebuilds, verifies, smokes, compares, and uploads all six unpublished target-native artifacts. Both regenerated Windows identities use compatibility `kind: "windows"`; x64 content ID is `sha256:4224aee6de820a94263f05ebfc58edd421921431e8cbdf1d5ec6ddcf783afc26` and arm64 content ID is `sha256:02edf462be83c2864a89546d5344d348f9e07ce10964660342608a8c614e47db`. Both Linux supplemental userland jobs and the Windows x64 floor job pass. The workflow remains red only because Windows arm64 runner build 26200 is not the required build 26100, after complete runtime smoke settles in 8,382.8952 ms with 49,811,456-byte RSS. Golden E2E [29393022784](https://github.com/stablyai/orca/actions/runs/29393022784) and PR Checks [29393022761](https://github.com/stablyai/orca/actions/runs/29393022761) are green. The parity prerequisite is closed under E-M4-WINDOWS-MANIFEST-PARITY-CI-001. Disconnected canonical assembly and signing-handoff modules are now implemented; focused and broad local gates pass under E-M4-MANIFEST-HANDOFF-LOCAL-001 and E-M4-MANIFEST-HANDOFF-LOCAL-002, including 38 release-side files / 214 tests, 3 desktop files / 47 tests, syntax, typecheck, full lint, formatting, max-lines, and diff checks. Exact-head native CI remains pending. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, 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) @@ -73,8 +73,9 @@ same change as the work it records. canonical bytes only from fully validated inputs, create a bounded credential-free signing request, and reconstruct and verify the returned Ed25519 signature before emitting a final manifest. Final detached-signature asset encoding, production credentials, publication, desktop consumers, and - tuple enablement remain outside this slice. No broader Milestone 4 box is checked until the - corresponding implementation and exact tests exist. + tuple enablement remain outside this slice. Focused and broad local gates are green under + E-M4-MANIFEST-HANDOFF-LOCAL-001 and E-M4-MANIFEST-HANDOFF-LOCAL-002; exact-head native CI remains + pending. No broader Milestone 4 box is checked until its evidence exists. - 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 @@ -192,11 +193,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: add RED tests and implement disconnected canonical unsigned-manifest - assembly, a bounded credential-free signing request, and returned-signature reconstruction and - verification. Keep final detached-signature asset encoding, real native/manifest signing - credentials, endpoint trust, missing exact-floor snapshots, production workflow wiring, - publication, desktop consumers, and every tuple's enabled state outside this slice. +- Next required action: commit and push the locally verified disconnected handoff, then prove the exact + head on all six native artifact jobs plus PR Checks and Golden E2E before recording this package + complete. Keep final detached-signature asset encoding, real native/manifest 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 @@ -1537,6 +1538,9 @@ focused commands as their scripts/tests are introduced. - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-aggregate-input.test.mjs config/scripts/ssh-relay-runtime-draft-readback.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` (E-M4-AGGREGATE-READBACK-LOCAL-001; static native-job wiring only) - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-compatibility.test.mjs` (E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-RED-001, E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001; exact Windows compatibility discriminator and canonical vector) - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-compatibility.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs src/main/ssh/ssh-relay-artifact-schema.test.ts` (E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-RED-001, E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001; build/desktop contract parity and static native-job wiring) +- [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs` (E-M4-MANIFEST-HANDOFF-LOCAL-RED-001, E-M4-MANIFEST-HANDOFF-LOCAL-001) +- [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay*.test.mjs` (E-M4-MANIFEST-HANDOFF-LOCAL-002; all release-side SSH-relay contracts) +- [x] `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` (E-M4-MANIFEST-HANDOFF-LOCAL-002; desktop schema/signature parity) ### Commands/scripts that must be added or formally identified @@ -9219,6 +9223,109 @@ diff --check`. request, and returned-signature verification. Keep final detached-signature asset encoding, production credentials, publication, desktop consumers, and tuple enablement disconnected. +### E-M4-MANIFEST-HANDOFF-LOCAL-RED-001 — Release-side assembly and signing handoff are absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact base `63899c182f3a831d61fb1f6566a0d0318bca4192` plus uncommitted + purpose-named tests; draft PR [#8741](https://github.com/stablyai/orca/pull/8741). +- Runner/network: local macOS 26.2 build 25C56 arm64, Node v26.0.0 and pnpm 10.24.0; no network, + runner, signing credential, release, publication, desktop consumer, SSH host, or production call. +- Command: + + ```sh + /usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \ + config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs \ + config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs + ``` + +- Result: expected RED. Both suites fail before collecting tests because + `ssh-relay-runtime-manifest-assembly.mjs` is absent; the signing-handoff suite also depends on that + boundary. Vitest reports 2 failed files in 264 ms; `/usr/bin/time -l` records 1.12 seconds wall, + 131,710,976-byte maximum RSS, zero swaps, and 95,835,440-byte peak memory footprint. +- Oracle proved: the existing desktop canonical/signature contract has no release-side module that + assembles and re-parses the exact canonical unsigned bytes, bounds the credential-free signing + request, or verifies the returned key ID/signature before final-manifest emission. +- Does not prove: implementation correctness, byte parity, Ed25519 return verification, native + runner behavior, production credentials, publication, desktop embedding, SSH behavior, or an + enabled tuple. +- Follow-up: implement only the two disconnected release-side boundaries and retain every + production/default behavior and final detached-signature asset-encoding decision unchanged. + +### E-M4-MANIFEST-HANDOFF-LOCAL-001 — Disconnected canonical assembly and signature return verify locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact base `63899c182f3a831d61fb1f6566a0d0318bca4192` plus the uncommitted + manifest validation, assembly, signing-handoff, test, and static workflow wiring changes; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). +- Runner/network: local macOS 26.2 build 25C56 arm64, Node v26.0.0 and pnpm 10.24.0; no network, + runner, signing credential, release, publication, desktop consumer, SSH host, or production call. +- Command: + + ```sh + /usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 \ + config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs \ + config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs + ``` + +- Result: GREEN. Vitest reports 2 files / 11 tests passed in 1.35 seconds; `/usr/bin/time -l` + records 15.14 seconds wall, 188,792,832-byte maximum RSS, zero swaps, and 96,490,920-byte peak + memory footprint. +- Oracle proved: validated tuple projections produce byte-identical canonical unsigned bytes to the + desktop contract; signing requests bind exact bytes, size, SHA-256, and `ed25519-v1`; returned key + IDs and signatures are allowlisted, deduplicated, cryptographically verified, deterministically + ordered, and revalidated through the desktop signed-manifest verifier before final emission. +- Does not prove: the broader SSH-relay suite, typecheck/lint/formatting gates, native runner parity, + production credentials, final detached-signature asset encoding, publication, desktop embedding, + SSH behavior, or an enabled tuple. +- Follow-up: run broader local gates, then exact-head all-six native CI; keep every production and + rollout boundary disconnected. + +### E-M4-MANIFEST-HANDOFF-LOCAL-002 — Broad local manifest-handoff regressions and static gates + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact base `63899c182f3a831d61fb1f6566a0d0318bca4192` plus the uncommitted + manifest validation, assembly, signing-handoff, test, workflow, and checklist changes; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). +- Runner/network: local macOS 26.2 build 25C56 arm64, Node v26.0.0 and pnpm 10.24.0; no network, + signing credential, release, publication, desktop consumer, SSH host, or production call. The + repository's Node 24 requirement is intentionally deferred to the exact native CI jobs rather than + inferred from this Node 26 run. +- Commands and results: + - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay*.test.mjs` + — GREEN, 38 files / 214 tests in 8.97 seconds; 10.13 seconds wall, 188,219,392-byte maximum + RSS, zero swaps, and 95,589,680-byte peak memory footprint. + - `/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` + — GREEN, 3 files / 47 tests in 1.34 seconds; 2.40 seconds wall, 131,973,120-byte maximum RSS, + zero swaps, and 96,081,200-byte peak memory footprint. + - `node --check` for the validation, assembly, assembly-test, signing-handoff, and + signing-handoff-test modules — GREEN with no output. + - Focused `pnpm exec oxlint` for all six touched script/test files — GREEN with no findings. + - Focused `pnpm exec oxfmt --check` for all nine touched code/workflow/checklist files — GREEN in + 1,023 ms. + - `/usr/bin/time -l pnpm run typecheck` — GREEN in 9.44 seconds wall with 1,182,597,120-byte + maximum RSS and zero swaps. + - `pnpm run lint` — GREEN in 26.12 seconds wall. Full oxlint, switch exhaustiveness, styled + scrollbars, 41 reliability gates, 355-entry max-lines ratchet, bundled-skill guides, + localization catalog/parity, and localization coverage pass. The 26 printed warnings are in + untouched existing files; none is introduced by this package. + - `pnpm run check:max-lines-ratchet` — GREEN, 355 grandfathered suppressions and no new bypasses. + - `git diff --check` — GREEN with no output. + - Structural bound audit — a maximum 240-byte file projection is 407 bytes and native attestation + projection is 334 bytes; `8 * 5,000 * (407 + 334) = 29,640,000`, leaving 3,914,432 bytes beneath + the 32 MiB canonical/signing ceiling for tuple and build envelopes. +- Oracle proved: the disconnected handoff preserves all existing release-side SSH-relay contracts, + emits bytes accepted by the desktop schema/signature implementation, stays syntactically and + statically valid, introduces no formatting/max-lines regression, and has a bounded ceiling + consistent with the declared eight-tuple/5,000-entry schema. +- Does not prove: Node 24/native-runner behavior, target-native build non-regression, production + credentials, final detached-signature asset encoding, publication, desktop embedding, SSH + transfer/install behavior, or an enabled tuple. +- Follow-up: commit and push the exact locally verified package, then record the all-six native jobs, + PR Checks, and Golden E2E at that exact head. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -9276,12 +9383,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Add RED tests and implement credential-free canonical unsigned-manifest assembly, a bounded signing -request, and returned-signature reconstruction and verification. Do not choose final detached- -signature asset encoding, connect release publication, use production signing credentials, or add a -desktop consumer in this slice. 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. +Commit and push the locally verified credential-free manifest handoff, then prove that exact head on +all six native artifact jobs plus PR Checks and Golden E2E. Do not choose final detached-signature +asset encoding, connect release publication, use production signing credentials, or add a desktop +consumer in this slice. 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 From 127ddbcc154fbdf2641bc80d67a3d68d862ab1ca Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 03:08:23 -0400 Subject: [PATCH 112/267] docs(ssh): record manifest handoff native CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 22 +++-- ...github-release-implementation-checklist.md | 92 +++++++++++++++---- 2 files changed, 87 insertions(+), 27 deletions(-) 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 eea4e72b1c7..4811498570e 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 @@ -60,15 +60,18 @@ complete a box. 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. -- [ ] **WP3 active implementation — Build the credential-free canonical manifest/signing handoff.** +- [ ] **WP3 active implementation — Build the credential-free fail-closed aggregate boundary.** Windows compatibility-kind parity is closed locally and on all six target-native jobs in exact-head run [29393022768](https://github.com/stablyai/orca/actions/runs/29393022768) under `E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001` and `E-M4-WINDOWS-MANIFEST-PARITY-CI-001`. - Disconnected canonical assembly and signing-handoff modules are implemented locally. Focused - and broad local gates pass under `E-M4-MANIFEST-HANDOFF-LOCAL-001` and - `E-M4-MANIFEST-HANDOFF-LOCAL-002`; exact-head native CI remains pending. No production - workflow, publication, desktop consumer, signing credential, or tuple is connected. + Disconnected canonical assembly and signing-handoff modules are closed locally and on all six + Node 24 native jobs in exact-head run + [29395319239](https://github.com/stablyai/orca/actions/runs/29395319239) under + `E-M4-MANIFEST-HANDOFF-CI-001`. Next, connect only the credential-free verified aggregate + inputs, canonical signing request, verified signer return, and immutable final-manifest output + behind fail-closed tests. No production workflow, publication, desktop consumer, signing + credential, or tuple is connected. ## Work packages, in required order @@ -111,13 +114,18 @@ complete a box. - [x] Correct the Windows compatibility discriminator and `bin/node.exe` manifest parity; prove both regenerated Windows identities and all-six native regressions (`E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001`, `E-M4-WINDOWS-MANIFEST-PARITY-CI-001`). -- [ ] **In progress — 2026-07-15, Codex implementation owner:** add disconnected canonical unsigned +- [x] Add disconnected canonical unsigned manifest assembly, a bounded signing request, and returned-signature verification. Keep final detached-signature asset encoding, production credentials, publication, desktop consumers, and every tuple outside this slice until their contracts and gates are explicit. Purpose-named RED and GREEN suites plus broad local regressions are recorded under `E-M4-MANIFEST-HANDOFF-LOCAL-RED-001`, `E-M4-MANIFEST-HANDOFF-LOCAL-001`, and - `E-M4-MANIFEST-HANDOFF-LOCAL-002`; exact-head native CI is next. + `E-M4-MANIFEST-HANDOFF-LOCAL-002`; all-six Node 24 proof is recorded under + `E-M4-MANIFEST-HANDOFF-CI-001`. +- [ ] **In progress — 2026-07-15, Codex implementation owner:** add the disconnected, credential-free + fail-closed aggregate boundary from exact verified runtime inputs through canonical request and + verified final-manifest bytes. Keep native/manifest credentials, publication, desktop + consumers, and every tuple outside this slice. - [ ] 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 08af19f089a..64a89d92ebb 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 credential-free canonical manifest/signing handoff — **In progress — 2026-07-15, Codex implementation owner**. The disconnected release-DAG, aggregate/read-back byte boundaries, and Windows artifact/manifest parity prerequisite are closed locally and on all six native build jobs under E-M4-RELEASE-DAG-LOCAL-001, E-M4-RELEASE-DAG-CI-001, E-M4-AGGREGATE-READBACK-LOCAL-001, E-M4-AGGREGATE-READBACK-CI-001, E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001, and E-M4-WINDOWS-MANIFEST-PARITY-CI-001. The active package is limited to canonical unsigned-manifest bytes, a bounded signing request, and returned-signature verification; it may not choose final detached-signature asset encoding, publish, connect desktop consumers, use production signing credentials, 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 run [29393022768](https://github.com/stablyai/orca/actions/runs/29393022768) at `c68a99039e4633d41e21fb30afdfeaa06f776369` executes the corrected compatibility/schema/workflow contracts under Node 24.18.0 and rebuilds, verifies, smokes, compares, and uploads all six unpublished target-native artifacts. Both regenerated Windows identities use compatibility `kind: "windows"`; x64 content ID is `sha256:4224aee6de820a94263f05ebfc58edd421921431e8cbdf1d5ec6ddcf783afc26` and arm64 content ID is `sha256:02edf462be83c2864a89546d5344d348f9e07ce10964660342608a8c614e47db`. Both Linux supplemental userland jobs and the Windows x64 floor job pass. The workflow remains red only because Windows arm64 runner build 26200 is not the required build 26100, after complete runtime smoke settles in 8,382.8952 ms with 49,811,456-byte RSS. Golden E2E [29393022784](https://github.com/stablyai/orca/actions/runs/29393022784) and PR Checks [29393022761](https://github.com/stablyai/orca/actions/runs/29393022761) are green. The parity prerequisite is closed under E-M4-WINDOWS-MANIFEST-PARITY-CI-001. Disconnected canonical assembly and signing-handoff modules are now implemented; focused and broad local gates pass under E-M4-MANIFEST-HANDOFF-LOCAL-001 and E-M4-MANIFEST-HANDOFF-LOCAL-002, including 38 release-side files / 214 tests, 3 desktop files / 47 tests, syntax, typecheck, full lint, formatting, max-lines, and diff checks. Exact-head native CI remains pending. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, and native trust remain separately gated. Nothing is published or enabled, and legacy remains the production default.
      +Current phase: Milestone 4 / Work Package 3 credential-free fail-closed aggregate boundary — **In progress — 2026-07-15, Codex implementation owner**. The disconnected release-DAG, aggregate/read-back byte boundaries, Windows artifact/manifest parity, and canonical manifest/signing handoff are closed locally and on all six native build jobs under E-M4-RELEASE-DAG-LOCAL-001, E-M4-RELEASE-DAG-CI-001, E-M4-AGGREGATE-READBACK-LOCAL-001, E-M4-AGGREGATE-READBACK-CI-001, E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001, E-M4-WINDOWS-MANIFEST-PARITY-CI-001, E-M4-MANIFEST-HANDOFF-LOCAL-001, E-M4-MANIFEST-HANDOFF-LOCAL-002, and E-M4-MANIFEST-HANDOFF-CI-001. The active package is limited to connecting exact verified aggregate inputs to canonical signing request and verified immutable final-manifest bytes under credential-free tests; it may not publish, connect desktop consumers, use production native/manifest signing credentials, 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** — commit `14355dfe0583f634a6e86ada9e1afcf7abe7a8fb` adds disconnected release-side validation, canonical assembly, bounded request, accepted-key/signature verification, deterministic final manifest emission, and all-six workflow wiring. Exact-head artifact run [29395319239](https://github.com/stablyai/orca/actions/runs/29395319239) executes both new suites under Node 24.18.0 on all six native jobs; every build, smoke, equality, upload, both Linux supplemental userland jobs, and the Windows x64 floor job pass. The overall artifact run remains red only because the Windows arm64 floor job observes build 26200 rather than required build 26100 after successful 5,818.68 ms runtime smoke with 49,385,472-byte RSS. Golden E2E [29395319242](https://github.com/stablyai/orca/actions/runs/29395319242) and PR Checks [29395319119](https://github.com/stablyai/orca/actions/runs/29395319119) are green. The handoff package is closed under E-M4-MANIFEST-HANDOFF-CI-001. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, aggregate publication wiring, desktop embedding, 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) @@ -69,13 +69,15 @@ same change as the work it records. verification binds each input and returned byte to exact name, size, SHA-256, tuple, content identity, source draft, approved HTTPS asset origin, and exact HTTP 200 under E-M4-AGGREGATE-READBACK-LOCAL-001 and E-M4-AGGREGATE-READBACK-CI-001. -- Active package: Work Package 3 disconnected canonical unsigned-manifest/signing handoff. Assemble - canonical bytes only from fully validated inputs, create a bounded credential-free signing request, - and reconstruct and verify the returned Ed25519 signature before emitting a final manifest. Final - detached-signature asset encoding, production credentials, publication, desktop consumers, and - tuple enablement remain outside this slice. Focused and broad local gates are green under - E-M4-MANIFEST-HANDOFF-LOCAL-001 and E-M4-MANIFEST-HANDOFF-LOCAL-002; exact-head native CI remains - pending. No broader Milestone 4 box is checked until its evidence exists. +- Completed Work Package 3 gate: disconnected canonical assembly, bounded credential-free signing + request, accepted-key/signature verification, deterministic final bytes, and all-six Node 24 + workflow parity are green under E-M4-MANIFEST-HANDOFF-LOCAL-001, + E-M4-MANIFEST-HANDOFF-LOCAL-002, and E-M4-MANIFEST-HANDOFF-CI-001. +- Active package: Work Package 3 disconnected fail-closed aggregate boundary. Connect exact verified + runtime inputs to canonical unsigned bytes, a bounded signing request, verified signer return, and + immutable final-manifest output under credential-free tests. Production native/manifest signing + credentials, publication, desktop consumers, and tuple enablement remain outside this slice. No + broader Milestone 4 box is checked until its evidence exists. - 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 @@ -193,11 +195,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: commit and push the locally verified disconnected handoff, then prove the exact - head on all six native artifact jobs plus PR Checks and Golden E2E before recording this package - complete. Keep final detached-signature asset encoding, real native/manifest signing credentials, - endpoint trust, missing exact-floor snapshots, production workflow wiring, publication, desktop - consumers, and every tuple's enabled state outside this slice. +- Next required action: add RED tests for the disconnected credential-free aggregate boundary, then + connect exact verified runtime inputs through canonical request and verified immutable + final-manifest bytes. Keep real native/manifest signing credentials, endpoint trust, missing + exact-floor snapshots, production publication, desktop consumers, and every tuple's enabled state + outside this slice. ## Non-Negotiable Invariants @@ -9326,6 +9328,56 @@ diff --check`. - Follow-up: commit and push the exact locally verified package, then record the all-six native jobs, PR Checks, and Golden E2E at that exact head. +### E-M4-MANIFEST-HANDOFF-CI-001 — Canonical handoff passes all six Node 24 native jobs + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact commit `14355dfe0583f634a6e86ada9e1afcf7abe7a8fb`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). +- Runs: + - SSH Relay Runtime Artifacts + [29395319239](https://github.com/stablyai/orca/actions/runs/29395319239) — overall expected RED + only for the separately declared Windows arm64 build-floor mismatch. + - PR Checks [29395319119](https://github.com/stablyai/orca/actions/runs/29395319119) — GREEN in + 14 minutes 22 seconds, including lint, reliability/max-lines, typecheck, Git 2.25 matrix, full + tests, unpacked app build, and packaged CLI smoke. + - Golden E2E [29395319242](https://github.com/stablyai/orca/actions/runs/29395319242) — GREEN on + macOS and Linux. +- Native build/test cells, all GitHub-hosted at the exact source commit with Node v24.18.0: + + | Tuple | Requested / resolved image | Arch | Contract suite | Build job / duration | + | ----------------- | ----------------------------------------------------- | ----- | -------------------------------------------------- | -------------------- | + | linux-x64-glibc | `ubuntu-24.04` / `ubuntu24` `20260705.232.1` | X64 | 37 files / 210 tests, 4.18 s | 87287406149 / 3m50s | + | linux-arm64-glibc | `ubuntu-24.04-arm` / `ubuntu24-arm64` `20260706.52.2` | ARM64 | 37 files / 210 tests, 3.80 s | 87287406182 / 5m03s | + | darwin-x64 | `macos-15-intel` / `macos15` `20260629.0276.1` | X64 | 37 files / 210 tests, 16.01 s | 87287406168 / 5m20s | + | darwin-arm64 | `macos-15` / `macos15` `20260706.0213.1` | ARM64 | 37 files / 210 tests, 4.65 s | 87287406210 / 2m31s | + | win32-x64 | `windows-2022` / `win22` `20260706.237.1` | X64 | 38 files / 206 passed + 8 platform-skipped, 9.07 s | 87287406172 / 5m23s | + | win32-arm64 | `windows-11-arm` / `win11-arm64` `20260706.102.1` | ARM64 | 38 files / 206 passed + 8 platform-skipped, 8.78 s | 87287406175 / 8m48s | + +- Direct new-suite oracle: every native job syntax-checks both new test modules and passes manifest + assembly 5/5 plus signing handoff 6/6. Per-job combined new-suite test times are Linux x64 + 136/254 ms, Linux arm64 91/193 ms, macOS x64 262/523 ms, macOS arm64 43/90 ms, Windows x64 + 148/295 ms, and Windows arm64 96/179 ms. +- Supplemental/result details: + - Linux x64 and arm64 digest-pinned Rocky 8 userland jobs pass exact downloaded-byte verification, + runtime smoke, glibc 2.28, libstdc++ 6.0.25, and GLIBCXX 3.4.25; kernel 4.18 remains open because + the container shares the hosted kernel. + - Windows Server 2022 x64 build 20348 passes its declared floor and full runtime execution. + - Windows arm64 verifies 60 archive entries / 42 files / 85,213,511 expanded bytes and content ID + `sha256:02edf462be83c2864a89546d5344d348f9e07ce10964660342608a8c614e47db`, then completes Node, + ConPTY, watcher, and resource-settlement smoke in 5,818.68 ms with 49,385,472-byte RSS and + 7,741.5065 ms total verification. Its floor job then fails exactly because observed Windows build + 26200 does not equal required build 26100; platform and ARM64 checks pass. +- Oracle proved: the release-side validator, canonical byte projection, bounded request, signer-return + verification, deterministic final manifest, and workflow wiring execute under the repository's + exact Node version on Linux, macOS, and Windows x64/arm64 without regressing target-native build, + smoke, equality, artifact upload, PR, or Golden E2E behavior. +- Does not prove: the missing Windows arm64 26100 snapshot, kernel 4.18, macOS 13.5, real Apple or + SignPath returns, protected manifest credentials/approval, final aggregate publication, desktop + embedding, SSH transfer/install, or an enabled tuple. +- Follow-up: begin RED tests for the credential-free fail-closed aggregate boundary; retain all + production/default and credential gates. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -9383,12 +9435,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the locally verified credential-free manifest handoff, then prove that exact head on -all six native artifact jobs plus PR Checks and Golden E2E. Do not choose final detached-signature -asset encoding, connect release publication, use production signing credentials, or add a desktop -consumer in this slice. 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. +Add RED tests for the disconnected credential-free fail-closed aggregate boundary, then connect exact +verified runtime inputs through canonical request and verified immutable final-manifest bytes. Do not +connect release publication, use production signing credentials, or add a desktop consumer in this +slice. 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 From 5e0c776d017b3cf4d57011cb1f4cdc919bbc8782 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 03:35:31 -0400 Subject: [PATCH 113/267] build(ssh): add fail-closed manifest aggregate Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 8 + .../ssh-relay-runtime-aggregate-input.mjs | 81 +++-- ...ssh-relay-runtime-aggregate-input.test.mjs | 30 +- .../ssh-relay-runtime-manifest-aggregate.mjs | 291 ++++++++++++++++ ...-relay-runtime-manifest-aggregate.test.mjs | 315 ++++++++++++++++++ .../ssh-relay-runtime-workflow.test.mjs | 6 +- ...elease-implementation-checklist-summary.md | 9 +- ...github-release-implementation-checklist.md | 140 +++++++- 8 files changed, 841 insertions(+), 39 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-manifest-aggregate.mjs create mode 100644 config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 6f8cde69090..fc83d911068 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -158,6 +158,8 @@ jobs: node --check config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs node --check config/scripts/ssh-relay-runtime-manifest-signing-handoff.mjs node --check config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs + node --check config/scripts/ssh-relay-runtime-manifest-aggregate.mjs + node --check config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs pnpm exec vitest run --config config/vitest.config.ts \ config/scripts/ssh-relay-runtime-compatibility.test.mjs \ config/scripts/ssh-relay-node-release-verification.test.mjs \ @@ -186,6 +188,7 @@ jobs: config/scripts/ssh-relay-runtime-draft-readback.test.mjs \ config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs \ config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs \ + config/scripts/ssh-relay-runtime-manifest-aggregate.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 \ @@ -531,6 +534,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime manifest signing handoff syntax check failed' } node --check config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime manifest signing handoff test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-manifest-aggregate.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime manifest aggregate syntax check failed' } + node --check config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime manifest aggregate test syntax check failed' } pnpm exec vitest run --config config/vitest.config.ts ` config/scripts/ssh-relay-runtime-compatibility.test.mjs ` config/scripts/ssh-relay-node-release-verification.test.mjs ` @@ -559,6 +566,7 @@ jobs: config/scripts/ssh-relay-runtime-draft-readback.test.mjs ` config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs ` config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs ` + config/scripts/ssh-relay-runtime-manifest-aggregate.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-aggregate-input.mjs b/config/scripts/ssh-relay-runtime-aggregate-input.mjs index f1410553d15..6279593dcee 100644 --- a/config/scripts/ssh-relay-runtime-aggregate-input.mjs +++ b/config/scripts/ssh-relay-runtime-aggregate-input.mjs @@ -9,8 +9,12 @@ const DIGEST_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 MAX_TUPLES = 8 +// Why: each tuple contributes only its descriptor, archive, SBOM, and provenance to this boundary. +const MAX_INPUT_FILES = MAX_TUPLES * 4 +const MAX_INPUT_BYTES = 1024 * 1024 * 1024 const AGGREGATE_TIMEOUT_MS = 15 * 60_000 const ASSET_FIELDS = ['tupleId', 'name', 'contentId', 'sha256', 'size'] +const FILE_FIELDS = ['name', 'sha256', 'size'] function assertObject(value, label) { if (value === null || typeof value !== 'object' || Array.isArray(value)) { @@ -71,6 +75,35 @@ function normalizeAssets(assets) { }) } +function normalizeFiles(files) { + if (!Array.isArray(files) || files.length === 0 || files.length > MAX_INPUT_FILES) { + throw new Error('SSH relay runtime aggregate files must be a bounded non-empty array') + } + const names = new Set() + let totalSize = 0 + return files.map((file, index) => { + assertExactFields(file, FILE_FIELDS, `file ${index}`) + if (typeof file.name !== 'string' || !ASSET_NAME_PATTERN.test(file.name)) { + throw new Error('SSH relay runtime aggregate file has an invalid name') + } + if (names.has(file.name)) { + throw new Error(`SSH relay runtime aggregate has a duplicate file: ${file.name}`) + } + names.add(file.name) + if (!DIGEST_PATTERN.test(file.sha256)) { + throw new Error('SSH relay runtime aggregate file has an invalid SHA-256') + } + if (!Number.isSafeInteger(file.size) || file.size <= 0 || file.size > MAX_ARCHIVE_BYTES) { + throw new Error('SSH relay runtime aggregate file has an invalid size') + } + totalSize += file.size + if (!Number.isSafeInteger(totalSize) || totalSize > MAX_INPUT_BYTES) { + throw new Error('SSH relay runtime aggregate files exceed the total size limit') + } + return { ...file } + }) +} + function sameFileState(before, after) { return ( before.dev === after.dev && @@ -81,40 +114,40 @@ function sameFileState(before, after) { ) } -async function hashArchive(path, asset, signal) { +async function hashFile(path, file, signal) { signal?.throwIfAborted() const before = await lstat(path, { bigint: true }) if (!before.isFile() || before.isSymbolicLink()) { - throw new Error(`SSH relay runtime aggregate input is not a regular file: ${asset.name}`) + throw new Error(`SSH relay runtime aggregate input is not a regular file: ${file.name}`) } - if (before.size !== BigInt(asset.size)) { - throw new Error(`SSH relay runtime aggregate input size mismatch: ${asset.name}`) + if (before.size !== BigInt(file.size)) { + throw new Error(`SSH relay runtime aggregate input size mismatch: ${file.name}`) } const hash = createHash('sha256') let bytes = 0 for await (const chunk of createReadStream(path, { signal })) { signal?.throwIfAborted() bytes += chunk.length - if (bytes > asset.size) { - throw new Error(`SSH relay runtime aggregate input exceeded its size: ${asset.name}`) + if (bytes > file.size) { + throw new Error(`SSH relay runtime aggregate input exceeded its size: ${file.name}`) } hash.update(chunk) } const after = await lstat(path, { bigint: true }) // Why: aggregate identity must describe one stable file, not bytes swapped during hashing. if (!sameFileState(before, after)) { - throw new Error(`SSH relay runtime aggregate input changed while hashing: ${asset.name}`) + throw new Error(`SSH relay runtime aggregate input changed while hashing: ${file.name}`) } - if (bytes !== asset.size) { - throw new Error(`SSH relay runtime aggregate input size mismatch: ${asset.name}`) + if (bytes !== file.size) { + throw new Error(`SSH relay runtime aggregate input size mismatch: ${file.name}`) } const digest = `sha256:${hash.digest('hex')}` - if (digest !== asset.sha256) { - throw new Error(`SSH relay runtime aggregate input SHA-256 mismatch: ${asset.name}`) + if (digest !== file.sha256) { + throw new Error(`SSH relay runtime aggregate input SHA-256 mismatch: ${file.name}`) } } -export async function verifySshRelayRuntimeAggregateInputs({ inputDirectory, assets, signal }) { +export async function verifySshRelayRuntimeAggregateFiles({ inputDirectory, files, signal }) { const effectiveSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(AGGREGATE_TIMEOUT_MS)]) : AbortSignal.timeout(AGGREGATE_TIMEOUT_MS) @@ -122,7 +155,7 @@ export async function verifySshRelayRuntimeAggregateInputs({ inputDirectory, ass if (typeof inputDirectory !== 'string' || inputDirectory.length === 0) { throw new Error('SSH relay runtime aggregate input directory is required') } - const normalized = normalizeAssets(assets) + const normalized = normalizeFiles(files) const root = resolve(inputDirectory) const rootMetadata = await lstat(root) if (!rootMetadata.isDirectory() || rootMetadata.isSymbolicLink()) { @@ -130,7 +163,7 @@ export async function verifySshRelayRuntimeAggregateInputs({ inputDirectory, ass } const entries = await readdir(root, { withFileTypes: true }) const actualNames = entries.map((entry) => entry.name).sort() - const expectedNames = normalized.map((asset) => asset.name).sort() + const expectedNames = normalized.map((file) => file.name).sort() if (JSON.stringify(actualNames) !== JSON.stringify(expectedNames)) { throw new Error('SSH relay runtime aggregate input directory has missing or unexpected files') } @@ -139,12 +172,22 @@ export async function verifySshRelayRuntimeAggregateInputs({ inputDirectory, ass throw new Error(`SSH relay runtime aggregate input is not a regular file: ${entry.name}`) } } - for (const asset of normalized) { - const path = join(root, asset.name) - if (basename(path) !== asset.name) { - throw new Error(`SSH relay runtime aggregate asset path is unsafe: ${asset.name}`) + for (const file of normalized) { + const path = join(root, file.name) + if (basename(path) !== file.name) { + throw new Error(`SSH relay runtime aggregate file path is unsafe: ${file.name}`) } - await hashArchive(path, asset, effectiveSignal) + await hashFile(path, file, effectiveSignal) } return normalized } + +export async function verifySshRelayRuntimeAggregateInputs({ inputDirectory, assets, signal }) { + const normalized = normalizeAssets(assets) + await verifySshRelayRuntimeAggregateFiles({ + inputDirectory, + files: normalized.map(({ name, sha256, size }) => ({ name, sha256, size })), + signal + }) + return normalized +} diff --git a/config/scripts/ssh-relay-runtime-aggregate-input.test.mjs b/config/scripts/ssh-relay-runtime-aggregate-input.test.mjs index 7dded7d750d..727016e2753 100644 --- a/config/scripts/ssh-relay-runtime-aggregate-input.test.mjs +++ b/config/scripts/ssh-relay-runtime-aggregate-input.test.mjs @@ -4,7 +4,10 @@ import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' -import { verifySshRelayRuntimeAggregateInputs } from './ssh-relay-runtime-aggregate-input.mjs' +import { + verifySshRelayRuntimeAggregateFiles, + verifySshRelayRuntimeAggregateInputs +} from './ssh-relay-runtime-aggregate-input.mjs' const temporaryDirectories = [] @@ -111,4 +114,29 @@ describe('SSH relay runtime aggregate inputs', () => { verifySshRelayRuntimeAggregateInputs({ ...input, signal: controller.signal }) ).rejects.toThrow(/cancel aggregate/i) }) + + it('bounds the generalized aggregate file count and declared total size', async () => { + const input = await fixture() + const reference = { + name: 'file-0.bin', + size: 100 * 1024 * 1024, + sha256: `sha256:${'a'.repeat(64)}` + } + const tooMany = Array.from({ length: 33 }, (_, index) => ({ + ...reference, + name: `file-${index}.bin`, + size: 1 + })) + await expect( + verifySshRelayRuntimeAggregateFiles({ inputDirectory: input.inputDirectory, files: tooMany }) + ).rejects.toThrow(/bounded/i) + + const tooLarge = Array.from({ length: 11 }, (_, index) => ({ + ...reference, + name: `file-${index}.bin` + })) + await expect( + verifySshRelayRuntimeAggregateFiles({ inputDirectory: input.inputDirectory, files: tooLarge }) + ).rejects.toThrow(/total size/i) + }) }) diff --git a/config/scripts/ssh-relay-runtime-manifest-aggregate.mjs b/config/scripts/ssh-relay-runtime-manifest-aggregate.mjs new file mode 100644 index 00000000000..429e9bffa7b --- /dev/null +++ b/config/scripts/ssh-relay-runtime-manifest-aggregate.mjs @@ -0,0 +1,291 @@ +import { createHash } from 'node:crypto' +import { readFile } from 'node:fs/promises' +import { join, resolve } from 'node:path' + +import { verifySshRelayRuntimeAggregateFiles } from './ssh-relay-runtime-aggregate-input.mjs' +import { assembleCanonicalSshRelayRuntimeManifest } from './ssh-relay-runtime-manifest-assembly.mjs' +import { + createSshRelayRuntimeManifestSigningRequest, + finalizeSshRelayRuntimeManifestSigningHandoff +} from './ssh-relay-runtime-manifest-signing-handoff.mjs' +import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' + +const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u +const ASSET_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,239}$/u +const MAX_TUPLES = 8 +const MAX_METADATA_BYTES = 32 * 1024 * 1024 +const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024 +const AGGREGATE_TIMEOUT_MS = 15 * 60_000 +const INPUT_FIELDS = ['archive', 'descriptor', 'provenance', 'sbom', 'tupleId'] +const FILE_FIELDS = ['name', 'sha256', 'size'] +const PREPARED_FIELDS = ['inputTuples', 'inputTuplesSha256', 'signingRequest', 'unsignedManifest'] + +export const SSH_RELAY_RUNTIME_MANIFEST_AGGREGATE_LIMITS = Object.freeze({ + maximumTuples: MAX_TUPLES, + maximumMetadataBytes: MAX_METADATA_BYTES, + timeoutMs: AGGREGATE_TIMEOUT_MS +}) + +function assertObject(value, label) { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`SSH relay runtime manifest aggregate ${label} must be an object`) + } +} + +function assertExactFields(value, fields, label) { + assertObject(value, label) + const actual = Object.keys(value).sort() + const expected = [...fields].sort() + if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) { + throw new Error( + `SSH relay runtime manifest aggregate ${label} has unexpected or missing fields` + ) + } +} + +function normalizeFile(value, label, maximumSize) { + assertExactFields(value, FILE_FIELDS, label) + if (typeof value.name !== 'string' || !ASSET_NAME_PATTERN.test(value.name)) { + throw new Error(`SSH relay runtime manifest aggregate ${label} has an invalid name`) + } + if (!DIGEST_PATTERN.test(value.sha256)) { + throw new Error(`SSH relay runtime manifest aggregate ${label} has an invalid SHA-256`) + } + if (!Number.isSafeInteger(value.size) || value.size <= 0 || value.size > maximumSize) { + throw new Error(`SSH relay runtime manifest aggregate ${label} exceeds its size limit`) + } + return { name: value.name, size: value.size, sha256: value.sha256 } +} + +function normalizeTupleInputs(tupleInputs) { + if (!Array.isArray(tupleInputs) || tupleInputs.length === 0 || tupleInputs.length > MAX_TUPLES) { + throw new Error('SSH relay runtime manifest aggregate tuple inputs must be a bounded array') + } + const tupleIds = new Set() + return tupleInputs + .map((input, index) => { + assertExactFields(input, INPUT_FIELDS, `tuple input ${index}`) + if (!Object.hasOwn(sshRelayRuntimeCompatibility, input.tupleId)) { + throw new Error( + `SSH relay runtime manifest aggregate has an unsupported tuple: ${input.tupleId}` + ) + } + if (tupleIds.has(input.tupleId)) { + throw new Error( + `SSH relay runtime manifest aggregate has a duplicate tuple: ${input.tupleId}` + ) + } + tupleIds.add(input.tupleId) + const descriptor = normalizeFile( + input.descriptor, + `${input.tupleId} descriptor`, + MAX_METADATA_BYTES + ) + if (descriptor.name !== `orca-ssh-relay-runtime-${input.tupleId}.manifest-tuple.json`) { + throw new Error( + `SSH relay runtime manifest aggregate ${input.tupleId} descriptor has an unexpected name` + ) + } + return { + tupleId: input.tupleId, + descriptor, + archive: normalizeFile(input.archive, `${input.tupleId} archive`, MAX_ARCHIVE_BYTES), + sbom: normalizeFile(input.sbom, `${input.tupleId} SBOM`, MAX_METADATA_BYTES), + provenance: normalizeFile( + input.provenance, + `${input.tupleId} provenance`, + MAX_METADATA_BYTES + ) + } + }) + .sort((left, right) => + left.tupleId < right.tupleId ? -1 : left.tupleId > right.tupleId ? 1 : 0 + ) +} + +function flattenFiles(tupleInputs) { + return tupleInputs.flatMap(({ descriptor, archive, sbom, provenance }) => [ + descriptor, + archive, + sbom, + provenance + ]) +} + +function sha256(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +function inputTuplesSha256(tupleInputs) { + return sha256(Buffer.from(JSON.stringify(tupleInputs), 'utf8')) +} + +function decodeDescriptor(bytes, input) { + if (bytes.length !== input.descriptor.size || sha256(bytes) !== input.descriptor.sha256) { + throw new Error( + `SSH relay runtime manifest aggregate descriptor bytes changed: ${input.descriptor.name}` + ) + } + let source + try { + source = new TextDecoder('utf-8', { fatal: true }).decode(bytes) + } catch { + throw new Error( + `SSH relay runtime manifest aggregate descriptor must be valid UTF-8: ${input.descriptor.name}` + ) + } + let descriptor + try { + descriptor = JSON.parse(source) + } catch { + throw new Error( + `SSH relay runtime manifest aggregate descriptor must be valid JSON: ${input.descriptor.name}` + ) + } + assertExactFields(descriptor, ['schemaVersion', 'tuple'], `${input.tupleId} descriptor`) + if (descriptor.schemaVersion !== 1) { + throw new Error( + `SSH relay runtime manifest aggregate ${input.tupleId} descriptor schema is unsupported` + ) + } + assertObject(descriptor.tuple, `${input.tupleId} descriptor tuple`) + if (descriptor.tuple.tupleId !== input.tupleId) { + throw new Error( + `SSH relay runtime manifest aggregate ${input.tupleId} descriptor has the wrong tuple` + ) + } + return descriptor.tuple +} + +function sameFile(actual, expected) { + return ( + actual?.name === expected.name && + actual?.size === expected.size && + actual?.sha256 === expected.sha256 + ) +} + +function assertTupleAssets(tuple, input) { + if (!sameFile(tuple.archive, input.archive)) { + throw new Error( + `SSH relay runtime manifest aggregate ${input.tupleId} archive disagrees with its descriptor` + ) + } + if (!sameFile(tuple.metadataAssets?.sbom, input.sbom)) { + throw new Error( + `SSH relay runtime manifest aggregate ${input.tupleId} SBOM disagrees with its descriptor` + ) + } + if (!sameFile(tuple.metadataAssets?.provenance, input.provenance)) { + throw new Error( + `SSH relay runtime manifest aggregate ${input.tupleId} provenance disagrees with its descriptor` + ) + } +} + +async function readTuples(inputDirectory, tupleInputs, signal) { + const root = resolve(inputDirectory) + const tuples = [] + for (const input of tupleInputs) { + signal?.throwIfAborted() + // Why: only the independently hashed post-sign descriptor may supply manifest identity/trust. + const bytes = await readFile(join(root, input.descriptor.name), { signal }) + const tuple = decodeDescriptor(bytes, input) + assertTupleAssets(tuple, input) + tuples.push(tuple) + } + return tuples +} + +function assertPreparedBindings(prepared, tupleInputs, assembled) { + if (prepared.inputTuplesSha256 !== inputTuplesSha256(tupleInputs)) { + throw new Error('SSH relay runtime manifest aggregate prepared input receipt drifted') + } + if (assembled.manifest.tuples.length !== tupleInputs.length) { + throw new Error('SSH relay runtime manifest aggregate prepared tuple count drifted') + } + for (const [index, input] of tupleInputs.entries()) { + const tuple = assembled.manifest.tuples[index] + if (tuple.tupleId !== input.tupleId) { + throw new Error('SSH relay runtime manifest aggregate prepared tuple identity drifted') + } + assertTupleAssets(tuple, input) + } + const expectedRequest = createSshRelayRuntimeManifestSigningRequest(assembled.canonicalBytes) + assertExactFields( + prepared.signingRequest, + Object.keys(expectedRequest), + 'prepared signing request' + ) + if ( + prepared.signingRequest.algorithm !== expectedRequest.algorithm || + prepared.signingRequest.payloadSize !== expectedRequest.payloadSize || + prepared.signingRequest.payloadSha256 !== expectedRequest.payloadSha256 || + !Buffer.from(prepared.signingRequest.canonicalBytes).equals(expectedRequest.canonicalBytes) + ) { + throw new Error('SSH relay runtime manifest aggregate prepared signing request drifted') + } +} + +export async function prepareSshRelayRuntimeManifestAggregate({ + inputDirectory, + build, + createdAt, + tupleInputs, + signal +}) { + const effectiveSignal = signal + ? AbortSignal.any([signal, AbortSignal.timeout(AGGREGATE_TIMEOUT_MS)]) + : AbortSignal.timeout(AGGREGATE_TIMEOUT_MS) + effectiveSignal.throwIfAborted() + const normalizedInputs = normalizeTupleInputs(tupleInputs) + // Why: an extra or omitted file must fail before any descriptor can influence signed content. + await verifySshRelayRuntimeAggregateFiles({ + inputDirectory, + files: flattenFiles(normalizedInputs), + signal: effectiveSignal + }) + const tuples = await readTuples(inputDirectory, normalizedInputs, effectiveSignal) + const assembled = assembleCanonicalSshRelayRuntimeManifest({ + schemaVersion: 1, + build, + createdAt, + tuples + }) + return { + inputTuples: normalizedInputs, + inputTuplesSha256: inputTuplesSha256(normalizedInputs), + unsignedManifest: assembled.manifest, + signingRequest: createSshRelayRuntimeManifestSigningRequest(assembled.canonicalBytes) + } +} + +export function finalizeSshRelayRuntimeManifestAggregate({ + prepared, + signingResults, + acceptedKeys +}) { + assertExactFields(prepared, PREPARED_FIELDS, 'prepared result') + const tupleInputs = normalizeTupleInputs(prepared.inputTuples) + // Why: finalization reassembles the verified projection so mutable handoff objects cannot drift. + const assembled = assembleCanonicalSshRelayRuntimeManifest(prepared.unsignedManifest) + assertPreparedBindings(prepared, tupleInputs, assembled) + const finalized = finalizeSshRelayRuntimeManifestSigningHandoff({ + request: prepared.signingRequest, + signingResults, + acceptedKeys + }) + const manifestAsset = { + name: 'orca-ssh-relay-runtime-manifest.json', + size: finalized.bytes.length, + sha256: finalized.sha256 + } + return { + inputTuples: tupleInputs, + inputTuplesSha256: prepared.inputTuplesSha256, + manifest: finalized.manifest, + bytes: finalized.bytes, + sha256: finalized.sha256, + manifestAsset + } +} diff --git a/config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs b/config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs new file mode 100644 index 00000000000..63df8669147 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs @@ -0,0 +1,315 @@ +import { createHash } from 'node:crypto' +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import nacl from 'tweetnacl' +import { afterEach, describe, expect, it } from 'vitest' + +import { createSshRelayArtifactTestManifest } from '../../src/main/ssh/ssh-relay-artifact-test-manifest.ts' +import { verifySshRelayArtifactManifest } from '../../src/main/ssh/ssh-relay-manifest-signature.ts' +import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' +import { + SSH_RELAY_RUNTIME_MANIFEST_AGGREGATE_LIMITS, + finalizeSshRelayRuntimeManifestAggregate, + prepareSshRelayRuntimeManifestAggregate +} from './ssh-relay-runtime-manifest-aggregate.mjs' +import { sshRelayRuntimeManifestKeyId } from './ssh-relay-runtime-manifest-signing-handoff.mjs' + +const temporaryDirectories = [] +const keyPair = nacl.sign.keyPair.fromSeed(Uint8Array.from({ length: 32 }, (_, index) => index)) + +function sha256(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +function fileReference(name, bytes) { + return { name, size: bytes.length, sha256: sha256(bytes) } +} + +function arm64TupleFrom(source) { + const tuple = structuredClone(source) + tuple.tupleId = 'linux-arm64-glibc' + tuple.architecture = 'arm64' + tuple.compatibility = structuredClone(sshRelayRuntimeCompatibility[tuple.tupleId]) + for (const entry of tuple.entries) { + entry.path = entry.path.replaceAll('watcher-linux-x64-glibc', 'watcher-linux-arm64-glibc') + } + for (const file of tuple.nativeVerification.files) { + file.path = file.path.replaceAll('watcher-linux-x64-glibc', 'watcher-linux-arm64-glibc') + } + tuple.metadataAssets.sbom.name = 'orca-ssh-relay-runtime-linux-arm64-glibc.spdx.json' + tuple.metadataAssets.provenance.name = 'orca-ssh-relay-runtime-linux-arm64-glibc.provenance.json' + tuple.contentId = computeSshRelayRuntimeContentId(tuple) + tuple.archive.name = `orca-ssh-relay-runtime-v1-${tuple.tupleId}-${tuple.contentId.slice('sha256:'.length)}.tar.xz` + return tuple +} + +async function writeTupleInput(inputDirectory, sourceTuple, suffix = '') { + const tuple = structuredClone(sourceTuple) + const archiveBytes = Buffer.from(`verified runtime archive ${tuple.tupleId}${suffix}`) + const sbomBytes = Buffer.from(`{"spdxVersion":"SPDX-2.3","tuple":"${tuple.tupleId}"}\n`) + const provenanceBytes = Buffer.from( + `{"_type":"https://in-toto.io/Statement/v1","tuple":"${tuple.tupleId}"}\n` + ) + tuple.archive.size = archiveBytes.length + tuple.archive.sha256 = sha256(archiveBytes) + tuple.metadataAssets.sbom = fileReference(tuple.metadataAssets.sbom.name, sbomBytes) + tuple.metadataAssets.provenance = fileReference( + tuple.metadataAssets.provenance.name, + provenanceBytes + ) + const descriptorName = `orca-ssh-relay-runtime-${tuple.tupleId}.manifest-tuple.json` + const descriptorBytes = Buffer.from( + `${JSON.stringify({ schemaVersion: 1, tuple }, null, 2)}\n`, + 'utf8' + ) + const input = { + tupleId: tuple.tupleId, + descriptor: fileReference(descriptorName, descriptorBytes), + archive: fileReference(tuple.archive.name, archiveBytes), + sbom: { ...tuple.metadataAssets.sbom }, + provenance: { ...tuple.metadataAssets.provenance } + } + await Promise.all([ + writeFile(join(inputDirectory, input.descriptor.name), descriptorBytes), + writeFile(join(inputDirectory, input.archive.name), archiveBytes), + writeFile(join(inputDirectory, input.sbom.name), sbomBytes), + writeFile(join(inputDirectory, input.provenance.name), provenanceBytes) + ]) + return { input, tuple } +} + +async function fixture({ includeArm64 = false } = {}) { + const inputDirectory = await mkdtemp(join(tmpdir(), 'orca-runtime-manifest-aggregate-')) + temporaryDirectories.push(inputDirectory) + const manifest = createSshRelayArtifactTestManifest() + const tuples = [manifest.tuples[0]] + if (includeArm64) { + tuples.push(arm64TupleFrom(manifest.tuples[0])) + } + const written = [] + for (const tuple of tuples) { + written.push(await writeTupleInput(inputDirectory, tuple)) + } + return { + inputDirectory, + build: structuredClone(manifest.build), + createdAt: manifest.createdAt, + tupleInputs: written.map((entry) => entry.input), + tuples: written.map((entry) => entry.tuple) + } +} + +function acceptedKey() { + return { + keyId: sshRelayRuntimeManifestKeyId(keyPair.publicKey), + publicKey: keyPair.publicKey + } +} + +function signingResult(request) { + return { + keyId: sshRelayRuntimeManifestKeyId(keyPair.publicKey), + signature: Buffer.from(nacl.sign.detached(request.canonicalBytes, keyPair.secretKey)).toString( + 'base64' + ) + } +} + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })) + ) +}) + +describe('SSH relay runtime fail-closed manifest aggregate', () => { + it('prepares exact verified tuple files and finalizes only verified signed bytes', async () => { + const input = await fixture() + const prepared = await prepareSshRelayRuntimeManifestAggregate(input) + + expect(prepared.inputTuples).toEqual(input.tupleInputs) + expect(prepared.unsignedManifest.tuples).toHaveLength(1) + expect(prepared.unsignedManifest.tuples[0]).toMatchObject({ + tupleId: input.tuples[0].tupleId, + contentId: input.tuples[0].contentId, + archive: input.tuples[0].archive, + metadataAssets: input.tuples[0].metadataAssets + }) + expect(prepared.unsignedManifest.tuples[0].nativeVerification.files).toEqual( + input.tuples[0].nativeVerification.files.toSorted((left, right) => + left.path < right.path ? -1 : left.path > right.path ? 1 : 0 + ) + ) + expect(prepared.signingRequest.canonicalBytes).toEqual( + Buffer.from(JSON.stringify(prepared.unsignedManifest), 'utf8') + ) + const result = finalizeSshRelayRuntimeManifestAggregate({ + prepared, + signingResults: [signingResult(prepared.signingRequest)], + acceptedKeys: [acceptedKey()] + }) + + expect(Object.keys(result).sort()).toEqual([ + 'bytes', + 'inputTuples', + 'inputTuplesSha256', + 'manifest', + 'manifestAsset', + 'sha256' + ]) + expect(result.manifestAsset).toEqual({ + name: 'orca-ssh-relay-runtime-manifest.json', + size: result.bytes.length, + sha256: result.sha256 + }) + expect(result.inputTuplesSha256).toBe(prepared.inputTuplesSha256) + expect(JSON.parse(result.bytes.toString('utf8'))).toEqual(result.manifest) + expect(verifySshRelayArtifactManifest(result.manifest, [acceptedKey()])).toEqual( + result.manifest + ) + }) + + it('fails closed on missing, extra, or mutated aggregate files', async () => { + const missing = await fixture() + await rm(join(missing.inputDirectory, missing.tupleInputs[0].provenance.name)) + await expect(prepareSshRelayRuntimeManifestAggregate(missing)).rejects.toThrow( + /missing|unexpected/i + ) + + const extra = await fixture() + await writeFile(join(extra.inputDirectory, 'unexpected.bin'), 'extra') + await expect(prepareSshRelayRuntimeManifestAggregate(extra)).rejects.toThrow( + /missing|unexpected/i + ) + + for (const field of ['archive', 'sbom', 'provenance', 'descriptor']) { + const mutated = await fixture() + await writeFile(join(mutated.inputDirectory, mutated.tupleInputs[0][field].name), 'mutated') + await expect(prepareSshRelayRuntimeManifestAggregate(mutated)).rejects.toThrow( + /size|sha-?256/i + ) + } + }) + + it('rejects descriptor, tuple, and declared asset drift', async () => { + const tuple = await fixture() + const tupleDescriptorPath = join(tuple.inputDirectory, tuple.tupleInputs[0].descriptor.name) + const tupleDocument = JSON.parse(await readFile(tupleDescriptorPath, 'utf8')) + tupleDocument.tuple.tupleId = 'linux-arm64-glibc' + const tupleBytes = Buffer.from(`${JSON.stringify(tupleDocument)}\n`) + await writeFile(tupleDescriptorPath, tupleBytes) + tuple.tupleInputs[0].descriptor = fileReference( + tuple.tupleInputs[0].descriptor.name, + tupleBytes + ) + await expect(prepareSshRelayRuntimeManifestAggregate(tuple)).rejects.toThrow( + /descriptor|tuple/i + ) + + const asset = await fixture() + asset.tupleInputs[0].sbom.sha256 = `sha256:${'f'.repeat(64)}` + await expect(prepareSshRelayRuntimeManifestAggregate(asset)).rejects.toThrow(/sha-?256/i) + + const descriptor = await fixture() + const descriptorPath = join( + descriptor.inputDirectory, + descriptor.tupleInputs[0].descriptor.name + ) + const document = JSON.parse(await readFile(descriptorPath, 'utf8')) + document.latest = true + const bytes = Buffer.from(`${JSON.stringify(document)}\n`) + await writeFile(descriptorPath, bytes) + descriptor.tupleInputs[0].descriptor = fileReference( + descriptor.tupleInputs[0].descriptor.name, + bytes + ) + await expect(prepareSshRelayRuntimeManifestAggregate(descriptor)).rejects.toThrow( + /descriptor.*field/i + ) + }) + + it('sorts tuple inputs and produces deterministic canonical requests', async () => { + const input = await fixture({ includeArm64: true }) + input.tupleInputs.reverse() + const first = await prepareSshRelayRuntimeManifestAggregate(input) + input.tupleInputs.reverse() + const second = await prepareSshRelayRuntimeManifestAggregate(input) + + expect(first.inputTuples.map((entry) => entry.tupleId)).toEqual([ + 'linux-arm64-glibc', + 'linux-x64-glibc' + ]) + expect(first.signingRequest.canonicalBytes).toEqual(second.signingRequest.canonicalBytes) + expect(first.signingRequest.payloadSha256).toBe(second.signingRequest.payloadSha256) + }) + + it('rejects prepared-request and returned-signature drift', async () => { + const input = await fixture() + const prepared = await prepareSshRelayRuntimeManifestAggregate(input) + const result = signingResult(prepared.signingRequest) + prepared.signingRequest = { + ...prepared.signingRequest, + payloadSha256: `sha256:${'f'.repeat(64)}` + } + expect(() => + finalizeSshRelayRuntimeManifestAggregate({ + prepared, + signingResults: [result], + acceptedKeys: [acceptedKey()] + }) + ).toThrow(/request|sha-?256/i) + + const receiptDrift = await prepareSshRelayRuntimeManifestAggregate(input) + receiptDrift.inputTuples[0].descriptor.sha256 = `sha256:${'e'.repeat(64)}` + expect(() => + finalizeSshRelayRuntimeManifestAggregate({ + prepared: receiptDrift, + signingResults: [signingResult(receiptDrift.signingRequest)], + acceptedKeys: [acceptedKey()] + }) + ).toThrow(/receipt.*drift/i) + + const fresh = await prepareSshRelayRuntimeManifestAggregate(input) + const invalid = signingResult(fresh.signingRequest) + invalid.signature = Buffer.alloc(64).toString('base64') + expect(() => + finalizeSshRelayRuntimeManifestAggregate({ + prepared: fresh, + signingResults: [invalid], + acceptedKeys: [acceptedKey()] + }) + ).toThrow(/signature/i) + }) + + it('honors cancellation and tuple/metadata bounds', async () => { + expect(SSH_RELAY_RUNTIME_MANIFEST_AGGREGATE_LIMITS).toEqual({ + maximumTuples: 8, + maximumMetadataBytes: 32 * 1024 * 1024, + timeoutMs: 15 * 60_000 + }) + const cancelled = await fixture() + const controller = new AbortController() + controller.abort(new Error('cancel manifest aggregate')) + await expect( + prepareSshRelayRuntimeManifestAggregate({ ...cancelled, signal: controller.signal }) + ).rejects.toThrow(/cancel manifest aggregate/i) + + const oversized = await fixture() + oversized.tupleInputs[0].descriptor.size = + SSH_RELAY_RUNTIME_MANIFEST_AGGREGATE_LIMITS.maximumMetadataBytes + 1 + await expect(prepareSshRelayRuntimeManifestAggregate(oversized)).rejects.toThrow(/size|limit/i) + + const overTupleLimit = await fixture() + overTupleLimit.tupleInputs = Array.from( + { length: SSH_RELAY_RUNTIME_MANIFEST_AGGREGATE_LIMITS.maximumTuples + 1 }, + () => structuredClone(overTupleLimit.tupleInputs[0]) + ) + await expect(prepareSshRelayRuntimeManifestAggregate(overTupleLimit)).rejects.toThrow( + /bounded|tuple/i + ) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 20bf57a7d90..1d57cb83f8f 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -110,7 +110,11 @@ describe('SSH relay runtime artifact workflow', () => { expect( source.match(/node --check config\/scripts\/ssh-relay-runtime-manifest-validation\.mjs/g) ).toHaveLength(2) - for (const moduleName of ['manifest-assembly', 'manifest-signing-handoff']) { + for (const moduleName of [ + 'manifest-assembly', + 'manifest-signing-handoff', + 'manifest-aggregate' + ]) { 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) 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 4811498570e..24ae72ccc8f 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 @@ -71,7 +71,10 @@ complete a box. `E-M4-MANIFEST-HANDOFF-CI-001`. Next, connect only the credential-free verified aggregate inputs, canonical signing request, verified signer return, and immutable final-manifest output behind fail-closed tests. No production workflow, publication, desktop consumer, signing - credential, or tuple is connected. + credential, or tuple is connected. The purpose-named missing-module RED is recorded under + `E-M4-MANIFEST-AGGREGATE-LOCAL-RED-001`; the implementation and broad local gates are green + under `E-M4-MANIFEST-AGGREGATE-LOCAL-001` and + `E-M4-MANIFEST-AGGREGATE-LOCAL-002`. Exact-head Node 24 native CI remains open. ## Work packages, in required order @@ -125,7 +128,9 @@ complete a box. - [ ] **In progress — 2026-07-15, Codex implementation owner:** add the disconnected, credential-free fail-closed aggregate boundary from exact verified runtime inputs through canonical request and verified final-manifest bytes. Keep native/manifest credentials, publication, desktop - consumers, and every tuple outside this slice. + consumers, and every tuple outside this slice. The code and local proof are complete under + `E-M4-MANIFEST-AGGREGATE-LOCAL-001` and `E-M4-MANIFEST-AGGREGATE-LOCAL-002`; native CI is the + remaining gate. - [ ] 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 64a89d92ebb..0861ee0014d 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 credential-free fail-closed aggregate boundary — **In progress — 2026-07-15, Codex implementation owner**. The disconnected release-DAG, aggregate/read-back byte boundaries, Windows artifact/manifest parity, and canonical manifest/signing handoff are closed locally and on all six native build jobs under E-M4-RELEASE-DAG-LOCAL-001, E-M4-RELEASE-DAG-CI-001, E-M4-AGGREGATE-READBACK-LOCAL-001, E-M4-AGGREGATE-READBACK-CI-001, E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001, E-M4-WINDOWS-MANIFEST-PARITY-CI-001, E-M4-MANIFEST-HANDOFF-LOCAL-001, E-M4-MANIFEST-HANDOFF-LOCAL-002, and E-M4-MANIFEST-HANDOFF-CI-001. The active package is limited to connecting exact verified aggregate inputs to canonical signing request and verified immutable final-manifest bytes under credential-free tests; it may not publish, connect desktop consumers, use production native/manifest signing credentials, 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** — commit `14355dfe0583f634a6e86ada9e1afcf7abe7a8fb` adds disconnected release-side validation, canonical assembly, bounded request, accepted-key/signature verification, deterministic final manifest emission, and all-six workflow wiring. Exact-head artifact run [29395319239](https://github.com/stablyai/orca/actions/runs/29395319239) executes both new suites under Node 24.18.0 on all six native jobs; every build, smoke, equality, upload, both Linux supplemental userland jobs, and the Windows x64 floor job pass. The overall artifact run remains red only because the Windows arm64 floor job observes build 26200 rather than required build 26100 after successful 5,818.68 ms runtime smoke with 49,385,472-byte RSS. Golden E2E [29395319242](https://github.com/stablyai/orca/actions/runs/29395319242) and PR Checks [29395319119](https://github.com/stablyai/orca/actions/runs/29395319119) are green. The handoff package is closed under E-M4-MANIFEST-HANDOFF-CI-001. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, aggregate publication wiring, desktop embedding, and native trust remain separately gated. Nothing is published or enabled, and legacy remains the production default.
      +Current phase: Milestone 4 / Work Package 3 credential-free fail-closed aggregate boundary — **In progress — 2026-07-15, Codex implementation owner**. The disconnected release-DAG, aggregate/read-back byte boundaries, Windows artifact/manifest parity, and canonical manifest/signing handoff are closed locally and on all six native build jobs under E-M4-RELEASE-DAG-LOCAL-001, E-M4-RELEASE-DAG-CI-001, E-M4-AGGREGATE-READBACK-LOCAL-001, E-M4-AGGREGATE-READBACK-CI-001, E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001, E-M4-WINDOWS-MANIFEST-PARITY-CI-001, E-M4-MANIFEST-HANDOFF-LOCAL-001, E-M4-MANIFEST-HANDOFF-LOCAL-002, and E-M4-MANIFEST-HANDOFF-CI-001. The exact-file aggregate boundary is implemented and locally green under E-M4-MANIFEST-AGGREGATE-LOCAL-001 and E-M4-MANIFEST-AGGREGATE-LOCAL-002; exact-head Node 24 native CI remains required. This package may not publish, connect desktop consumers, use production native/manifest signing credentials, 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** — the purpose-named missing-module RED is closed by a bounded aggregate that verifies the exact descriptor/archive/SBOM/provenance file set, derives canonical unsigned bytes only from the hash-verified post-sign tuple descriptor, creates the existing Ed25519 request, reassembles it at finalization, verifies the signer return, and emits one content-addressed final-manifest asset. Focused 20-test and broad 221-test release-side suites, 47 desktop parity tests, syntax, typecheck, full lint, formatting, max-lines, and diff gates pass under E-M4-MANIFEST-AGGREGATE-LOCAL-001 and E-M4-MANIFEST-AGGREGATE-LOCAL-002. The suite is wired into all six native artifact jobs; exact-head Node 24 CI is next. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, aggregate publication wiring, desktop embedding, 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) @@ -75,9 +75,11 @@ same change as the work it records. E-M4-MANIFEST-HANDOFF-LOCAL-002, and E-M4-MANIFEST-HANDOFF-CI-001. - Active package: Work Package 3 disconnected fail-closed aggregate boundary. Connect exact verified runtime inputs to canonical unsigned bytes, a bounded signing request, verified signer return, and - immutable final-manifest output under credential-free tests. Production native/manifest signing - credentials, publication, desktop consumers, and tuple enablement remain outside this slice. No - broader Milestone 4 box is checked until its evidence exists. + immutable final-manifest output under credential-free tests. The implementation and local gates + are green under E-M4-MANIFEST-AGGREGATE-LOCAL-001 and E-M4-MANIFEST-AGGREGATE-LOCAL-002; exact-head + Node 24 native CI remains open. Production native/manifest signing credentials, publication, + desktop consumers, and tuple enablement remain outside this slice. No broader Milestone 4 box is + checked until its evidence exists. - 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 @@ -195,11 +197,10 @@ 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: add RED tests for the disconnected credential-free aggregate boundary, then - connect exact verified runtime inputs through canonical request and verified immutable - final-manifest bytes. Keep real native/manifest signing credentials, endpoint trust, missing - exact-floor snapshots, production publication, desktop consumers, and every tuple's enabled state - outside this slice. +- Next required action: commit and push the locally green disconnected aggregate boundary, then + collect exact-head Node 24 proof from all six native build jobs. Keep real native/manifest signing + credentials, endpoint trust, missing exact-floor snapshots, production publication, desktop + consumers, and every tuple's enabled state outside this slice. ## Non-Negotiable Invariants @@ -1541,6 +1542,8 @@ focused commands as their scripts/tests are introduced. - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-compatibility.test.mjs` (E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-RED-001, E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001; exact Windows compatibility discriminator and canonical vector) - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-compatibility.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs src/main/ssh/ssh-relay-artifact-schema.test.ts` (E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-RED-001, E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001; build/desktop contract parity and static native-job wiring) - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs` (E-M4-MANIFEST-HANDOFF-LOCAL-RED-001, E-M4-MANIFEST-HANDOFF-LOCAL-001) +- [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs` (E-M4-MANIFEST-AGGREGATE-LOCAL-RED-001, E-M4-MANIFEST-AGGREGATE-LOCAL-001) +- [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs config/scripts/ssh-relay-runtime-aggregate-input.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` (E-M4-MANIFEST-AGGREGATE-LOCAL-001; exact-file boundary and all-six workflow-source wiring) - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay*.test.mjs` (E-M4-MANIFEST-HANDOFF-LOCAL-002; all release-side SSH-relay contracts) - [x] `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` (E-M4-MANIFEST-HANDOFF-LOCAL-002; desktop schema/signature parity) @@ -9378,6 +9381,111 @@ diff --check`. - Follow-up: begin RED tests for the credential-free fail-closed aggregate boundary; retain all production/default and credential gates. +### E-M4-MANIFEST-AGGREGATE-LOCAL-RED-001 — Fail-closed manifest aggregate is absent + +- Date: 2026-07-15 +- Commit SHA / PR: uncommitted purpose-named RED test atop + `127ddbcc154fbdf2641bc80d67a3d68d862ab1ca`; draft PR #8741 +- Runner: macOS 26.2 arm64, native local worktree; repository Node/pnpm toolchain +- Remote: not applicable; disconnected credential-free release boundary +- Transport/network: local filesystem only; no network, GitHub release, signer, or credential +- Exact command: + + ```sh + pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs + ``` + +- Result: FAIL as required; one suite failed before collection because + `ssh-relay-runtime-manifest-aggregate.mjs` does not exist, with zero tests collected. +- Duration and resource metrics: Vitest duration 210 ms; memory and file-descriptor deltas not + measured for this expected import failure. +- Artifact/log/trace link: local terminal output; no external artifact. +- Oracle proved: the purpose-named contract cannot pass through an existing permissive or accidental + aggregate implementation; the credential-free boundary is genuinely absent before this package. +- Does not prove: input-byte validation, descriptor binding, deterministic assembly, signing-return + verification, publication, credentials, desktop embedding, native trust, or any enabled tuple. +- Checklist items satisfied: RED prerequisite for the active Work Package 3 aggregate boundary. +- Follow-up: implement the disconnected boundary, turn this exact suite green, then run broad local + and all-six native regression gates without connecting publication or consumers. + +### E-M4-MANIFEST-AGGREGATE-LOCAL-001 — Exact-file aggregate and signing boundary pass locally + +- Date: 2026-07-15 +- Commit SHA / PR: uncommitted implementation atop + `127ddbcc154fbdf2641bc80d67a3d68d862ab1ca`; draft PR #8741 +- Runner: macOS 26.2 arm64, native local worktree; Node v26.0.0 and pnpm 10.24.0 +- Remote: not applicable; disconnected credential-free release boundary +- Transport/network: local filesystem only; no network, GitHub release, signer service, credential, + SSH connection, or publication +- Exact command: + + ```sh + /usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs config/scripts/ssh-relay-runtime-aggregate-input.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs + ``` + +- Result: PASS; 3 files / 20 tests, including 6 purpose-named aggregate tests, 8 aggregate-input + tests, and 6 workflow-source tests. +- Duration and resource metrics: Vitest 482 ms; 1.04 seconds wall; 132,956,160-byte maximum RSS; + 96,146,832-byte peak memory footprint; zero swaps. +- Artifact/log/trace link: local terminal output; no external artifact. +- Oracle proved: at most eight tuple declarations select one exact regular-file set containing a + bounded descriptor, archive, SBOM, and provenance per tuple; missing, extra, linked, mutated, + oversized, duplicate, unsupported, or cancelled inputs fail before assembly. Only the hashed + descriptor supplies the complete post-sign tuple/native-attestation projection. Descriptor and + declared asset drift fail, tuple order is deterministic, the 15-minute boundary is cancellable, + finalization reassembles the canonical request, invalid signer returns fail, and the resulting + manifest passes the desktop verifier. POSIX and Windows workflow source includes syntax and test + execution for the new module. +- Does not prove: repository Node 24 behavior, a descriptor emitted by a real post-sign native job, + Apple/SignPath trust, protected signing credentials, final detached-signature asset encoding, + release publication/read-back, desktop embedding, SSH transfer/install, or an enabled tuple. +- Checklist items satisfied: local implementation gate for the active Work Package 3 credential-free + aggregate boundary. +- Follow-up: run broad local gates, then commit/push and collect all-six exact-head Node 24 CI. + +### E-M4-MANIFEST-AGGREGATE-LOCAL-002 — Broad aggregate regressions and static gates pass + +- Date: 2026-07-15 +- Commit SHA / PR: same uncommitted package atop + `127ddbcc154fbdf2641bc80d67a3d68d862ab1ca`; draft PR #8741 +- Runner: macOS 26.2 arm64, native local worktree; Node v26.0.0 and pnpm 10.24.0 +- Remote: not applicable +- Transport/network: local filesystem only; no network, credential, release, SSH host, publication, + desktop consumer, or production/default call +- Commands and results: + - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay*.test.mjs` + — PASS, 39 files / 221 tests in 5.70 seconds; 6.49 seconds wall; 187,924,480-byte maximum + RSS; 96,081,176-byte peak memory footprint; zero swaps. + - `/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 / 47 tests in 485 ms; 1.19 seconds wall; 172,572,672-byte maximum RSS; + 95,819,128-byte peak memory footprint; zero swaps. + - `node --check` for the aggregate-input, manifest-aggregate, their tests, and workflow-source test + — PASS with no output. + - Focused `pnpm exec oxlint` for all five touched script/test files — PASS with no findings. + - Focused `pnpm exec oxfmt --check` for the touched code, workflow, and both checklist files — PASS + in 1,451 ms. + - `/usr/bin/time -l pnpm run typecheck` — PASS in 2.62 seconds wall with 1,266,810,880-byte + maximum RSS and zero swaps. + - `/usr/bin/time -l pnpm run lint` — PASS in 8.64 seconds wall with 2,023,014,400-byte maximum + RSS and 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. + - `pnpm run check:max-lines-ratchet` — PASS, 355 grandfathered suppressions and no new bypasses. + - `git diff --check` — PASS with no output. +- Supplemental command correction: an initial quoted-glob invocation treated + `config/scripts/ssh-relay*.test.mjs` as a literal filter and correctly returned “No test files + found”; the exact unquoted checklist command above immediately replaced it and is the recorded + executable result. +- Oracle proved: the new boundary preserves every release-side SSH-relay contract, desktop schema + and signature parity, repository type/static/reliability/localization gates, formatting, and file + line budgets without adding a bypass. No existing Node/npm resolver file was touched. +- Does not prove: Node 24/native-runner execution, real post-sign tuple outputs, production signing, + publication, desktop embedding, SSH behavior, native trust, exact oldest baselines, or any enabled + tuple. +- Checklist items satisfied: broad local regression/static gate for the active Work Package 3 + package. +- Follow-up: commit and push the exact package, then record all-six native CI at that exact head. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -9435,12 +9543,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Add RED tests for the disconnected credential-free fail-closed aggregate boundary, then connect exact -verified runtime inputs through canonical request and verified immutable final-manifest bytes. Do not -connect release publication, use production signing credentials, or add a desktop consumer in this -slice. 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. +Commit and push the locally green disconnected credential-free aggregate boundary, then collect +exact-head Node 24 proof from all six native build jobs. Do not connect release publication, use +production signing credentials, or add a desktop consumer in this slice. 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 From cfea521c7c5f63a22c9895ac58931bc75577bb00 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 05:44:14 -0400 Subject: [PATCH 114/267] build(ssh): produce verified runtime manifest tuples Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 8 + .../ssh-relay-runtime-manifest-tuple.mjs | 361 +++++++++++++++++ .../ssh-relay-runtime-manifest-tuple.test.mjs | 375 ++++++++++++++++++ .../ssh-relay-runtime-manifest-validation.mjs | 42 +- .../ssh-relay-runtime-workflow.test.mjs | 3 +- ...elease-implementation-checklist-summary.md | 44 +- ...github-release-implementation-checklist.md | 270 +++++++++++-- ...6-07-14-ssh-relay-github-release-plan.html | 9 + .../ssh/ssh-relay-artifact-consistency.ts | 34 +- .../ssh/ssh-relay-artifact-schema.test.ts | 100 ++++- 10 files changed, 1191 insertions(+), 55 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-manifest-tuple.mjs create mode 100644 config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index fc83d911068..b725137291d 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -160,6 +160,8 @@ jobs: node --check config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs node --check config/scripts/ssh-relay-runtime-manifest-aggregate.mjs node --check config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs + node --check config/scripts/ssh-relay-runtime-manifest-tuple.mjs + node --check config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs pnpm exec vitest run --config config/vitest.config.ts \ config/scripts/ssh-relay-runtime-compatibility.test.mjs \ config/scripts/ssh-relay-node-release-verification.test.mjs \ @@ -189,6 +191,7 @@ jobs: config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs \ config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs \ config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs \ + config/scripts/ssh-relay-runtime-manifest-tuple.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 \ @@ -538,6 +541,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime manifest aggregate syntax check failed' } node --check config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime manifest aggregate test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-manifest-tuple.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime manifest tuple syntax check failed' } + node --check config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime manifest tuple test syntax check failed' } pnpm exec vitest run --config config/vitest.config.ts ` config/scripts/ssh-relay-runtime-compatibility.test.mjs ` config/scripts/ssh-relay-node-release-verification.test.mjs ` @@ -567,6 +574,7 @@ jobs: config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs ` config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs ` config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs ` + config/scripts/ssh-relay-runtime-manifest-tuple.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-manifest-tuple.mjs b/config/scripts/ssh-relay-runtime-manifest-tuple.mjs new file mode 100644 index 00000000000..da01e878f44 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-manifest-tuple.mjs @@ -0,0 +1,361 @@ +import { createHash } from 'node:crypto' +import { createReadStream } from 'node:fs' +import { lstat, readdir, realpath, rm, writeFile } from 'node:fs/promises' +import { join, relative, resolve, sep } from 'node:path' + +import { verifySshRelayRuntimeAggregateFiles } from './ssh-relay-runtime-aggregate-input.mjs' +import { inspectSshRelayRuntimeArchive } from './ssh-relay-runtime-archive.mjs' +import { assertSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' +import { parseSshRelayRuntimeManifestTuple } from './ssh-relay-runtime-manifest-validation.mjs' +import { buildSshRelayRuntimeNativeSigningPlan } from './ssh-relay-runtime-native-signing-plan.mjs' +import { verifyRuntimeTree } from './verify-ssh-relay-runtime.mjs' + +const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u +const ASSET_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,239}$/u +const PRINTABLE_VERSION_PATTERN = /^[\x20-\x7e]+$/u +const TEAM_IDENTIFIER_PATTERN = /^[A-Z0-9]{10}$/u +const THUMBPRINT_PATTERN = /^[A-F0-9]{40}$/u +const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024 +const MAX_METADATA_BYTES = 32 * 1024 * 1024 +const MAX_DESCRIPTOR_BYTES = 32 * 1024 * 1024 +const PRODUCER_TIMEOUT_MS = 15 * 60_000 +const NATIVE_ROLES = new Set(['node', 'node-pty-native', 'parcel-watcher-native', 'native-runtime']) + +function compareAscii(left, right) { + return left < right ? -1 : left > right ? 1 : 0 +} + +function assertObject(value, label) { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`Runtime manifest tuple ${label} must be an object`) + } +} + +function assertExactFields(value, fields, label) { + assertObject(value, label) + const actual = Object.keys(value).sort(compareAscii) + const expected = [...fields].sort(compareAscii) + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + throw new Error(`Runtime manifest tuple ${label} has unexpected or missing fields`) + } +} + +function containsPath(parent, candidate) { + const path = relative(parent, candidate) + return path === '' || (path !== '..' && !path.startsWith(`..${sep}`)) +} + +async function physicalDirectory(path, label) { + const metadata = await lstat(path) + if (!metadata.isDirectory() || metadata.isSymbolicLink()) { + throw new Error(`Runtime manifest tuple ${label} must be a real directory`) + } + return realpath(path) +} + +function expectedNames(identity) { + const digest = identity.contentId.slice('sha256:'.length) + const extension = identity.tupleId.startsWith('win32-') ? 'zip' : 'tar.xz' + const prefix = `orca-ssh-relay-runtime-${identity.tupleId}` + return { + archive: `orca-ssh-relay-runtime-v1-${identity.tupleId}-${digest}.${extension}`, + sbom: `${prefix}.spdx.json`, + provenance: `${prefix}.provenance.json`, + descriptor: `${prefix}.manifest-tuple.json` + } +} + +function sameFileState(before, after) { + return ( + before.dev === after.dev && + before.ino === after.ino && + before.size === after.size && + before.mtimeNs === after.mtimeNs && + before.ctimeNs === after.ctimeNs + ) +} + +async function describeStableFile(root, name, maximumBytes, label, signal) { + signal.throwIfAborted() + const path = join(root, name) + const before = await lstat(path, { bigint: true }) + if (!before.isFile() || before.isSymbolicLink()) { + throw new Error(`Runtime manifest tuple ${label} must be a regular file`) + } + if (before.size <= 0n || before.size > BigInt(maximumBytes)) { + throw new Error(`Runtime manifest tuple ${label} exceeds its bounded size`) + } + const hash = createHash('sha256') + let size = 0 + for await (const chunk of createReadStream(path, { signal })) { + signal.throwIfAborted() + size += chunk.length + if (size > maximumBytes) { + throw new Error(`Runtime manifest tuple ${label} exceeds its bounded size`) + } + hash.update(chunk) + } + const after = await lstat(path, { bigint: true }) + if (!sameFileState(before, after) || BigInt(size) !== before.size) { + throw new Error(`Runtime manifest tuple ${label} changed while hashing`) + } + return { name, size, sha256: `sha256:${hash.digest('hex')}` } +} + +async function assertInitialInputSet(root, names) { + const entries = await readdir(root, { withFileTypes: true }) + const actual = entries.map((entry) => entry.name).sort(compareAscii) + const expected = [names.archive, names.sbom, names.provenance].sort(compareAscii) + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + throw new Error('Runtime manifest tuple requires an exclusive exact input set') + } + for (const entry of entries) { + if (!entry.isFile() || entry.isSymbolicLink()) { + throw new Error(`Runtime manifest tuple input must be a regular file: ${entry.name}`) + } + } +} + +function assertFinalIdentity(identity) { + assertObject(identity, 'final identity') + assertSshRelayRuntimeClosureEntries(identity) + const files = identity.entries.filter((entry) => entry.type === 'file') + const expandedSize = files.reduce((total, entry) => total + entry.size, 0) + if ( + Object.hasOwn(identity, 'archive') || + !DIGEST_PATTERN.test(identity.contentId ?? '') || + computeSshRelayRuntimeContentId(identity) !== identity.contentId || + identity.fileCount !== files.length || + identity.expandedSize !== expandedSize + ) { + throw new Error('Runtime manifest tuple final content identity is inconsistent') + } +} + +function assertVerificationFileShape(file, platform) { + assertObject(file, 'native verification file') + if ( + typeof file.path !== 'string' || + typeof file.role !== 'string' || + !DIGEST_PATTERN.test(file.sha256 ?? '') + ) { + throw new Error('Runtime manifest tuple native verification file is malformed') + } + if (platform === 'darwin') { + const signerKind = file.role === 'node' ? 'official-node' : 'orca-built' + if ( + file.signerKind !== signerKind || + typeof file.authority !== 'string' || + file.authority.length === 0 || + !TEAM_IDENTIFIER_PATTERN.test(file.teamIdentifier ?? '') + ) { + throw new Error('Runtime manifest tuple macOS native verification report is incomplete') + } + } else if (platform === 'win32') { + if ( + (file.role === 'node' + ? file.signerKind !== 'official-node' + : !['orca-built', 'preserved-upstream'].includes(file.signerKind)) || + typeof file.signerSubject !== 'string' || + file.signerSubject.length === 0 || + !THUMBPRINT_PATTERN.test(file.signerThumbprint ?? '') + ) { + throw new Error('Runtime manifest tuple Windows native verification report is incomplete') + } + } +} + +export function createSshRelayRuntimeManifestNativeVerification({ + identity, + report, + tool, + verifiedAt +}) { + assertObject(report, 'native verification report') + const plan = buildSshRelayRuntimeNativeSigningPlan(identity) + if (report.tupleId !== identity.tupleId || report.finalContentId !== identity.contentId) { + throw new Error('Runtime manifest tuple native verification content identity is stale') + } + if ( + plan.platform === 'linux' + ? report.sourceContentId !== undefined && report.sourceContentId !== identity.contentId + : !DIGEST_PATTERN.test(report.sourceContentId ?? '') || + report.sourceContentId === identity.contentId + ) { + throw new Error('Runtime manifest tuple native verification source identity is invalid') + } + if (!Array.isArray(report.verifiedFiles)) { + throw new Error('Runtime manifest tuple native verification files must be an array') + } + const actual = new Map() + for (const file of report.verifiedFiles) { + assertVerificationFileShape(file, plan.platform) + if (actual.has(file.path)) { + throw new Error(`Runtime manifest tuple has duplicate native verification: ${file.path}`) + } + actual.set(file.path, file) + } + const expected = plan.verificationFiles + if (actual.size !== expected.length) { + throw new Error('Runtime manifest tuple native verification report is not complete') + } + for (const file of expected) { + const verified = actual.get(file.path) + if ( + !verified || + verified.role !== file.role || + verified.sha256 !== file.sourceSha256 || + !NATIVE_ROLES.has(verified.role) + ) { + throw new Error(`Runtime manifest tuple native verification hash mismatch: ${file.path}`) + } + } + assertExactFields(tool, ['name', 'version'], 'native verification tool') + if ( + typeof tool.name !== 'string' || + !ASSET_NAME_PATTERN.test(tool.name) || + typeof tool.version !== 'string' || + tool.version.length > 64 || + !PRINTABLE_VERSION_PATTERN.test(tool.version) + ) { + throw new Error('Runtime manifest tuple native verification tool is invalid') + } + if ( + typeof verifiedAt !== 'string' || + new Date(verifiedAt).toISOString() !== verifiedAt || + !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(verifiedAt) + ) { + throw new Error('Runtime manifest tuple verified timestamp is not canonical') + } + return { + policy: plan.policy, + tool: { name: tool.name, version: tool.version }, + verifiedAt, + files: expected.map((file) => ({ path: file.path, sha256: file.sourceSha256 })) + } +} + +function tupleProjection(identity, archive, sbom, provenance, verification) { + return { + tupleId: identity.tupleId, + os: identity.os, + architecture: identity.architecture, + compatibility: identity.compatibility, + nodeVersion: identity.nodeVersion, + dependencies: identity.dependencies, + entries: identity.entries, + contentId: identity.contentId, + archive: { + ...archive, + expandedSize: identity.expandedSize, + fileCount: identity.fileCount + }, + metadataAssets: { sbom, provenance }, + nativeVerification: verification + } +} + +export async function writeSshRelayRuntimeManifestTupleDescriptor({ + runtimeRoot, + inputDirectory, + finalIdentity, + verificationReport, + nativeVerificationTool, + verifiedAt, + signal +}) { + const effectiveSignal = signal + ? AbortSignal.any([signal, AbortSignal.timeout(PRODUCER_TIMEOUT_MS)]) + : AbortSignal.timeout(PRODUCER_TIMEOUT_MS) + effectiveSignal.throwIfAborted() + assertFinalIdentity(finalIdentity) + const verification = createSshRelayRuntimeManifestNativeVerification({ + identity: finalIdentity, + report: verificationReport, + tool: nativeVerificationTool, + verifiedAt + }) + const [physicalRuntime, physicalInput] = await Promise.all([ + physicalDirectory(resolve(runtimeRoot), 'runtime root'), + physicalDirectory(resolve(inputDirectory), 'input root') + ]) + if ( + containsPath(physicalRuntime, physicalInput) || + containsPath(physicalInput, physicalRuntime) + ) { + throw new Error('Runtime manifest tuple runtime and input roots must be physically disjoint') + } + const names = expectedNames(finalIdentity) + await assertInitialInputSet(physicalInput, names) + // Why: a native report cannot authorize a descriptor for bytes that changed after its probes. + await verifyRuntimeTree(physicalRuntime, finalIdentity) + const [archive, sbom, provenance] = await Promise.all([ + describeStableFile(physicalInput, names.archive, MAX_ARCHIVE_BYTES, 'archive', effectiveSignal), + describeStableFile( + physicalInput, + names.sbom, + MAX_METADATA_BYTES, + 'SBOM metadata', + effectiveSignal + ), + describeStableFile( + physicalInput, + names.provenance, + MAX_METADATA_BYTES, + 'provenance metadata', + effectiveSignal + ) + ]) + await inspectSshRelayRuntimeArchive(join(physicalInput, names.archive), finalIdentity, { + signal: effectiveSignal + }) + await verifyRuntimeTree(physicalRuntime, finalIdentity) + const tuple = parseSshRelayRuntimeManifestTuple( + tupleProjection(finalIdentity, archive, sbom, provenance, verification) + ) + const descriptorBytes = Buffer.from( + `${JSON.stringify({ schemaVersion: 1, tuple }, null, 2)}\n`, + 'utf8' + ) + if (descriptorBytes.length === 0 || descriptorBytes.length > MAX_DESCRIPTOR_BYTES) { + throw new Error('Runtime manifest tuple descriptor exceeds its bounded size') + } + const descriptor = { + name: names.descriptor, + size: descriptorBytes.length, + sha256: `sha256:${createHash('sha256').update(descriptorBytes).digest('hex')}` + } + const descriptorPath = join(physicalInput, names.descriptor) + let descriptorWritten = false + try { + await writeFile(descriptorPath, descriptorBytes, { + flag: 'wx', + mode: 0o600, + signal: effectiveSignal + }) + descriptorWritten = true + await verifySshRelayRuntimeAggregateFiles({ + inputDirectory: physicalInput, + files: [descriptor, archive, sbom, provenance], + signal: effectiveSignal + }) + return { + tupleId: finalIdentity.tupleId, + tuple, + input: { tupleId: finalIdentity.tupleId, descriptor, archive, sbom, provenance } + } + } catch (error) { + if (descriptorWritten) { + await rm(descriptorPath, { force: true }) + } + throw error + } +} + +export const SSH_RELAY_RUNTIME_MANIFEST_TUPLE_LIMITS = Object.freeze({ + maximumArchiveBytes: MAX_ARCHIVE_BYTES, + maximumMetadataBytes: MAX_METADATA_BYTES, + maximumDescriptorBytes: MAX_DESCRIPTOR_BYTES, + timeoutMs: PRODUCER_TIMEOUT_MS +}) diff --git a/config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs b/config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs new file mode 100644 index 00000000000..430ac0be8e7 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs @@ -0,0 +1,375 @@ +import { createHash } from 'node:crypto' +import { + appendFile, + mkdir, + mkdtemp, + readFile, + readdir, + rm, + truncate, + writeFile +} from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { createSshRelayRuntimeArchive } from './ssh-relay-runtime-archive.mjs' +import { expectedSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' +import { + SSH_RELAY_RUNTIME_MANIFEST_TUPLE_LIMITS, + createSshRelayRuntimeManifestNativeVerification, + writeSshRelayRuntimeManifestTupleDescriptor +} from './ssh-relay-runtime-manifest-tuple.mjs' +import { buildSshRelayRuntimeNativeSigningPlan } from './ssh-relay-runtime-native-signing-plan.mjs' + +const temporaryDirectories = [] +const SOURCE_DATE_EPOCH = 1_788_739_200 +const VERIFIED_AT = '2026-07-15T00:00:00.000Z' + +function sha256(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +async function runtimeFixture() { + const root = await mkdtemp(join(tmpdir(), 'ssh-relay-manifest-tuple-')) + temporaryDirectories.push(root) + const runtimeRoot = join(root, 'runtime') + const inputDirectory = join(root, 'aggregate-input') + await Promise.all([mkdir(runtimeRoot), mkdir(inputDirectory)]) + const entries = [] + for (const entry of expectedSshRelayRuntimeClosureEntries('win32-x64')) { + const path = join(runtimeRoot, ...entry.path.split('/')) + if (entry.type === 'directory') { + await mkdir(path, { recursive: true, mode: entry.mode }) + entries.push(entry) + continue + } + const bytes = Buffer.from(`final signed fixture:${entry.path}`) + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, bytes, { mode: entry.mode }) + entries.push({ ...entry, size: bytes.length, sha256: sha256(bytes) }) + } + const base = { + identitySchemaVersion: 1, + tupleId: 'win32-x64', + os: 'win32', + architecture: 'x64', + compatibility: sshRelayRuntimeCompatibility['win32-x64'], + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries + } + const files = entries.filter((entry) => entry.type === 'file') + const finalIdentity = { + ...base, + contentId: computeSshRelayRuntimeContentId(base), + fileCount: files.length, + expandedSize: files.reduce((total, entry) => total + entry.size, 0) + } + const archive = await createSshRelayRuntimeArchive({ + runtimeRoot, + outputDirectory: inputDirectory, + identity: finalIdentity, + sourceDateEpoch: SOURCE_DATE_EPOCH + }) + const sbomName = 'orca-ssh-relay-runtime-win32-x64.spdx.json' + const provenanceName = 'orca-ssh-relay-runtime-win32-x64.provenance.json' + await Promise.all([ + writeFile(join(inputDirectory, sbomName), '{"spdxVersion":"SPDX-2.3"}\n'), + writeFile(join(inputDirectory, provenanceName), '{"_type":"https://in-toto.io/Statement/v1"}\n') + ]) + const plan = buildSshRelayRuntimeNativeSigningPlan(finalIdentity) + return { + root, + runtimeRoot, + inputDirectory, + finalIdentity, + archive, + descriptorName: 'orca-ssh-relay-runtime-win32-x64.manifest-tuple.json', + verificationReport: { + tupleId: finalIdentity.tupleId, + sourceContentId: `sha256:${'f'.repeat(64)}`, + finalContentId: finalIdentity.contentId, + verifiedFiles: plan.verificationFiles.map((entry) => ({ + path: entry.path, + role: entry.role, + sha256: entry.sourceSha256, + signerKind: entry.role === 'node' ? 'official-node' : 'orca-built', + signerSubject: entry.role === 'node' ? 'CN=OpenJS Foundation' : 'CN=SignPath Foundation', + signerThumbprint: entry.role === 'node' ? 'A'.repeat(40) : 'F'.repeat(40) + })) + }, + nativeVerificationTool: { name: 'pwsh', version: '7.4.6' }, + verifiedAt: VERIFIED_AT + } +} + +function producerInput(fixture) { + return { + runtimeRoot: fixture.runtimeRoot, + inputDirectory: fixture.inputDirectory, + finalIdentity: fixture.finalIdentity, + verificationReport: fixture.verificationReport, + nativeVerificationTool: fixture.nativeVerificationTool, + verifiedAt: fixture.verifiedAt + } +} + +function identityForTuple(tupleId) { + const os = tupleId.startsWith('linux-') + ? 'linux' + : tupleId.startsWith('darwin-') + ? 'darwin' + : 'win32' + const architecture = tupleId.endsWith('arm64') ? 'arm64' : 'x64' + const entries = expectedSshRelayRuntimeClosureEntries(tupleId).map((entry) => { + if (entry.type === 'directory') { + return entry + } + const bytes = Buffer.from(`identity fixture:${tupleId}:${entry.path}`) + return { ...entry, size: bytes.length, sha256: sha256(bytes) } + }) + const base = { + identitySchemaVersion: 1, + tupleId, + os, + architecture, + compatibility: sshRelayRuntimeCompatibility[tupleId], + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries + } + const files = entries.filter((entry) => entry.type === 'file') + return { + ...base, + contentId: computeSshRelayRuntimeContentId(base), + fileCount: files.length, + expandedSize: files.reduce((total, entry) => total + entry.size, 0) + } +} + +function verificationReportFor(identity) { + const plan = buildSshRelayRuntimeNativeSigningPlan(identity) + return { + tupleId: identity.tupleId, + sourceContentId: plan.platform === 'linux' ? identity.contentId : `sha256:${'f'.repeat(64)}`, + finalContentId: identity.contentId, + verifiedFiles: plan.verificationFiles.map((entry) => { + const base = { path: entry.path, role: entry.role, sha256: entry.sourceSha256 } + if (plan.platform === 'darwin') { + const officialNode = entry.role === 'node' + return { + ...base, + signerKind: officialNode ? 'official-node' : 'orca-built', + authority: officialNode + ? 'Developer ID Application: Node.js Foundation' + : 'Developer ID Application: Orca', + teamIdentifier: officialNode ? 'HX7739G8FX' : 'ABCDEFGHIJ' + } + } + return base + }) + } +} + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })) + ) +}) + +describe('SSH relay runtime post-sign manifest tuple', () => { + it('writes one exact descriptor from the verified final tree and archive', async () => { + const fixture = await runtimeFixture() + + const result = await writeSshRelayRuntimeManifestTupleDescriptor(producerInput(fixture)) + const descriptor = JSON.parse( + await readFile(join(fixture.inputDirectory, fixture.descriptorName), 'utf8') + ) + + expect((await readdir(fixture.inputDirectory)).toSorted()).toEqual( + [ + fixture.archive.name, + 'orca-ssh-relay-runtime-win32-x64.spdx.json', + 'orca-ssh-relay-runtime-win32-x64.provenance.json', + fixture.descriptorName + ].toSorted() + ) + expect(descriptor).toEqual({ schemaVersion: 1, tuple: result.tuple }) + expect(result).toMatchObject({ + tupleId: 'win32-x64', + tuple: { + contentId: fixture.finalIdentity.contentId, + archive: { + name: fixture.archive.name, + size: fixture.archive.size, + sha256: fixture.archive.sha256 + }, + nativeVerification: { + policy: 'signpath-authenticode-v1', + tool: fixture.nativeVerificationTool, + verifiedAt: VERIFIED_AT + } + }, + input: { + tupleId: 'win32-x64', + descriptor: { name: fixture.descriptorName }, + archive: { + name: fixture.archive.name, + size: fixture.archive.size, + sha256: fixture.archive.sha256 + } + } + }) + expect(result.tuple.nativeVerification.files).toEqual( + fixture.verificationReport.verifiedFiles + .map(({ path, sha256: digest }) => ({ path, sha256: digest })) + .toSorted((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0)) + ) + }) + + it('rejects stale, incomplete, duplicate, or mismatched native verification reports', async () => { + const stale = await runtimeFixture() + stale.verificationReport.finalContentId = `sha256:${'e'.repeat(64)}` + await expect(writeSshRelayRuntimeManifestTupleDescriptor(producerInput(stale))).rejects.toThrow( + /verification.*content|content.*verification/i + ) + + const missing = await runtimeFixture() + missing.verificationReport.verifiedFiles.pop() + await expect( + writeSshRelayRuntimeManifestTupleDescriptor(producerInput(missing)) + ).rejects.toThrow(/native verification.*complete|missing/i) + + const duplicate = await runtimeFixture() + duplicate.verificationReport.verifiedFiles.push({ + ...duplicate.verificationReport.verifiedFiles[0] + }) + await expect( + writeSshRelayRuntimeManifestTupleDescriptor(producerInput(duplicate)) + ).rejects.toThrow(/duplicate|complete/i) + + const mismatched = await runtimeFixture() + mismatched.verificationReport.verifiedFiles[0].sha256 = `sha256:${'d'.repeat(64)}` + await expect( + writeSshRelayRuntimeManifestTupleDescriptor(producerInput(mismatched)) + ).rejects.toThrow(/native verification.*hash|mismatch/i) + }) + + it('rejects final-tree or archive mutation without leaving a descriptor', async () => { + const tree = await runtimeFixture() + await appendFile(join(tree.runtimeRoot, 'relay.js'), ':mutated') + await expect(writeSshRelayRuntimeManifestTupleDescriptor(producerInput(tree))).rejects.toThrow( + /tree.*integrity|integrity.*tree/i + ) + await expect(readFile(join(tree.inputDirectory, tree.descriptorName))).rejects.toMatchObject({ + code: 'ENOENT' + }) + + const archive = await runtimeFixture() + await writeFile(join(archive.inputDirectory, archive.archive.name), 'mutated archive') + await expect( + writeSshRelayRuntimeManifestTupleDescriptor(producerInput(archive)) + ).rejects.toThrow(/archive|zip/i) + await expect( + readFile(join(archive.inputDirectory, archive.descriptorName)) + ).rejects.toMatchObject({ code: 'ENOENT' }) + }) + + it('requires an exclusive exact input set and bounded regular metadata', async () => { + const extra = await runtimeFixture() + await writeFile(join(extra.inputDirectory, 'unexpected.bin'), 'extra') + await expect(writeSshRelayRuntimeManifestTupleDescriptor(producerInput(extra))).rejects.toThrow( + /missing|unexpected|exact/i + ) + + const existing = await runtimeFixture() + await writeFile(join(existing.inputDirectory, existing.descriptorName), 'stale') + await expect( + writeSshRelayRuntimeManifestTupleDescriptor(producerInput(existing)) + ).rejects.toThrow(/exclusive|unexpected|exact/i) + + const oversized = await runtimeFixture() + await truncate( + join(oversized.inputDirectory, 'orca-ssh-relay-runtime-win32-x64.spdx.json'), + SSH_RELAY_RUNTIME_MANIFEST_TUPLE_LIMITS.maximumMetadataBytes + 1 + ) + await expect( + writeSshRelayRuntimeManifestTupleDescriptor(producerInput(oversized)) + ).rejects.toThrow(/metadata|size|bounded/i) + }) + + it.skipIf(process.platform === 'win32')('rejects linked aggregate inputs', async () => { + const fixture = await runtimeFixture() + const sbom = join(fixture.inputDirectory, 'orca-ssh-relay-runtime-win32-x64.spdx.json') + const target = join(fixture.root, 'linked-sbom') + await writeFile(target, 'linked') + await rm(sbom) + const { symlink } = await import('node:fs/promises') + await symlink(target, sbom) + + await expect( + writeSshRelayRuntimeManifestTupleDescriptor(producerInput(fixture)) + ).rejects.toThrow(/regular|unexpected|linked/i) + }) + + it('rejects malformed attestation metadata and honors cancellation', async () => { + const tool = await runtimeFixture() + tool.nativeVerificationTool.version = 'bad\nversion' + await expect(writeSshRelayRuntimeManifestTupleDescriptor(producerInput(tool))).rejects.toThrow( + /tool|version|native verification/i + ) + + const time = await runtimeFixture() + time.verifiedAt = '2026-07-15T00:00:00Z' + await expect(writeSshRelayRuntimeManifestTupleDescriptor(producerInput(time))).rejects.toThrow( + /timestamp|verified|date/i + ) + + const cancelled = await runtimeFixture() + const controller = new AbortController() + controller.abort(new Error('cancel tuple descriptor')) + await expect( + writeSshRelayRuntimeManifestTupleDescriptor({ + ...producerInput(cancelled), + signal: controller.signal + }) + ).rejects.toThrow(/cancel tuple descriptor/i) + }) + + it('derives Linux and macOS native policies from their complete verifier reports', () => { + const linux = identityForTuple('linux-arm64-glibc') + const macos = identityForTuple('darwin-arm64') + const common = { tool: { name: 'node', version: '24.18.0' }, verifiedAt: VERIFIED_AT } + + const linuxVerification = createSshRelayRuntimeManifestNativeVerification({ + ...common, + identity: linux, + report: verificationReportFor(linux) + }) + expect(linuxVerification.policy).toBe('linux-hash-only-v1') + expect(linuxVerification.files).toHaveLength(3) + const macosReport = verificationReportFor(macos) + const macosVerification = createSshRelayRuntimeManifestNativeVerification({ + ...common, + identity: macos, + report: macosReport + }) + expect(macosVerification.policy).toBe('apple-developer-id-v1') + expect(macosVerification.files).toHaveLength(4) + + delete macosReport.verifiedFiles[0].teamIdentifier + expect(() => + createSshRelayRuntimeManifestNativeVerification({ + ...common, + identity: macos, + report: macosReport + }) + ).toThrow(/macOS.*incomplete/i) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-manifest-validation.mjs b/config/scripts/ssh-relay-runtime-manifest-validation.mjs index 575f9be7073..2bbf6d18d04 100644 --- a/config/scripts/ssh-relay-runtime-manifest-validation.mjs +++ b/config/scripts/ssh-relay-runtime-manifest-validation.mjs @@ -230,13 +230,7 @@ function assertTupleEntries(tuple) { ['relay', 'relay.js'], ['relay-watcher', 'relay-watcher.js'] ]) - for (const role of [ - 'node', - 'relay', - 'relay-watcher', - 'node-pty-native', - 'parcel-watcher-native' - ]) { + for (const role of ['node', 'relay', 'relay-watcher', 'parcel-watcher-native']) { const matching = files.filter((entry) => entry.role === role) if ( matching.length !== 1 || @@ -245,6 +239,32 @@ function assertTupleEntries(tuple) { throw new Error(`SSH relay runtime manifest requires one valid ${role} entry`) } } + const nativePaths = + tuple.os === 'win32' + ? { + 'node-pty-native': [ + 'node_modules/node-pty/build/Release/conpty.node', + 'node_modules/node-pty/build/Release/conpty_console_list.node' + ], + 'native-runtime': [ + 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe', + 'node_modules/node-pty/build/Release/conpty/conpty.dll' + ] + } + : { + 'node-pty-native': ['node_modules/node-pty/build/Release/pty.node'], + 'native-runtime': + tuple.os === 'darwin' ? ['node_modules/node-pty/build/Release/spawn-helper'] : [] + } + for (const [role, expected] of Object.entries(nativePaths)) { + const actual = files + .filter((entry) => entry.role === role) + .map((entry) => entry.path) + .sort() + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + throw new Error(`SSH relay runtime manifest has invalid ${role} closure`) + } + } if ( !files.some((entry) => entry.path === 'node_modules/node-pty/package.json') || !files.some((entry) => entry.path === 'node_modules/@parcel/watcher/package.json') || @@ -337,6 +357,12 @@ function assertTupleConsistency(tuple) { } } +export function parseSshRelayRuntimeManifestTuple(input) { + const parsed = tupleSchema.parse(input) + assertTupleConsistency(parsed) + return parsed +} + function parseReleaseTag(tag) { for (const [pattern, channel] of [ [/^v(\d+\.\d+\.\d+)$/u, 'stable'], @@ -363,7 +389,7 @@ export function parseSshRelayRuntimeUnsignedManifest(input) { throw new Error(`SSH relay runtime manifest has a duplicate tuple: ${tuple.tupleId}`) } tupleIds.add(tuple.tupleId) - assertTupleConsistency(tuple) + parseSshRelayRuntimeManifestTuple(tuple) } return parsed } diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 1d57cb83f8f..9ea5e1fdb8a 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -113,7 +113,8 @@ describe('SSH relay runtime artifact workflow', () => { for (const moduleName of [ 'manifest-assembly', 'manifest-signing-handoff', - 'manifest-aggregate' + 'manifest-aggregate', + 'manifest-tuple' ]) { const script = `ssh-relay-runtime-${moduleName}` expect(source.split(`${script}.test.mjs`)).toHaveLength(5) diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md index 24ae72ccc8f..a4fdb0da153 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 @@ -60,7 +60,7 @@ complete a box. 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. -- [ ] **WP3 active implementation — Build the credential-free fail-closed aggregate boundary.** +- [ ] **WP3 active implementation — Supply verified post-sign tuple descriptors to the aggregate.** Windows compatibility-kind parity is closed locally and on all six target-native jobs in exact-head run [29393022768](https://github.com/stablyai/orca/actions/runs/29393022768) under @@ -68,13 +68,24 @@ complete a box. Disconnected canonical assembly and signing-handoff modules are closed locally and on all six Node 24 native jobs in exact-head run [29395319239](https://github.com/stablyai/orca/actions/runs/29395319239) under - `E-M4-MANIFEST-HANDOFF-CI-001`. Next, connect only the credential-free verified aggregate - inputs, canonical signing request, verified signer return, and immutable final-manifest output - behind fail-closed tests. No production workflow, publication, desktop consumer, signing - credential, or tuple is connected. The purpose-named missing-module RED is recorded under - `E-M4-MANIFEST-AGGREGATE-LOCAL-RED-001`; the implementation and broad local gates are green - under `E-M4-MANIFEST-AGGREGATE-LOCAL-001` and - `E-M4-MANIFEST-AGGREGATE-LOCAL-002`. Exact-head Node 24 native CI remains open. + `E-M4-MANIFEST-HANDOFF-CI-001`. The disconnected credential-free aggregate boundary is closed + locally and on all six Node 24 native jobs in exact-head run + [29397871159](https://github.com/stablyai/orca/actions/runs/29397871159) under + `E-M4-MANIFEST-AGGREGATE-LOCAL-001`, `E-M4-MANIFEST-AGGREGATE-LOCAL-002`, and + `E-M4-MANIFEST-AGGREGATE-CI-001`. Next, produce the exact post-sign tuple descriptor only from + a fully verified returned runtime tree and bind it to the archive, SBOM, provenance, native + assessment, and content identity under credential-free fail-closed tests. No production + workflow, publication, desktop consumer, signing credential, or tuple is connected. The + purpose-named missing-module RED is recorded under + `E-M4-MANIFEST-TUPLE-LOCAL-RED-001`. The first implementation run exposed that both manifest + validators incorrectly require one node-pty native file for Windows even though the proven + closure contains `conpty.node` and `conpty_console_list.node`; correct and parity-test the exact + per-platform role counts first (`E-M4-MANIFEST-TUPLE-SCHEMA-RED-001`). The correction, + credential-free producer, 228-test release suite, 48-test desktop parity suite, and static gates + are locally green under `E-M4-MANIFEST-TUPLE-LOCAL-001` and + `E-M4-MANIFEST-TUPLE-LOCAL-002`. Commit/push authorization for PR-contained work was granted + on 2026-07-15; committing, pushing, and collecting exact-head Node 24 native CI is the active + gate. Merging to `main` remains prohibited without separate user action. ## Work packages, in required order @@ -125,12 +136,21 @@ complete a box. `E-M4-MANIFEST-HANDOFF-LOCAL-RED-001`, `E-M4-MANIFEST-HANDOFF-LOCAL-001`, and `E-M4-MANIFEST-HANDOFF-LOCAL-002`; all-six Node 24 proof is recorded under `E-M4-MANIFEST-HANDOFF-CI-001`. -- [ ] **In progress — 2026-07-15, Codex implementation owner:** add the disconnected, credential-free +- [x] Add the disconnected, credential-free fail-closed aggregate boundary from exact verified runtime inputs through canonical request and verified final-manifest bytes. Keep native/manifest credentials, publication, desktop - consumers, and every tuple outside this slice. The code and local proof are complete under - `E-M4-MANIFEST-AGGREGATE-LOCAL-001` and `E-M4-MANIFEST-AGGREGATE-LOCAL-002`; native CI is the - remaining gate. + consumers, and every tuple outside this slice + (`E-M4-MANIFEST-AGGREGATE-LOCAL-001`, `E-M4-MANIFEST-AGGREGATE-LOCAL-002`, + `E-M4-MANIFEST-AGGREGATE-CI-001`). +- [ ] **In progress — 2026-07-15, Codex implementation owner:** add the credential-free post-sign + tuple-descriptor producer and native-verification handoff needed to supply the aggregate. + Derive it only from a fully verified returned runtime tree; keep real signing, publication, + desktop consumers, and every tuple outside this slice. The missing-module RED is recorded under + `E-M4-MANIFEST-TUPLE-LOCAL-RED-001`. Before GREEN, correct release/desktop tuple-role + cardinality for the real Windows closure under `E-M4-MANIFEST-TUPLE-SCHEMA-RED-001`. Code and + local proof are complete under `E-M4-MANIFEST-TUPLE-LOCAL-001` and + `E-M4-MANIFEST-TUPLE-LOCAL-002`; commit/push is authorized and exact-head Node 24 native CI is + now the active gate. Do not merge to `main`. - [ ] 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 0861ee0014d..6ca81fa5f14 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 credential-free fail-closed aggregate boundary — **In progress — 2026-07-15, Codex implementation owner**. The disconnected release-DAG, aggregate/read-back byte boundaries, Windows artifact/manifest parity, and canonical manifest/signing handoff are closed locally and on all six native build jobs under E-M4-RELEASE-DAG-LOCAL-001, E-M4-RELEASE-DAG-CI-001, E-M4-AGGREGATE-READBACK-LOCAL-001, E-M4-AGGREGATE-READBACK-CI-001, E-M4-WINDOWS-MANIFEST-PARITY-LOCAL-001, E-M4-WINDOWS-MANIFEST-PARITY-CI-001, E-M4-MANIFEST-HANDOFF-LOCAL-001, E-M4-MANIFEST-HANDOFF-LOCAL-002, and E-M4-MANIFEST-HANDOFF-CI-001. The exact-file aggregate boundary is implemented and locally green under E-M4-MANIFEST-AGGREGATE-LOCAL-001 and E-M4-MANIFEST-AGGREGATE-LOCAL-002; exact-head Node 24 native CI remains required. This package may not publish, connect desktop consumers, use production native/manifest signing credentials, 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** — the purpose-named missing-module RED is closed by a bounded aggregate that verifies the exact descriptor/archive/SBOM/provenance file set, derives canonical unsigned bytes only from the hash-verified post-sign tuple descriptor, creates the existing Ed25519 request, reassembles it at finalization, verifies the signer return, and emits one content-addressed final-manifest asset. Focused 20-test and broad 221-test release-side suites, 47 desktop parity tests, syntax, typecheck, full lint, formatting, max-lines, and diff gates pass under E-M4-MANIFEST-AGGREGATE-LOCAL-001 and E-M4-MANIFEST-AGGREGATE-LOCAL-002. The suite is wired into all six native artifact jobs; exact-head Node 24 CI is next. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, aggregate publication wiring, desktop embedding, and native trust remain separately gated. Nothing is published or enabled, and legacy remains the production default.
      +Current phase: Milestone 4 / Work Package 3 post-sign tuple descriptor plus exact closure/schema parity correction — **In progress — 2026-07-15, Codex implementation owner**. The tuple-specific native role correction and credential-free producer are locally green under E-M4-MANIFEST-TUPLE-LOCAL-001 and E-M4-MANIFEST-TUPLE-LOCAL-002. The producer verifies a physically disjoint final runtime tree before and after archive inspection, accepts only a complete platform-specific native-verifier report bound to the final content ID, hashes bounded stable archive/SBOM/provenance inputs, writes one exclusive tuple descriptor, and re-verifies the exact closed four-file aggregate set. Exact-head Node 24 execution on all six native jobs remains required before this slice closes. It may not publish, connect desktop consumers, use production signing credentials, 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** — focused producer/aggregate/workflow/desktop parity passes 4 files / 50 tests, the release-side suite passes 40 files / 228 tests, desktop parity passes 3 files / 48 tests, and typecheck, full lint, formatting, syntax, max-lines, and diff gates pass under E-M4-MANIFEST-TUPLE-LOCAL-001 and E-M4-MANIFEST-TUPLE-LOCAL-002. The HTML plan, release validator, and desktop validator now share exact Linux/macOS/Windows native role cardinality. The user authorized Codex to drive PR-contained commit, push, CI, and publication-preparation work end to end while explicitly prohibiting merge to `main`; committing and pushing this package for exact-head Node 24 native CI is the active gate. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, publication wiring, semantic regeneration of post-sign SBOM/provenance, desktop embedding, 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) @@ -73,13 +73,18 @@ same change as the work it records. request, accepted-key/signature verification, deterministic final bytes, and all-six Node 24 workflow parity are green under E-M4-MANIFEST-HANDOFF-LOCAL-001, E-M4-MANIFEST-HANDOFF-LOCAL-002, and E-M4-MANIFEST-HANDOFF-CI-001. -- Active package: Work Package 3 disconnected fail-closed aggregate boundary. Connect exact verified - runtime inputs to canonical unsigned bytes, a bounded signing request, verified signer return, and - immutable final-manifest output under credential-free tests. The implementation and local gates - are green under E-M4-MANIFEST-AGGREGATE-LOCAL-001 and E-M4-MANIFEST-AGGREGATE-LOCAL-002; exact-head - Node 24 native CI remains open. Production native/manifest signing credentials, publication, - desktop consumers, and tuple enablement remain outside this slice. No broader Milestone 4 box is - checked until its evidence exists. +- Completed Work Package 3 gate: the disconnected fail-closed aggregate boundary is green locally + and on all six exact-head Node 24 native jobs under E-M4-MANIFEST-AGGREGATE-LOCAL-001, + E-M4-MANIFEST-AGGREGATE-LOCAL-002, and E-M4-MANIFEST-AGGREGATE-CI-001. +- Active package: Work Package 3 credential-free post-sign tuple-descriptor producer and + native-verification handoff. The descriptor must be derived only after returned-tree identity and + native policy verification and must bind the exact archive, SBOM, provenance, native assessment, + and content identity. E-M4-MANIFEST-TUPLE-SCHEMA-RED-001 requires an in-package release/desktop + correction for tuple-specific native role counts before the producer can pass the real Windows + closure. The correction and producer are locally green under E-M4-MANIFEST-TUPLE-LOCAL-001 and + E-M4-MANIFEST-TUPLE-LOCAL-002; exact-head Node 24 native CI remains open. Production signing + credentials, publication, desktop consumers, and tuple enablement remain outside this slice. No + broader Milestone 4 box is checked until its evidence exists. - 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 @@ -197,10 +202,12 @@ 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: commit and push the locally green disconnected aggregate boundary, then - collect exact-head Node 24 proof from all six native build jobs. Keep real native/manifest signing - credentials, endpoint trust, missing exact-floor snapshots, production publication, desktop - consumers, and every tuple's enabled state outside this slice. +- Next required action: commit and push the locally green post-sign tuple package, then collect + exact-head Node 24 proof from all six native build jobs. Commit/push and PR-contained CI work are + authorized; merging to `main` is not. Keep semantic + post-sign metadata generation, real native/manifest signing credentials, endpoint trust, missing + exact-floor snapshots, production publication, desktop consumers, and every tuple's enabled state + outside this slice. ## Non-Negotiable Invariants @@ -1544,6 +1551,10 @@ focused commands as their scripts/tests are introduced. - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs` (E-M4-MANIFEST-HANDOFF-LOCAL-RED-001, E-M4-MANIFEST-HANDOFF-LOCAL-001) - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs` (E-M4-MANIFEST-AGGREGATE-LOCAL-RED-001, E-M4-MANIFEST-AGGREGATE-LOCAL-001) - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs config/scripts/ssh-relay-runtime-aggregate-input.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` (E-M4-MANIFEST-AGGREGATE-LOCAL-001; exact-file boundary and all-six workflow-source wiring) +- [x] Exact-head Node 24 native workflow execution of + `config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs` on all six runner families + (E-M4-MANIFEST-AGGREGATE-CI-001) +- [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs` (E-M4-MANIFEST-TUPLE-LOCAL-RED-001, E-M4-MANIFEST-TUPLE-SCHEMA-RED-001, E-M4-MANIFEST-TUPLE-LOCAL-001) - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay*.test.mjs` (E-M4-MANIFEST-HANDOFF-LOCAL-002; all release-side SSH-relay contracts) - [x] `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` (E-M4-MANIFEST-HANDOFF-LOCAL-002; desktop schema/signature parity) @@ -9411,8 +9422,7 @@ diff --check`. ### E-M4-MANIFEST-AGGREGATE-LOCAL-001 — Exact-file aggregate and signing boundary pass locally - Date: 2026-07-15 -- Commit SHA / PR: uncommitted implementation atop - `127ddbcc154fbdf2641bc80d67a3d68d862ab1ca`; draft PR #8741 +- Commit SHA / PR: `5e0c776d017b3cf4d57011cb1f4cdc919bbc8782`; draft PR #8741 - Runner: macOS 26.2 arm64, native local worktree; Node v26.0.0 and pnpm 10.24.0 - Remote: not applicable; disconnected credential-free release boundary - Transport/network: local filesystem only; no network, GitHub release, signer service, credential, @@ -9446,8 +9456,7 @@ diff --check`. ### E-M4-MANIFEST-AGGREGATE-LOCAL-002 — Broad aggregate regressions and static gates pass - Date: 2026-07-15 -- Commit SHA / PR: same uncommitted package atop - `127ddbcc154fbdf2641bc80d67a3d68d862ab1ca`; draft PR #8741 +- Commit SHA / PR: `5e0c776d017b3cf4d57011cb1f4cdc919bbc8782`; draft PR #8741 - Runner: macOS 26.2 arm64, native local worktree; Node v26.0.0 and pnpm 10.24.0 - Remote: not applicable - Transport/network: local filesystem only; no network, credential, release, SSH host, publication, @@ -9484,7 +9493,218 @@ diff --check`. tuple. - Checklist items satisfied: broad local regression/static gate for the active Work Package 3 package. -- Follow-up: commit and push the exact package, then record all-six native CI at that exact head. +- Follow-up: collect and record all-six native CI at that exact head. + +### E-M4-MANIFEST-AGGREGATE-CI-001 — Exact-file aggregate passes all six native Node 24 jobs + +- Date: 2026-07-15 +- Commit SHA / PR: `5e0c776d017b3cf4d57011cb1f4cdc919bbc8782`; draft PR #8741 +- Workflow and source: exact-head SSH Relay Runtime Artifacts run + [29397871159](https://github.com/stablyai/orca/actions/runs/29397871159), checked out from the PR + head rather than GitHub's synthetic merge; Node v24.18.0 +- Native runners and results: + - macOS arm64: `macos15` image `20260706.0213.1`, job + [87295382518](https://github.com/stablyai/orca/actions/runs/29397871159/job/87295382518), PASS + in 2m55s; aggregate suite 6/6 in 119 ms. + - Windows x64: `win22` image `20260706.237.1`, job + [87295382522](https://github.com/stablyai/orca/actions/runs/29397871159/job/87295382522), PASS + in 5m00s; aggregate suite 6/6 in 342 ms. + - macOS x64: `macos15` image `20260629.0276.1`, job + [87295382539](https://github.com/stablyai/orca/actions/runs/29397871159/job/87295382539), PASS + in 5m17s; aggregate suite 6/6 in 675 ms. + - Linux x64: `ubuntu24` image `20260705.232.1`, job + [87295382567](https://github.com/stablyai/orca/actions/runs/29397871159/job/87295382567), PASS + in 4m22s; aggregate suite 6/6 in 257 ms. + - Windows arm64: `win11-arm64` image `20260706.102.1`, job + [87295382578](https://github.com/stablyai/orca/actions/runs/29397871159/job/87295382578), PASS + in 9m46s; aggregate suite 6/6 in 553 ms. + - Linux arm64: `ubuntu24-arm64` image `20260714.61.1`, job + [87295382626](https://github.com/stablyai/orca/actions/runs/29397871159/job/87295382626), PASS + in 5m21s; aggregate suite 6/6 in 188 ms. +- Exact workflow command: the POSIX and Windows native jobs run `node --check` for the aggregate + module and suite, then include + `config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs` in the purpose-named Vitest artifact + contract command. POSIX jobs report 38 files / 217 passed; Windows jobs report 39 files / 213 + passed plus 8 explicitly platform-skipped tests. +- Supplemental exact-head results: + - Rocky 8 oldest-userland jobs pass on Linux x64 + [87296348785](https://github.com/stablyai/orca/actions/runs/29397871159/job/87296348785) in 35s and + Linux arm64 + [87296348786](https://github.com/stablyai/orca/actions/runs/29397871159/job/87296348786) in 58s. + - Windows x64 oldest-floor job + [87297136187](https://github.com/stablyai/orca/actions/runs/29397871159/job/87297136187) passes. + Content ID is `sha256:4224aee6de820a94263f05ebfc58edd421921431e8cbdf1d5ec6ddcf783afc26`; + smoke is 8,788.4616 ms, RSS is 49,508,352 bytes, and total verification is 9,909.3156 ms. + - Windows arm64 oldest-floor job + [87297136099](https://github.com/stablyai/orca/actions/runs/29397871159/job/87297136099) fails only + the declared OS-build oracle because the hosted image is build 26200 rather than required build 26100. Before that rejection, the runtime passes platform/architecture and full smoke with 60 + entries, 42 files, 85,213,511 expanded bytes, content ID + `sha256:02edf462be83c2864a89546d5344d348f9e07ce10964660342608a8c614e47db`, + 5,835.1398 ms smoke, 49,713,152-byte RSS, and 7,680.2139 ms total verification. +- Broad exact-head regressions: PR Checks + [29397871156](https://github.com/stablyai/orca/actions/runs/29397871156) job + [87295381961](https://github.com/stablyai/orca/actions/runs/29397871156/job/87295381961) passes; + Golden E2E + [29397871176](https://github.com/stablyai/orca/actions/runs/29397871176) passes on macOS job + [87295382080](https://github.com/stablyai/orca/actions/runs/29397871176/job/87295382080) and Linux job + [87295382148](https://github.com/stablyai/orca/actions/runs/29397871176/job/87295382148). +- Oracle proved: syntax and the complete exact-file aggregate contract execute under repository Node + 24 on every native target family without regressing native runtime construction, smoke, artifact + upload, PR verification, or Golden E2E. The expected overall artifact-workflow failure is solely + the pre-existing Windows arm64 exact-floor gap and occurs after the native arm64 aggregate and + runtime jobs succeed. +- Does not prove: a descriptor produced from real signed returned bytes, Apple/SignPath signing, + native trust, protected manifest signing, detached-signature encoding, publication/read-back, + desktop embedding, SSH transfer/install, or any enabled tuple. +- Checklist items satisfied: all-six exact-head CI gate for the disconnected credential-free + aggregate boundary. Broader Work Package 3 and Milestone 4 boxes remain open. +- Follow-up: add the credential-free post-sign tuple-descriptor producer/native-verification handoff + under a purpose-named RED before wiring any signing credential, publication, or desktop consumer. + +### E-M4-MANIFEST-TUPLE-LOCAL-RED-001 — Post-sign tuple producer is absent + +- Date: 2026-07-15 +- Commit SHA / PR: uncommitted purpose-named test atop + `5e0c776d017b3cf4d57011cb1f4cdc919bbc8782`; draft PR #8741 +- Runner: macOS 26.2 arm64, native local worktree; repository Node/pnpm toolchain +- Remote: not applicable; disconnected credential-free release boundary +- Transport/network: local filesystem only; no network, signer, credential, release, SSH host, + publication, desktop consumer, or production/default call +- Exact command: + + ```sh + /usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs + ``` + +- Result: FAIL as required; one suite failed before collection because + `ssh-relay-runtime-manifest-tuple.mjs` does not exist, with zero tests collected. +- Duration and resource metrics: Vitest 179 ms; 1.27 seconds wall; 132,268,032-byte maximum RSS; + 96,425,312-byte peak memory footprint; zero swaps. +- Artifact/log/trace link: local terminal output; no external artifact. +- Oracle proved: the complete post-sign tree/archive/native-report to aggregate-descriptor contract + cannot pass through an existing permissive or accidental implementation; the producer is + genuinely absent before this package. +- Does not prove: any producer behavior, metadata semantics, real signed bytes, native trust, + publication, desktop embedding, SSH transfer/install, or an enabled tuple. +- Checklist items satisfied: RED prerequisite for the active Work Package 3 post-sign tuple package. +- Follow-up: implement the disconnected credential-free producer, turn the exact six-case suite + green, then run focused aggregate and broad release/static regressions before requesting native CI. + +### E-M4-MANIFEST-TUPLE-SCHEMA-RED-001 — Real Windows closure exposes stale manifest cardinality + +- Date: 2026-07-15 +- Commit SHA / PR: uncommitted implementation atop + `5e0c776d017b3cf4d57011cb1f4cdc919bbc8782`; draft PR #8741 +- Runner: macOS 26.2 arm64, native local worktree; Node v26.0.0 and pnpm 10.24.0 +- Remote: not applicable; disconnected credential-free release boundary +- Transport/network: local filesystem and generated Windows ZIP only; no network, signer, + credential, release, SSH host, publication, desktop consumer, or production/default call +- Exact command: + + ```sh + /usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs + ``` + +- Result: expected evidence-gate failure; 5 tests pass and the complete real-closure case fails with + `SSH relay runtime manifest requires one valid node-pty-native entry`. The fixture contains the + exact proven Windows x64 closure, including both `conpty.node` and + `conpty_console_list.node` plus the two ConPTY runtime files. +- Duration and resource metrics: Vitest 504 ms total with 265 ms test time; 1.24 seconds wall; + 137,576,448-byte maximum RSS; 95,933,816-byte peak memory footprint; zero swaps. +- Root cause: release-side `assertTupleEntries` and desktop-side + `assertRequiredRuntimeEntries` use a platform-agnostic exactly-one rule for `node-pty-native`. + That rule accepts the simplified Windows test conversion but cannot accept the target-native + artifact closure already proven in Work Package 2. Neither validator also enforces exact + tuple-specific `native-runtime` counts. +- Plan correction: the HTML plan now states the exact cardinality contract: Linux has one + node-pty-native and zero native-runtime files; macOS has one and one; Windows has two and two. + Both release and desktop validators must enforce the same role counts, and tests must use the real + Windows closure rather than a path-renamed Linux fixture. +- Does not prove: the correction, producer GREEN, native runner behavior, real signed bytes, native + trust, publication, desktop embedding, SSH transfer/install, or any enabled tuple. +- Checklist items satisfied: evidence-backed design correction prerequisite for the active package. +- Follow-up: add paired release/desktop RED-to-GREEN tests for exact role counts, make the narrow + validator correction, rerun the post-sign producer suite, and preserve all non-production gates. + +### E-M4-MANIFEST-TUPLE-LOCAL-001 — Post-sign descriptor and exact role parity pass locally + +- Date: 2026-07-15 +- Commit SHA / PR: uncommitted package atop + `5e0c776d017b3cf4d57011cb1f4cdc919bbc8782`; draft PR #8741 +- Runner: macOS 26.2 arm64, native local worktree; Node v26.0.0 and pnpm 10.24.0 +- Remote: not applicable; disconnected credential-free release boundary +- Transport/network: local filesystem and generated Windows ZIP only; no network, signer, + credential, release, SSH host, publication, desktop consumer, or production/default call +- Exact command: + + ```sh + /usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs src/main/ssh/ssh-relay-artifact-schema.test.ts + ``` + +- Result: PASS; 4 files / 50 tests: 7 post-sign tuple cases, 6 aggregate cases, 6 workflow-source + cases, and 31 desktop manifest-schema cases. +- Duration and resource metrics: Vitest 989 ms with 377 ms test time; 1.68 seconds wall; + 132,825,088-byte maximum RSS; 96,048,576-byte peak memory footprint; zero swaps. +- Oracle proved: + - the final runtime and aggregate-input roots must be real, physically disjoint directories; + - the complete final tree is content-ID verified before and after bounded archive inspection; + - Windows reports cover exact authenticated Node, both node-pty native modules, and both ConPTY + runtime files with required signer evidence; Linux and macOS reports derive their own exact + policies and three/four-file native closures; + - stale content IDs, source identity drift, missing/duplicate/mismatched native reports, malformed + tool/time data, runtime/archive mutation, extra/existing/linked inputs, oversized metadata, and + cancellation fail before a usable descriptor remains; + - stable bounded archive/SBOM/provenance bytes are hashed, the manifest tuple passes the same + release schema used by aggregation, one exclusive descriptor is written, and the exact closed + four-file set is rehashed before return; + - release and desktop validators accept the exact target-native Windows closure and reject missing + or extra node-pty/native-runtime members with matching tuple-specific rules; + - POSIX and Windows workflow source syntax-check and execute the new suite under the existing + credential-free native jobs. +- Does not prove: Node 24/native-runner execution, semantic regeneration or validation of post-sign + SBOM/provenance content, a report from real Apple/SignPath returned bytes, native trust, protected + signing, publication/read-back, desktop embedding, SSH transfer/install, or any enabled tuple. +- Checklist items satisfied: local functional/schema/workflow gate for the active Work Package 3 + post-sign tuple package. +- Follow-up: run broad release, desktop parity, and repository static gates, then require all-six + exact-head Node 24 CI before closing the package. + +### E-M4-MANIFEST-TUPLE-LOCAL-002 — Broad post-sign tuple regressions and static gates pass + +- Date: 2026-07-15 +- Commit SHA / PR: same uncommitted package atop + `5e0c776d017b3cf4d57011cb1f4cdc919bbc8782`; draft PR #8741 +- Runner: macOS 26.2 arm64, native local worktree; Node v26.0.0 and pnpm 10.24.0 +- Remote: not applicable +- Transport/network: local filesystem only; no signer, credential, release, SSH host, publication, + desktop consumer, or production/default call +- Commands and results: + - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay*.test.mjs` + — PASS, 40 files / 228 tests in 8.25 seconds; 9.15 seconds wall; 188,055,552-byte maximum + RSS; 96,474,536-byte peak memory footprint; zero swaps. + - `/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 528 ms; 1.19 seconds wall; 172,195,840-byte maximum RSS; + 96,245,160-byte peak memory footprint; zero swaps. + - `/usr/bin/time -l pnpm run typecheck` — PASS in 8.46 seconds wall with + 1,181,024,256-byte maximum RSS and zero swaps. + - `/usr/bin/time -l pnpm run lint` — PASS in 18.95 seconds wall with + 2,071,773,184-byte maximum RSS and zero swaps. All 41 reliability gates, the 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` for manifest validation, tuple source/test, and workflow test — PASS. + - Focused `pnpm exec oxfmt --check` for all ten touched code/workflow/plan files — PASS. + - `pnpm run check:max-lines-ratchet` — PASS, 355 grandfathered suppressions and no new bypasses. + - `git diff --check` — PASS with no output. +- Oracle proved: the post-sign producer and evidence-backed schema correction preserve every + release-side SSH-relay contract, desktop signature/asset parity, repository type/static/reliability + gates, localization, formatting, and file line budgets without a bypass. The user-owned + Node/npm resolver files are untouched. +- Does not prove: repository Node 24 or native-runner execution, real post-sign metadata/signatures, + oldest baselines, native trust, publication, desktop embedding, SSH behavior, or an enabled tuple. +- Checklist items satisfied: broad local regression/static gate for the active package. +- Follow-up: commit and push the authorized PR-contained package, then require all six exact-head + Node 24 native jobs before marking this package complete. Keep every production/default gate + unchanged and do not merge to `main`. ## Accepted Gaps @@ -9543,12 +9763,14 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the locally green disconnected credential-free aggregate boundary, then collect -exact-head Node 24 proof from all six native build jobs. Do not connect release publication, use -production signing credentials, or add a desktop consumer in this slice. 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. +Commit and push the authorized, locally green post-sign tuple package, then collect exact-head Node +24 proof from all six native build jobs. Do not check the package complete before that CI exists and +do not merge to `main`. Retain every production/default gate. The next independent artifact-only slice after +CI is semantic regeneration and verification of the post-sign SBOM/provenance bytes consumed here; +do not connect release publication, production signing credentials, 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 diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 793d985b54c..fef6ddd37bc 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1195,6 +1195,15 @@

      Build for the runtime, architecture, and libc together.

      run PTY smoke with the production useConptyDll: true behavior rather than silently exercising system ConPTY. +
    • + Manifest role cardinality must match the exact target-native closure: Linux has + one node-pty native file and no native-runtime files, macOS has one node-pty + native file plus spawn-helper, and Windows has both + conpty.node and conpty_console_list.node plus + conpty.dll and OpenConsole.exe. Release and desktop + validators reject either missing or extra role members with the same + tuple-specific rules. +
    • @parcel/watcher@2.5.6 with exactly one native optional package.
    • diff --git a/src/main/ssh/ssh-relay-artifact-consistency.ts b/src/main/ssh/ssh-relay-artifact-consistency.ts index 36bfd023e4b..dffb51dad50 100644 --- a/src/main/ssh/ssh-relay-artifact-consistency.ts +++ b/src/main/ssh/ssh-relay-artifact-consistency.ts @@ -21,13 +21,7 @@ function assertRequiredRuntimeEntries(tuple: SshRelayRuntimeTuple): void { ['relay', 'relay.js'], ['relay-watcher', 'relay-watcher.js'] ]) - for (const role of [ - 'node', - 'relay', - 'relay-watcher', - 'node-pty-native', - 'parcel-watcher-native' - ]) { + for (const role of ['node', 'relay', 'relay-watcher', 'parcel-watcher-native']) { const matching = files.filter((file) => file.role === role) if (matching.length !== 1) { throw new Error(`Runtime tuple ${tuple.tupleId} requires exactly one ${role} entry`) @@ -37,6 +31,32 @@ function assertRequiredRuntimeEntries(tuple: SshRelayRuntimeTuple): void { throw new Error(`Runtime tuple ${tuple.tupleId} has invalid ${role} path`) } } + const nativePaths = + tuple.os === 'win32' + ? { + 'node-pty-native': [ + 'node_modules/node-pty/build/Release/conpty.node', + 'node_modules/node-pty/build/Release/conpty_console_list.node' + ], + 'native-runtime': [ + 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe', + 'node_modules/node-pty/build/Release/conpty/conpty.dll' + ] + } + : { + 'node-pty-native': ['node_modules/node-pty/build/Release/pty.node'], + 'native-runtime': + tuple.os === 'darwin' ? ['node_modules/node-pty/build/Release/spawn-helper'] : [] + } + for (const [role, expected] of Object.entries(nativePaths)) { + const actual = files + .filter((entry) => entry.role === role) + .map((entry) => entry.path) + .sort() + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + throw new Error(`Runtime tuple ${tuple.tupleId} has invalid ${role} closure`) + } + } for (const requiredPath of [ 'node_modules/node-pty/package.json', 'node_modules/@parcel/watcher/package.json' diff --git a/src/main/ssh/ssh-relay-artifact-schema.test.ts b/src/main/ssh/ssh-relay-artifact-schema.test.ts index d52ad42cafe..ef004d3655c 100644 --- a/src/main/ssh/ssh-relay-artifact-schema.test.ts +++ b/src/main/ssh/ssh-relay-artifact-schema.test.ts @@ -5,6 +5,15 @@ import { parseSshRelayArtifactManifest } from './ssh-relay-artifact-schema' import { sshRelayRuntimeArchiveName } from './ssh-relay-release-asset' import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity' +function finalizeTuple(manifest: ReturnType): void { + const tuple = manifest.tuples[0] + const files = tuple.entries.filter((entry) => entry.type === 'file') + tuple.contentId = computeSshRelayRuntimeContentId(tuple) + tuple.archive.name = sshRelayRuntimeArchiveName(tuple.tupleId, tuple.contentId) + tuple.archive.fileCount = files.length + tuple.archive.expandedSize = files.reduce((total, entry) => total + entry.size, 0) +} + function createWindowsManifest() { const manifest = createSshRelayArtifactTestManifest() const tuple = manifest.tuples[0] @@ -22,15 +31,67 @@ function createWindowsManifest() { entry.path = entry.path .replace('bin/node', 'bin/node.exe') .replace('watcher-linux-x64-glibc', 'watcher-win32-x64') + .replace( + 'node_modules/node-pty/build/Release/pty.node', + 'node_modules/node-pty/build/Release/conpty.node' + ) } + tuple.entries.push( + { + path: 'node_modules/node-pty/build/Release/conpty', + type: 'directory', + mode: 0o755 + }, + { + path: 'node_modules/node-pty/build/Release/conpty_console_list.node', + type: 'file', + role: 'node-pty-native', + size: 31, + mode: 0o755, + sha256: `sha256:${'9'.repeat(64)}` + }, + { + path: 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe', + type: 'file', + role: 'native-runtime', + size: 32, + mode: 0o755, + sha256: `sha256:${'a'.repeat(64)}` + }, + { + path: 'node_modules/node-pty/build/Release/conpty/conpty.dll', + type: 'file', + role: 'native-runtime', + size: 33, + mode: 0o755, + sha256: `sha256:${'b'.repeat(64)}` + } + ) tuple.nativeVerification.policy = 'signpath-authenticode-v1' for (const file of tuple.nativeVerification.files) { file.path = file.path .replace('bin/node', 'bin/node.exe') .replace('watcher-linux-x64-glibc', 'watcher-win32-x64') + .replace( + 'node_modules/node-pty/build/Release/pty.node', + 'node_modules/node-pty/build/Release/conpty.node' + ) } - tuple.contentId = computeSshRelayRuntimeContentId(tuple) - tuple.archive.name = sshRelayRuntimeArchiveName(tuple.tupleId, tuple.contentId) + tuple.nativeVerification.files.push( + { + path: 'node_modules/node-pty/build/Release/conpty_console_list.node', + sha256: `sha256:${'9'.repeat(64)}` + }, + { + path: 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe', + sha256: `sha256:${'a'.repeat(64)}` + }, + { + path: 'node_modules/node-pty/build/Release/conpty/conpty.dll', + sha256: `sha256:${'b'.repeat(64)}` + } + ) + finalizeTuple(manifest) return manifest } @@ -42,13 +103,46 @@ describe('SSH relay artifact manifest schema', () => { expect(parsed.tuples[0].tupleId).toBe('linux-x64-glibc') }) - it('accepts the target-native Windows Node path and compatibility discriminator', () => { + it('accepts the exact target-native Windows native closure and compatibility discriminator', () => { const parsed = parseSshRelayArtifactManifest(createWindowsManifest()) + const files = parsed.tuples[0].entries.filter((entry) => entry.type === 'file') expect(parsed.tuples[0].compatibility.kind).toBe('windows') expect(parsed.tuples[0].entries).toContainEqual( expect.objectContaining({ path: 'bin/node.exe', role: 'node' }) ) + expect(files.filter((entry) => entry.role === 'node-pty-native')).toHaveLength(2) + expect(files.filter((entry) => entry.role === 'native-runtime')).toHaveLength(2) + expect(parsed.tuples[0].nativeVerification.files).toHaveLength(6) + }) + + it('rejects missing or extra tuple-specific Windows native role members', () => { + const missing = createWindowsManifest() + const missingPath = 'node_modules/node-pty/build/Release/conpty_console_list.node' + missing.tuples[0].entries = missing.tuples[0].entries.filter( + (entry) => entry.path !== missingPath + ) + missing.tuples[0].nativeVerification.files = missing.tuples[0].nativeVerification.files.filter( + (entry) => entry.path !== missingPath + ) + finalizeTuple(missing) + expect(() => parseSshRelayArtifactManifest(missing)).toThrow(/node-pty-native.*closure/i) + + const extra = createWindowsManifest() + extra.tuples[0].entries.push({ + path: 'node_modules/node-pty/build/Release/conpty/unexpected.dll', + type: 'file', + role: 'native-runtime', + size: 34, + mode: 0o755, + sha256: `sha256:${'c'.repeat(64)}` + }) + extra.tuples[0].nativeVerification.files.push({ + path: 'node_modules/node-pty/build/Release/conpty/unexpected.dll', + sha256: `sha256:${'c'.repeat(64)}` + }) + finalizeTuple(extra) + expect(() => parseSshRelayArtifactManifest(extra)).toThrow(/native-runtime.*closure/i) }) it('rejects unsupported schema versions, extra fields, and non-canonical timestamps', () => { From 1989e801af4a922a646d62ffd13e235615462975 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 06:29:58 -0400 Subject: [PATCH 115/267] build(ssh): verify post-sign runtime metadata Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 8 + .../ssh-relay-runtime-manifest-tuple.mjs | 8 + .../ssh-relay-runtime-manifest-tuple.test.mjs | 73 +++- .../ssh-relay-runtime-post-sign-metadata.mjs | 362 ++++++++++++++++++ ...-relay-runtime-post-sign-metadata.test.mjs | 283 ++++++++++++++ .../scripts/ssh-relay-runtime-provenance.mjs | 37 +- .../ssh-relay-runtime-provenance.test.mjs | 1 + .../ssh-relay-runtime-workflow.test.mjs | 3 +- ...elease-implementation-checklist-summary.md | 28 +- ...github-release-implementation-checklist.md | 196 +++++++++- 10 files changed, 966 insertions(+), 33 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-post-sign-metadata.mjs create mode 100644 config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index b725137291d..6baac3d15ed 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -162,6 +162,8 @@ jobs: node --check config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs node --check config/scripts/ssh-relay-runtime-manifest-tuple.mjs node --check config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs + node --check config/scripts/ssh-relay-runtime-post-sign-metadata.mjs + node --check config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs pnpm exec vitest run --config config/vitest.config.ts \ config/scripts/ssh-relay-runtime-compatibility.test.mjs \ config/scripts/ssh-relay-node-release-verification.test.mjs \ @@ -192,6 +194,7 @@ jobs: config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs \ config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs \ config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs \ + config/scripts/ssh-relay-runtime-post-sign-metadata.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 \ @@ -545,6 +548,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime manifest tuple syntax check failed' } node --check config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime manifest tuple test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-post-sign-metadata.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime post-sign metadata syntax check failed' } + node --check config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime post-sign metadata test syntax check failed' } pnpm exec vitest run --config config/vitest.config.ts ` config/scripts/ssh-relay-runtime-compatibility.test.mjs ` config/scripts/ssh-relay-node-release-verification.test.mjs ` @@ -575,6 +582,7 @@ jobs: config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs ` config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs ` config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs ` + config/scripts/ssh-relay-runtime-post-sign-metadata.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-manifest-tuple.mjs b/config/scripts/ssh-relay-runtime-manifest-tuple.mjs index da01e878f44..8755986c57e 100644 --- a/config/scripts/ssh-relay-runtime-manifest-tuple.mjs +++ b/config/scripts/ssh-relay-runtime-manifest-tuple.mjs @@ -9,6 +9,7 @@ import { assertSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' import { parseSshRelayRuntimeManifestTuple } from './ssh-relay-runtime-manifest-validation.mjs' import { buildSshRelayRuntimeNativeSigningPlan } from './ssh-relay-runtime-native-signing-plan.mjs' +import { verifySshRelayRuntimePostSignMetadata } from './ssh-relay-runtime-post-sign-metadata.mjs' import { verifyRuntimeTree } from './verify-ssh-relay-runtime.mjs' const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u @@ -310,6 +311,13 @@ export async function writeSshRelayRuntimeManifestTupleDescriptor({ await inspectSshRelayRuntimeArchive(join(physicalInput, names.archive), finalIdentity, { signal: effectiveSignal }) + await verifySshRelayRuntimePostSignMetadata({ + finalIdentity, + archive: { ...archive, path: join(physicalInput, names.archive) }, + sbomPath: join(physicalInput, names.sbom), + provenancePath: join(physicalInput, names.provenance), + signal: effectiveSignal + }) await verifyRuntimeTree(physicalRuntime, finalIdentity) const tuple = parseSshRelayRuntimeManifestTuple( tupleProjection(finalIdentity, archive, sbom, provenance, verification) diff --git a/config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs b/config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs index 430ac0be8e7..043230fc8b4 100644 --- a/config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs +++ b/config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs @@ -24,6 +24,8 @@ import { writeSshRelayRuntimeManifestTupleDescriptor } from './ssh-relay-runtime-manifest-tuple.mjs' import { buildSshRelayRuntimeNativeSigningPlan } from './ssh-relay-runtime-native-signing-plan.mjs' +import { createSshRelayRuntimeProvenance } from './ssh-relay-runtime-provenance.mjs' +import { createSshRelayRuntimeSbom } from './ssh-relay-runtime-sbom.mjs' const temporaryDirectories = [] const SOURCE_DATE_EPOCH = 1_788_739_200 @@ -77,9 +79,60 @@ async function runtimeFixture() { }) const sbomName = 'orca-ssh-relay-runtime-win32-x64.spdx.json' const provenanceName = 'orca-ssh-relay-runtime-win32-x64.provenance.json' + const sbom = createSshRelayRuntimeSbom({ + identity: finalIdentity, + archive, + sourceDateEpoch: SOURCE_DATE_EPOCH + }) + const toolchain = Object.fromEntries( + [ + 'buildNode', + 'bundledNode', + 'compiler', + 'buildSystem', + 'python', + 'archive', + 'linker', + 'nodeAddonApi', + 'nodeGyp' + ].map((name, index) => [ + name, + { version: `${name} version`, sha256: `sha256:${(index + 1).toString(16).repeat(64)}` } + ]) + ) + const provenance = createSshRelayRuntimeProvenance({ + identity: finalIdentity, + archive, + nodeRelease: { + baseUrl: 'https://nodejs.org/dist/v24.18.0', + archives: { + 'win32-x64': { name: 'node-v24.18.0-win-x64.zip', sha256: 'a'.repeat(64) } + }, + signature: { + key: { sourceUrl: 'https://example.invalid/key.asc', sha256: 'b'.repeat(64) } + }, + windowsBuildInputs: { + headersArchive: { name: 'node-v24.18.0-headers.tar.gz', sha256: 'c'.repeat(64) }, + importLibraries: { + 'win32-x64': { name: 'win-x64/node.lib', sha256: 'd'.repeat(64) } + } + } + }, + sourceDateEpoch: SOURCE_DATE_EPOCH, + gitCommit: '1'.repeat(40), + builder: 'local://win32/x64', + runner: { + os: 'Windows', + architecture: 'X64', + environment: 'local', + requestedLabel: 'local', + image: { os: 'win32', version: 'local' } + }, + toolchain + }) await Promise.all([ - writeFile(join(inputDirectory, sbomName), '{"spdxVersion":"SPDX-2.3"}\n'), - writeFile(join(inputDirectory, provenanceName), '{"_type":"https://in-toto.io/Statement/v1"}\n') + writeFile(join(inputDirectory, sbomName), `${JSON.stringify(sbom)}\n`), + writeFile(join(inputDirectory, provenanceName), `${JSON.stringify(provenance)}\n`) ]) const plan = buildSshRelayRuntimeNativeSigningPlan(finalIdentity) return { @@ -281,6 +334,22 @@ describe('SSH relay runtime post-sign manifest tuple', () => { ).rejects.toMatchObject({ code: 'ENOENT' }) }) + it('rejects semantically stale post-sign metadata before writing a descriptor', async () => { + const fixture = await runtimeFixture() + const sbomPath = join(fixture.inputDirectory, 'orca-ssh-relay-runtime-win32-x64.spdx.json') + const sbom = JSON.parse(await readFile(sbomPath, 'utf8')) + sbom.packages.find((entry) => entry.name === 'orca-ssh-relay').versionInfo = + `sha256:${'e'.repeat(64)}` + await writeFile(sbomPath, `${JSON.stringify(sbom)}\n`) + + await expect( + writeSshRelayRuntimeManifestTupleDescriptor(producerInput(fixture)) + ).rejects.toThrow(/SBOM.*content|content.*SBOM/i) + await expect( + readFile(join(fixture.inputDirectory, fixture.descriptorName)) + ).rejects.toMatchObject({ code: 'ENOENT' }) + }) + it('requires an exclusive exact input set and bounded regular metadata', async () => { const extra = await runtimeFixture() await writeFile(join(extra.inputDirectory, 'unexpected.bin'), 'extra') diff --git a/config/scripts/ssh-relay-runtime-post-sign-metadata.mjs b/config/scripts/ssh-relay-runtime-post-sign-metadata.mjs new file mode 100644 index 00000000000..c06f4e11c3f --- /dev/null +++ b/config/scripts/ssh-relay-runtime-post-sign-metadata.mjs @@ -0,0 +1,362 @@ +import { createHash } from 'node:crypto' +import { lstat, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises' +import { basename, join, relative, resolve, sep } from 'node:path' +import { isDeepStrictEqual } from 'node:util' + +import { inspectSshRelayRuntimeArchive } from './ssh-relay-runtime-archive.mjs' +import { assertSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' +import { createSshRelayRuntimeProvenance } from './ssh-relay-runtime-provenance.mjs' +import { createSshRelayRuntimeSbom } from './ssh-relay-runtime-sbom.mjs' +import { assertSshRelayRuntimeToolchain } from './ssh-relay-runtime-toolchain.mjs' +import { verifyRuntimeTree } from './verify-ssh-relay-runtime.mjs' + +const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u +const MAX_METADATA_BYTES = 32 * 1024 * 1024 +const METADATA_TIMEOUT_MS = 15 * 60_000 +const NATIVE_ROLES = new Set(['node', 'node-pty-native', 'parcel-watcher-native', 'native-runtime']) + +function assertObject(value, label) { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`Runtime post-sign metadata ${label} must be an object`) + } +} + +function containsPath(parent, candidate) { + const path = relative(parent, candidate) + return path === '' || (path !== '..' && !path.startsWith(`..${sep}`)) +} + +async function physicalDirectory(path, label) { + const metadata = await lstat(path) + if (!metadata.isDirectory() || metadata.isSymbolicLink()) { + throw new Error(`Runtime post-sign metadata ${label} must be a real directory`) + } + return realpath(path) +} + +function expectedNames(identity) { + const prefix = `orca-ssh-relay-runtime-${identity.tupleId}` + const extension = identity.os === 'win32' ? 'zip' : 'tar.xz' + return { + archive: `orca-ssh-relay-runtime-v1-${identity.tupleId}-${identity.contentId.slice('sha256:'.length)}.${extension}`, + sbom: `${prefix}.spdx.json`, + provenance: `${prefix}.provenance.json` + } +} + +function assertFinalIdentity(identity) { + assertObject(identity, 'final identity') + assertSshRelayRuntimeClosureEntries(identity) + const files = identity.entries.filter((entry) => entry.type === 'file') + if ( + Object.hasOwn(identity, 'archive') || + !DIGEST_PATTERN.test(identity.contentId ?? '') || + computeSshRelayRuntimeContentId(identity) !== identity.contentId || + identity.fileCount !== files.length || + identity.expandedSize !== files.reduce((total, entry) => total + entry.size, 0) + ) { + throw new Error('Runtime post-sign metadata final content identity is inconsistent') + } +} + +function sameFileState(before, after) { + return ( + before.dev === after.dev && + before.ino === after.ino && + before.size === after.size && + before.mtimeNs === after.mtimeNs && + before.ctimeNs === after.ctimeNs + ) +} + +async function readStableFile(path, maximumBytes, label, signal) { + signal.throwIfAborted() + const before = await lstat(path, { bigint: true }) + if (!before.isFile() || before.isSymbolicLink()) { + throw new Error(`Runtime post-sign metadata ${label} must be a regular file`) + } + if (before.size <= 0n || before.size > BigInt(maximumBytes)) { + throw new Error(`Runtime post-sign metadata ${label} exceeds its bounded size`) + } + const bytes = await readFile(path, { signal }) + const after = await lstat(path, { bigint: true }) + if (!sameFileState(before, after) || BigInt(bytes.length) !== before.size) { + throw new Error(`Runtime post-sign metadata ${label} changed while reading`) + } + return bytes +} + +async function readStableJson(path, label, signal) { + const bytes = await readStableFile(path, MAX_METADATA_BYTES, label, signal) + try { + return JSON.parse(bytes.toString('utf8')) + } catch (error) { + throw new Error(`Runtime post-sign metadata ${label} is not valid JSON: ${error.message}`) + } +} + +function assertArchiveReference(archive, identity, names) { + assertObject(archive, 'archive reference') + if ( + archive.name !== names.archive || + basename(archive.path ?? '') !== names.archive || + !Number.isSafeInteger(archive.size) || + archive.size <= 0 || + !DIGEST_PATTERN.test(archive.sha256 ?? '') + ) { + throw new Error('Runtime post-sign metadata archive reference is inconsistent') + } +} + +async function verifyArchiveReference(outputRoot, archive, identity, names, signal) { + assertArchiveReference(archive, identity, names) + const archivePath = join(outputRoot, names.archive) + if ((await realpath(resolve(archive.path))) !== (await realpath(archivePath))) { + throw new Error('Runtime post-sign metadata archive must be inside the exclusive output root') + } + const bytes = await readStableFile(archivePath, 100 * 1024 * 1024, 'archive', signal) + const digest = `sha256:${createHash('sha256').update(bytes).digest('hex')}` + if (bytes.length !== archive.size || digest !== archive.sha256) { + throw new Error('Runtime post-sign metadata archive size or digest mismatch') + } + await inspectSshRelayRuntimeArchive(archivePath, identity, { signal }) + return { name: archive.name, size: archive.size, sha256: archive.sha256 } +} + +function expectedNativeFiles(identity) { + return identity.entries + .filter((entry) => entry.type === 'file' && NATIVE_ROLES.has(entry.role)) + .map((entry) => ({ path: entry.path, sha256: entry.sha256 })) +} + +function validResolvedDependencies(dependencies, identity) { + const expectedCount = identity.os === 'win32' ? 5 : 3 + return ( + Array.isArray(dependencies) && + dependencies.length === expectedCount && + dependencies.every((entry) => { + const digests = Object.entries(entry?.digest ?? {}) + return ( + typeof entry?.uri === 'string' && + entry.uri.length > 0 && + digests.length === 1 && + ((digests[0][0] === 'sha256' && /^[0-9a-f]{64}$/u.test(digests[0][1])) || + (digests[0][0] === 'gitCommit' && /^[0-9a-f]{40}$/u.test(digests[0][1]))) + ) + }) + ) +} + +function validRunner(runner) { + return ( + runner !== null && + typeof runner === 'object' && + !Array.isArray(runner) && + ['os', 'architecture', 'environment', 'requestedLabel'].every( + (field) => typeof runner[field] === 'string' && runner[field].length > 0 + ) && + typeof runner.image?.os === 'string' && + runner.image.os.length > 0 && + typeof runner.image?.version === 'string' && + runner.image.version.length > 0 + ) +} + +function assertSbomBinding(sbom, identity, archive) { + assertObject(sbom, 'SBOM') + const created = sbom.creationInfo?.created + const createdMilliseconds = typeof created === 'string' ? Date.parse(created) : Number.NaN + if (!Number.isSafeInteger(createdMilliseconds) || createdMilliseconds % 1000 !== 0) { + throw new Error('Runtime post-sign metadata SBOM creation timestamp is invalid') + } + const expected = createSshRelayRuntimeSbom({ + identity, + archive, + sourceDateEpoch: createdMilliseconds / 1000 + }) + if (!isDeepStrictEqual(sbom, expected)) { + const relay = sbom.packages?.filter((entry) => entry.name === 'orca-ssh-relay') ?? [] + if (relay.length !== 1 || relay[0].versionInfo !== identity.contentId) { + throw new Error('Runtime post-sign metadata SBOM content identity is stale') + } + throw new Error('Runtime post-sign metadata SBOM file or archive binding is stale') + } +} + +function assertProvenanceBinding(provenance, identity, archive) { + assertObject(provenance, 'provenance') + const expectedSubject = [ + { name: archive.name, digest: { sha256: archive.sha256.slice('sha256:'.length) } } + ] + if (!isDeepStrictEqual(provenance.subject, expectedSubject)) { + throw new Error('Runtime post-sign metadata provenance archive binding is stale') + } + const definition = provenance.predicate?.buildDefinition + const parameters = definition?.externalParameters + const runDetails = provenance.predicate?.runDetails + if ( + provenance._type !== 'https://in-toto.io/Statement/v1' || + provenance.predicateType !== 'https://slsa.dev/provenance/v1' || + definition?.buildType !== 'https://github.com/stablyai/orca/ssh-relay-runtime-build/v1' || + parameters?.tuple !== identity.tupleId || + parameters?.nodeVersion !== identity.nodeVersion || + parameters?.contentId !== identity.contentId || + !Number.isSafeInteger(parameters?.sourceDateEpoch) || + !isDeepStrictEqual(Object.keys(parameters).sort(), [ + 'contentId', + 'nodeVersion', + 'sourceDateEpoch', + 'tuple' + ]) || + !validResolvedDependencies(definition.resolvedDependencies, identity) || + typeof runDetails?.builder?.id !== 'string' || + runDetails.builder.id.length === 0 || + typeof runDetails?.metadata?.invocationId !== 'string' || + runDetails.metadata.invocationId.length === 0 || + !validRunner(runDetails.metadata.runner) + ) { + throw new Error('Runtime post-sign metadata provenance content identity is stale') + } + assertSshRelayRuntimeToolchain(definition.internalParameters?.toolchain, identity.tupleId) + const expectedByproducts = [{ name: 'native-files', content: expectedNativeFiles(identity) }] + if (!isDeepStrictEqual(runDetails.byproducts, expectedByproducts)) { + throw new Error('Runtime post-sign metadata provenance native-file binding is stale') + } +} + +export async function verifySshRelayRuntimePostSignMetadata({ + finalIdentity, + archive, + sbomPath, + provenancePath, + signal +}) { + const effectiveSignal = signal + ? AbortSignal.any([signal, AbortSignal.timeout(METADATA_TIMEOUT_MS)]) + : AbortSignal.timeout(METADATA_TIMEOUT_MS) + effectiveSignal.throwIfAborted() + assertFinalIdentity(finalIdentity) + const names = expectedNames(finalIdentity) + assertArchiveReference(archive, finalIdentity, names) + const [sbom, provenance] = await Promise.all([ + readStableJson(resolve(sbomPath), 'SBOM', effectiveSignal), + readStableJson(resolve(provenancePath), 'provenance', effectiveSignal) + ]) + assertSbomBinding(sbom, finalIdentity, archive) + assertProvenanceBinding(provenance, finalIdentity, archive) + return { sbom, provenance } +} + +function jsonAsset(name, value) { + const bytes = Buffer.from(`${JSON.stringify(value, null, 2)}\n`, 'utf8') + if (bytes.length === 0 || bytes.length > MAX_METADATA_BYTES) { + throw new Error(`Runtime post-sign metadata ${name} exceeds its bounded size`) + } + return { + bytes, + reference: { + name, + size: bytes.length, + sha256: `sha256:${createHash('sha256').update(bytes).digest('hex')}` + } + } +} + +async function assertExclusiveArchive(root, archiveName) { + const entries = await readdir(root, { withFileTypes: true }) + if (entries.length !== 1 || entries[0].name !== archiveName || !entries[0].isFile()) { + throw new Error('Runtime post-sign metadata requires an exclusive exact archive input') + } +} + +export async function writeSshRelayRuntimePostSignMetadata({ + runtimeRoot, + outputDirectory, + finalIdentity, + archive, + nodeRelease, + sourceDateEpoch, + gitCommit, + builder, + runner, + toolchain, + signal +}) { + const effectiveSignal = signal + ? AbortSignal.any([signal, AbortSignal.timeout(METADATA_TIMEOUT_MS)]) + : AbortSignal.timeout(METADATA_TIMEOUT_MS) + effectiveSignal.throwIfAborted() + assertFinalIdentity(finalIdentity) + const names = expectedNames(finalIdentity) + const [physicalRuntime, physicalOutput] = await Promise.all([ + physicalDirectory(resolve(runtimeRoot), 'runtime root'), + physicalDirectory(resolve(outputDirectory), 'output root') + ]) + if ( + containsPath(physicalRuntime, physicalOutput) || + containsPath(physicalOutput, physicalRuntime) + ) { + throw new Error( + 'Runtime post-sign metadata runtime and output roots must be physically disjoint' + ) + } + await assertExclusiveArchive(physicalOutput, names.archive) + await verifyRuntimeTree(physicalRuntime, finalIdentity) + const archiveReference = await verifyArchiveReference( + physicalOutput, + archive, + finalIdentity, + names, + effectiveSignal + ) + const sbom = createSshRelayRuntimeSbom({ identity: finalIdentity, archive, sourceDateEpoch }) + const provenance = createSshRelayRuntimeProvenance({ + identity: finalIdentity, + archive, + nodeRelease, + sourceDateEpoch, + gitCommit, + builder, + runner, + toolchain + }) + const sbomAsset = jsonAsset(names.sbom, sbom) + const provenanceAsset = jsonAsset(names.provenance, provenance) + const written = [] + try { + for (const asset of [sbomAsset, provenanceAsset]) { + const path = join(physicalOutput, asset.reference.name) + written.push(path) + await writeFile(path, asset.bytes, { flag: 'wx', mode: 0o600, signal: effectiveSignal }) + } + const verified = await verifySshRelayRuntimePostSignMetadata({ + finalIdentity, + archive, + sbomPath: join(physicalOutput, names.sbom), + provenancePath: join(physicalOutput, names.provenance), + signal: effectiveSignal + }) + if (!isDeepStrictEqual(verified, { sbom, provenance })) { + throw new Error('Runtime post-sign metadata changed after exclusive publication') + } + // Why: metadata is usable only if the final tree and archive remain unchanged through emission. + await verifyRuntimeTree(physicalRuntime, finalIdentity) + await verifyArchiveReference(physicalOutput, archive, finalIdentity, names, effectiveSignal) + return { + tupleId: finalIdentity.tupleId, + contentId: finalIdentity.contentId, + archive: archiveReference, + sbom: sbomAsset.reference, + provenance: provenanceAsset.reference + } + } catch (error) { + await Promise.all(written.map((path) => rm(path, { force: true }))) + throw error + } +} + +export const SSH_RELAY_RUNTIME_POST_SIGN_METADATA_LIMITS = Object.freeze({ + maximumMetadataBytes: MAX_METADATA_BYTES, + timeoutMs: METADATA_TIMEOUT_MS +}) diff --git a/config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs b/config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs new file mode 100644 index 00000000000..596418d154a --- /dev/null +++ b/config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs @@ -0,0 +1,283 @@ +import { createHash } from 'node:crypto' +import { appendFile, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { createSshRelayRuntimeArchive } from './ssh-relay-runtime-archive.mjs' +import { expectedSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' +import { + verifySshRelayRuntimePostSignMetadata, + writeSshRelayRuntimePostSignMetadata +} from './ssh-relay-runtime-post-sign-metadata.mjs' + +const temporaryDirectories = [] +const SOURCE_DATE_EPOCH = 1_788_739_200 +const GIT_COMMIT = '1'.repeat(40) + +function sha256(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +function toolchain() { + return Object.fromEntries( + [ + 'buildNode', + 'bundledNode', + 'compiler', + 'buildSystem', + 'python', + 'archive', + 'nodeAddonApi', + 'nodeGyp', + 'strip' + ].map((name, index) => [ + name, + { version: `${name} version`, sha256: `sha256:${(index + 6).toString(16).repeat(64)}` } + ]) + ) +} + +async function runtimeFixture() { + const root = await mkdtemp(join(tmpdir(), 'ssh-relay-post-sign-metadata-')) + temporaryDirectories.push(root) + const runtimeRoot = join(root, 'runtime') + const outputDirectory = join(root, 'output') + await Promise.all([mkdir(runtimeRoot), mkdir(outputDirectory)]) + const tupleId = 'linux-x64-glibc' + const entries = [] + for (const entry of expectedSshRelayRuntimeClosureEntries(tupleId)) { + const path = join(runtimeRoot, ...entry.path.split('/')) + if (entry.type === 'directory') { + await mkdir(path, { recursive: true, mode: entry.mode }) + entries.push(entry) + continue + } + const bytes = Buffer.from(`final signed fixture:${entry.path}`) + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, bytes, { mode: entry.mode }) + entries.push({ ...entry, size: bytes.length, sha256: sha256(bytes) }) + } + const base = { + identitySchemaVersion: 1, + tupleId, + os: 'linux', + architecture: 'x64', + compatibility: sshRelayRuntimeCompatibility[tupleId], + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries + } + const files = entries.filter((entry) => entry.type === 'file') + const finalIdentity = { + ...base, + contentId: computeSshRelayRuntimeContentId(base), + fileCount: files.length, + expandedSize: files.reduce((total, entry) => total + entry.size, 0) + } + const archive = await createSshRelayRuntimeArchive({ + runtimeRoot, + outputDirectory, + identity: finalIdentity, + sourceDateEpoch: SOURCE_DATE_EPOCH + }) + return { + root, + runtimeRoot, + outputDirectory, + finalIdentity, + archive, + nodeRelease: { + baseUrl: 'https://nodejs.org/dist/v24.18.0', + archives: { + [tupleId]: { + name: 'node-v24.18.0-linux-x64.tar.xz', + sha256: 'b'.repeat(64) + } + }, + signature: { + key: { + sourceUrl: 'https://github.com/nodejs/release-keys/raw/commit/gpg/key.asc', + sha256: 'c'.repeat(64) + } + } + }, + builder: `https://github.com/stablyai/orca/blob/${GIT_COMMIT}/.github/workflows/ssh-relay-runtime-artifacts.yml`, + runner: { + os: 'Linux', + architecture: 'X64', + environment: 'github-hosted', + requestedLabel: 'ubuntu-24.04', + image: { os: 'ubuntu24', version: '20260705.232.1' } + }, + toolchain: toolchain() + } +} + +function writerInput(fixture) { + return { + runtimeRoot: fixture.runtimeRoot, + outputDirectory: fixture.outputDirectory, + finalIdentity: fixture.finalIdentity, + archive: fixture.archive, + nodeRelease: fixture.nodeRelease, + sourceDateEpoch: SOURCE_DATE_EPOCH, + gitCommit: GIT_COMMIT, + builder: fixture.builder, + runner: fixture.runner, + toolchain: fixture.toolchain + } +} + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })) + ) +}) + +describe('SSH relay runtime post-sign metadata', () => { + it('regenerates exact SBOM and provenance from the verified final tree', async () => { + const fixture = await runtimeFixture() + + const result = await writeSshRelayRuntimePostSignMetadata(writerInput(fixture)) + const sbom = JSON.parse(await readFile(join(fixture.outputDirectory, result.sbom.name), 'utf8')) + const provenance = JSON.parse( + await readFile(join(fixture.outputDirectory, result.provenance.name), 'utf8') + ) + + expect((await readdir(fixture.outputDirectory)).toSorted()).toEqual( + [fixture.archive.name, result.sbom.name, result.provenance.name].toSorted() + ) + expect(result).toMatchObject({ + tupleId: fixture.finalIdentity.tupleId, + contentId: fixture.finalIdentity.contentId, + archive: { + name: fixture.archive.name, + size: fixture.archive.size, + sha256: fixture.archive.sha256 + } + }) + expect(sbom.documentNamespace).toBe( + `https://github.com/stablyai/orca/ssh-relay-runtime/spdx/${fixture.archive.sha256.slice('sha256:'.length)}` + ) + expect(sbom.packages.find((entry) => entry.name === 'orca-ssh-relay').versionInfo).toBe( + fixture.finalIdentity.contentId + ) + expect(sbom.files).toHaveLength(fixture.finalIdentity.fileCount) + expect(provenance.subject).toEqual([ + { + name: fixture.archive.name, + digest: { sha256: fixture.archive.sha256.slice('sha256:'.length) } + } + ]) + expect(provenance.predicate.buildDefinition.externalParameters).toMatchObject({ + tuple: fixture.finalIdentity.tupleId, + nodeVersion: fixture.finalIdentity.nodeVersion, + contentId: fixture.finalIdentity.contentId, + sourceDateEpoch: SOURCE_DATE_EPOCH + }) + expect(provenance.predicate.runDetails.byproducts).toContainEqual( + expect.objectContaining({ name: 'native-files', content: expect.any(Array) }) + ) + }) + + it('rejects tree or archive mutation and removes partial metadata', async () => { + const tree = await runtimeFixture() + await appendFile(join(tree.runtimeRoot, 'relay.js'), ':mutated') + await expect(writeSshRelayRuntimePostSignMetadata(writerInput(tree))).rejects.toThrow( + /tree.*integrity|integrity.*tree/i + ) + expect(await readdir(tree.outputDirectory)).toEqual([tree.archive.name]) + + const archive = await runtimeFixture() + await writeFile(archive.archive.path, 'mutated archive') + await expect(writeSshRelayRuntimePostSignMetadata(writerInput(archive))).rejects.toThrow( + /archive|digest|size/i + ) + expect(await readdir(archive.outputDirectory)).toEqual([archive.archive.name]) + }) + + it('makes tuple consumers reject stale SBOM and provenance bindings', async () => { + const sbomFixture = await runtimeFixture() + const sbomAssets = await writeSshRelayRuntimePostSignMetadata(writerInput(sbomFixture)) + const sbomPath = join(sbomFixture.outputDirectory, sbomAssets.sbom.name) + const sbom = JSON.parse(await readFile(sbomPath, 'utf8')) + sbom.packages.find((entry) => entry.name === 'orca-ssh-relay').versionInfo = + `sha256:${'e'.repeat(64)}` + await writeFile(sbomPath, `${JSON.stringify(sbom)}\n`) + await expect( + verifySshRelayRuntimePostSignMetadata({ + finalIdentity: sbomFixture.finalIdentity, + archive: sbomFixture.archive, + sbomPath, + provenancePath: join(sbomFixture.outputDirectory, sbomAssets.provenance.name) + }) + ).rejects.toThrow(/SBOM.*content|content.*SBOM/i) + + const provenanceFixture = await runtimeFixture() + const provenanceAssets = await writeSshRelayRuntimePostSignMetadata( + writerInput(provenanceFixture) + ) + const provenancePath = join(provenanceFixture.outputDirectory, provenanceAssets.provenance.name) + const provenance = JSON.parse(await readFile(provenancePath, 'utf8')) + provenance.subject[0].digest.sha256 = 'd'.repeat(64) + await writeFile(provenancePath, `${JSON.stringify(provenance)}\n`) + await expect( + verifySshRelayRuntimePostSignMetadata({ + finalIdentity: provenanceFixture.finalIdentity, + archive: provenanceFixture.archive, + sbomPath: join(provenanceFixture.outputDirectory, provenanceAssets.sbom.name), + provenancePath + }) + ).rejects.toThrow(/provenance.*archive|archive.*provenance/i) + + provenance.subject[0].digest.sha256 = provenanceFixture.archive.sha256.slice('sha256:'.length) + provenance.predicate.buildDefinition.externalParameters.contentId = `sha256:${'c'.repeat(64)}` + await writeFile(provenancePath, `${JSON.stringify(provenance)}\n`) + await expect( + verifySshRelayRuntimePostSignMetadata({ + finalIdentity: provenanceFixture.finalIdentity, + archive: provenanceFixture.archive, + sbomPath: join(provenanceFixture.outputDirectory, provenanceAssets.sbom.name), + provenancePath + }) + ).rejects.toThrow(/provenance.*content|content.*provenance/i) + + provenance.predicate.buildDefinition.externalParameters.contentId = + provenanceFixture.finalIdentity.contentId + provenance.predicate.runDetails.byproducts[0].content[0].sha256 = `sha256:${'b'.repeat(64)}` + await writeFile(provenancePath, `${JSON.stringify(provenance)}\n`) + await expect( + verifySshRelayRuntimePostSignMetadata({ + finalIdentity: provenanceFixture.finalIdentity, + archive: provenanceFixture.archive, + sbomPath: join(provenanceFixture.outputDirectory, provenanceAssets.sbom.name), + provenancePath + }) + ).rejects.toThrow(/provenance.*native|native.*provenance/i) + }) + + it('requires an exclusive archive input and honors cancellation', async () => { + const extra = await runtimeFixture() + await writeFile(join(extra.outputDirectory, 'unexpected.json'), '{}') + await expect(writeSshRelayRuntimePostSignMetadata(writerInput(extra))).rejects.toThrow( + /exclusive|unexpected|exact/i + ) + + const cancelled = await runtimeFixture() + const controller = new AbortController() + controller.abort(new Error('cancel post-sign metadata')) + await expect( + writeSshRelayRuntimePostSignMetadata({ + ...writerInput(cancelled), + signal: controller.signal + }) + ).rejects.toThrow(/cancel post-sign metadata/i) + expect(await readdir(cancelled.outputDirectory)).toEqual([cancelled.archive.name]) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-provenance.mjs b/config/scripts/ssh-relay-runtime-provenance.mjs index ee460e39fea..4afdb83cac4 100644 --- a/config/scripts/ssh-relay-runtime-provenance.mjs +++ b/config/scripts/ssh-relay-runtime-provenance.mjs @@ -15,8 +15,7 @@ async function writeJson(path, value) { return { name: path.split(/[\\/]/).at(-1), size: bytes.length, sha256: sha256(bytes) } } -export async function writeSshRelayRuntimeMetadata({ - outputDirectory, +export function createSshRelayRuntimeProvenance({ identity, archive, nodeRelease, @@ -27,10 +26,6 @@ export async function writeSshRelayRuntimeMetadata({ toolchain }) { assertSshRelayRuntimeToolchain(toolchain, identity.tupleId) - const sbomName = `orca-ssh-relay-runtime-${identity.tupleId}.spdx.json` - const provenanceName = `orca-ssh-relay-runtime-${identity.tupleId}.provenance.json` - const identityName = `orca-ssh-relay-runtime-${identity.tupleId}.identity.json` - const sbom = createSshRelayRuntimeSbom({ identity, archive, sourceDateEpoch }) const nativeFiles = identity.entries .filter( (entry) => @@ -59,7 +54,7 @@ export async function writeSshRelayRuntimeMetadata({ { uri: `${nodeRelease.baseUrl}/${library.name}`, digest: { sha256: library.sha256 } } ) } - const provenance = { + return { _type: 'https://in-toto.io/Statement/v1', subject: [{ name: archive.name, digest: { sha256: archive.sha256.slice('sha256:'.length) } }], predicateType: 'https://slsa.dev/provenance/v1', @@ -69,6 +64,7 @@ export async function writeSshRelayRuntimeMetadata({ externalParameters: { tuple: identity.tupleId, nodeVersion: identity.nodeVersion, + contentId: identity.contentId, sourceDateEpoch }, internalParameters: { toolchain }, @@ -84,6 +80,33 @@ export async function writeSshRelayRuntimeMetadata({ } } } +} + +export async function writeSshRelayRuntimeMetadata({ + outputDirectory, + identity, + archive, + nodeRelease, + sourceDateEpoch, + gitCommit, + builder, + runner, + toolchain +}) { + const sbomName = `orca-ssh-relay-runtime-${identity.tupleId}.spdx.json` + const provenanceName = `orca-ssh-relay-runtime-${identity.tupleId}.provenance.json` + const identityName = `orca-ssh-relay-runtime-${identity.tupleId}.identity.json` + const sbom = createSshRelayRuntimeSbom({ identity, archive, sourceDateEpoch }) + const provenance = createSshRelayRuntimeProvenance({ + identity, + archive, + nodeRelease, + sourceDateEpoch, + gitCommit, + builder, + runner, + toolchain + }) const identityDocument = { ...identity, archive: { diff --git a/config/scripts/ssh-relay-runtime-provenance.test.mjs b/config/scripts/ssh-relay-runtime-provenance.test.mjs index ba5340d6704..112c1a06d0e 100644 --- a/config/scripts/ssh-relay-runtime-provenance.test.mjs +++ b/config/scripts/ssh-relay-runtime-provenance.test.mjs @@ -122,6 +122,7 @@ describe('SSH relay runtime published metadata', () => { metadata: { runner } }) expect(provenance.predicate.buildDefinition).toMatchObject({ + externalParameters: { contentId: input.identity.contentId }, internalParameters: { toolchain }, resolvedDependencies: expect.arrayContaining([ { diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 9ea5e1fdb8a..6e800b58fa3 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -114,7 +114,8 @@ describe('SSH relay runtime artifact workflow', () => { 'manifest-assembly', 'manifest-signing-handoff', 'manifest-aggregate', - 'manifest-tuple' + 'manifest-tuple', + 'post-sign-metadata' ]) { const script = `ssh-relay-runtime-${moduleName}` expect(source.split(`${script}.test.mjs`)).toHaveLength(5) diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md index a4fdb0da153..01458e2f5c8 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 @@ -60,7 +60,7 @@ complete a box. 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. -- [ ] **WP3 active implementation — Supply verified post-sign tuple descriptors to the aggregate.** +- [ ] **WP3 active implementation — Tuple descriptors proven; regenerate post-sign metadata next.** Windows compatibility-kind parity is closed locally and on all six target-native jobs in exact-head run [29393022768](https://github.com/stablyai/orca/actions/runs/29393022768) under @@ -83,9 +83,15 @@ complete a box. per-platform role counts first (`E-M4-MANIFEST-TUPLE-SCHEMA-RED-001`). The correction, credential-free producer, 228-test release suite, 48-test desktop parity suite, and static gates are locally green under `E-M4-MANIFEST-TUPLE-LOCAL-001` and - `E-M4-MANIFEST-TUPLE-LOCAL-002`. Commit/push authorization for PR-contained work was granted - on 2026-07-15; committing, pushing, and collecting exact-head Node 24 native CI is the active - gate. Merging to `main` remains prohibited without separate user action. + `E-M4-MANIFEST-TUPLE-LOCAL-002`. Exact-head run + [29405619251](https://github.com/stablyai/orca/actions/runs/29405619251) passes the new seven-test + tuple suite and full runtime construction on all six Node 24 native jobs under + `E-M4-MANIFEST-TUPLE-CI-001`; PR Checks + [29405619196](https://github.com/stablyai/orca/actions/runs/29405619196) and Golden E2E + [29405619242](https://github.com/stablyai/orca/actions/runs/29405619242) are green. The artifact + run is red only for the declared Windows arm64 floor mismatch (hosted build 26200 versus + required 26100). Next, regenerate and semantically verify post-sign SBOM/provenance from the + verified final tree. Merging to `main` remains prohibited without separate user action. ## Work packages, in required order @@ -142,15 +148,23 @@ complete a box. consumers, and every tuple outside this slice (`E-M4-MANIFEST-AGGREGATE-LOCAL-001`, `E-M4-MANIFEST-AGGREGATE-LOCAL-002`, `E-M4-MANIFEST-AGGREGATE-CI-001`). -- [ ] **In progress — 2026-07-15, Codex implementation owner:** add the credential-free post-sign +- [x] Add the credential-free post-sign tuple-descriptor producer and native-verification handoff needed to supply the aggregate. Derive it only from a fully verified returned runtime tree; keep real signing, publication, desktop consumers, and every tuple outside this slice. The missing-module RED is recorded under `E-M4-MANIFEST-TUPLE-LOCAL-RED-001`. Before GREEN, correct release/desktop tuple-role cardinality for the real Windows closure under `E-M4-MANIFEST-TUPLE-SCHEMA-RED-001`. Code and local proof are complete under `E-M4-MANIFEST-TUPLE-LOCAL-001` and - `E-M4-MANIFEST-TUPLE-LOCAL-002`; commit/push is authorized and exact-head Node 24 native CI is - now the active gate. Do not merge to `main`. + `E-M4-MANIFEST-TUPLE-LOCAL-002`; all-six exact-head Node 24 proof is recorded under + `E-M4-MANIFEST-TUPLE-CI-001`. +- [ ] **In progress — 2026-07-15, Codex implementation owner:** regenerate SBOM and provenance from + the verified post-sign runtime tree and semantically bind both outputs to the final content + identity before tuple assembly. Keep credentials, publication, desktop consumers, and every + tuple disconnected. The purpose-named missing-module RED is recorded under + `E-M4-POST-SIGN-METADATA-LOCAL-RED-001`. The implementation, semantic tuple-consumer gate, + 233-test release suite, 48-test desktop parity suite, and static gates are locally green under + `E-M4-POST-SIGN-METADATA-LOCAL-001` and `E-M4-POST-SIGN-METADATA-LOCAL-002`; exact-head Node 24 + execution on all six native jobs is required before this item closes. - [ ] 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 6ca81fa5f14..75418f07646 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 post-sign tuple descriptor plus exact closure/schema parity correction — **In progress — 2026-07-15, Codex implementation owner**. The tuple-specific native role correction and credential-free producer are locally green under E-M4-MANIFEST-TUPLE-LOCAL-001 and E-M4-MANIFEST-TUPLE-LOCAL-002. The producer verifies a physically disjoint final runtime tree before and after archive inspection, accepts only a complete platform-specific native-verifier report bound to the final content ID, hashes bounded stable archive/SBOM/provenance inputs, writes one exclusive tuple descriptor, and re-verifies the exact closed four-file aggregate set. Exact-head Node 24 execution on all six native jobs remains required before this slice closes. It may not publish, connect desktop consumers, use production signing credentials, 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** — focused producer/aggregate/workflow/desktop parity passes 4 files / 50 tests, the release-side suite passes 40 files / 228 tests, desktop parity passes 3 files / 48 tests, and typecheck, full lint, formatting, syntax, max-lines, and diff gates pass under E-M4-MANIFEST-TUPLE-LOCAL-001 and E-M4-MANIFEST-TUPLE-LOCAL-002. The HTML plan, release validator, and desktop validator now share exact Linux/macOS/Windows native role cardinality. The user authorized Codex to drive PR-contained commit, push, CI, and publication-preparation work end to end while explicitly prohibiting merge to `main`; committing and pushing this package for exact-head Node 24 native CI is the active gate. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, publication wiring, semantic regeneration of post-sign SBOM/provenance, desktop embedding, and native trust remain separately gated. Nothing is published or enabled, and legacy remains the production default.
      +Current phase: Milestone 4 / Work Package 3 semantic post-sign SBOM/provenance regeneration — **In progress — 2026-07-15, Codex implementation owner**. The preceding tuple-specific native-role correction and credential-free descriptor producer are closed locally and on all six exact-head Node 24 native jobs under E-M4-MANIFEST-TUPLE-LOCAL-001, E-M4-MANIFEST-TUPLE-LOCAL-002, and E-M4-MANIFEST-TUPLE-CI-001. The active slice must regenerate bounded SBOM and provenance bytes from the verified final runtime tree and semantically bind them to the final content identity before tuple assembly. Its purpose-named missing-module RED is recorded under E-M4-POST-SIGN-METADATA-LOCAL-RED-001. It may not publish, connect desktop consumers, use production signing credentials, 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** — semantic post-sign metadata regeneration and tuple-consumer verification are locally green under E-M4-POST-SIGN-METADATA-LOCAL-001 and E-M4-POST-SIGN-METADATA-LOCAL-002: focused 5 files / 22 tests, release-side 41 files / 233 tests, desktop parity 3 files / 48 tests, typecheck, full lint, formatting, syntax, max-lines, and diff gates pass. The writer verifies the final tree and archive before and after exclusive bounded emission; SBOM and provenance bind the final content ID, exact archive, complete file/native inventory, builder/toolchain context, and tuple. Tuple assembly rejects stale semantic bindings before writing a descriptor. Exact-head Node 24 execution on all six native jobs remains required. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, publication wiring, desktop embedding, and native trust remain separately gated. Nothing is published or enabled, 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) @@ -76,15 +76,21 @@ same change as the work it records. - Completed Work Package 3 gate: the disconnected fail-closed aggregate boundary is green locally and on all six exact-head Node 24 native jobs under E-M4-MANIFEST-AGGREGATE-LOCAL-001, E-M4-MANIFEST-AGGREGATE-LOCAL-002, and E-M4-MANIFEST-AGGREGATE-CI-001. -- Active package: Work Package 3 credential-free post-sign tuple-descriptor producer and +- Completed Work Package 3 gate: credential-free post-sign tuple-descriptor producer and native-verification handoff. The descriptor must be derived only after returned-tree identity and native policy verification and must bind the exact archive, SBOM, provenance, native assessment, and content identity. E-M4-MANIFEST-TUPLE-SCHEMA-RED-001 requires an in-package release/desktop correction for tuple-specific native role counts before the producer can pass the real Windows - closure. The correction and producer are locally green under E-M4-MANIFEST-TUPLE-LOCAL-001 and - E-M4-MANIFEST-TUPLE-LOCAL-002; exact-head Node 24 native CI remains open. Production signing - credentials, publication, desktop consumers, and tuple enablement remain outside this slice. No - broader Milestone 4 box is checked until its evidence exists. + closure. The correction and producer are green locally and on all six exact-head Node 24 native + jobs under E-M4-MANIFEST-TUPLE-LOCAL-001, E-M4-MANIFEST-TUPLE-LOCAL-002, and + E-M4-MANIFEST-TUPLE-CI-001. +- Active package: Work Package 3 semantic regeneration and verification of post-sign SBOM and + provenance from the verified final runtime tree. E-M4-POST-SIGN-METADATA-LOCAL-RED-001 proves the + purpose-named boundary was absent before implementation. The implementation is locally green under + E-M4-POST-SIGN-METADATA-LOCAL-001 and E-M4-POST-SIGN-METADATA-LOCAL-002; exact-head Node 24 native + CI remains open. Production signing credentials, publication, desktop consumers, and tuple + enablement remain outside this slice. No broader Milestone 4 box is checked until its evidence + exists. - 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 @@ -202,10 +208,10 @@ 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: commit and push the locally green post-sign tuple package, then collect - exact-head Node 24 proof from all six native build jobs. Commit/push and PR-contained CI work are - authorized; merging to `main` is not. Keep semantic - post-sign metadata generation, real native/manifest signing credentials, endpoint trust, missing +- Next required action: commit and push the locally green semantic post-sign metadata package, then + collect exact-head Node 24 proof from all six native build jobs. Commit/push and PR-contained CI + work are authorized; merging to `main` is not. Keep real native/manifest signing credentials, + endpoint trust, missing exact-floor snapshots, production publication, desktop consumers, and every tuple's enabled state outside this slice. @@ -1555,6 +1561,10 @@ focused commands as their scripts/tests are introduced. `config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs` on all six runner families (E-M4-MANIFEST-AGGREGATE-CI-001) - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs` (E-M4-MANIFEST-TUPLE-LOCAL-RED-001, E-M4-MANIFEST-TUPLE-SCHEMA-RED-001, E-M4-MANIFEST-TUPLE-LOCAL-001) +- [x] Exact-head Node 24 native workflow execution of + `config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs` on all six runner families + (E-M4-MANIFEST-TUPLE-CI-001) +- [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs config/scripts/ssh-relay-runtime-provenance.test.mjs config/scripts/ssh-relay-runtime-sbom.test.mjs config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` (E-M4-POST-SIGN-METADATA-LOCAL-001) - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay*.test.mjs` (E-M4-MANIFEST-HANDOFF-LOCAL-002; all release-side SSH-relay contracts) - [x] `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` (E-M4-MANIFEST-HANDOFF-LOCAL-002; desktop schema/signature parity) @@ -9706,6 +9716,162 @@ diff --check`. Node 24 native jobs before marking this package complete. Keep every production/default gate unchanged and do not merge to `main`. +### E-M4-MANIFEST-TUPLE-CI-001 — Post-sign tuple producer passes all six native Node 24 jobs + +- Date: 2026-07-15 +- Commit SHA / PR: `cfea521c7c5f63a22c9895ac58931bc75577bb00`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741) +- Workflow and source: exact-head SSH Relay Runtime Artifacts run + [29405619251](https://github.com/stablyai/orca/actions/runs/29405619251), checked out from the PR + head rather than GitHub's synthetic merge; Node v24.18.0 +- Native runners and results: + - Linux x64: `ubuntu24` image `20260705.232.1`, job + [87320220032](https://github.com/stablyai/orca/actions/runs/29405619251/job/87320220032), PASS + in 3m55s; tuple suite 7/7 in 983 ms; artifact contracts 39 files / 224 tests in 5.53s. + - Linux arm64: `ubuntu24-arm64` image `20260714.61.1`, job + [87320220059](https://github.com/stablyai/orca/actions/runs/29405619251/job/87320220059), PASS + in 5m12s; tuple suite 7/7 in 790 ms; artifact contracts 39 files / 224 tests in 4.18s. + - macOS x64: `macos15` image `20260629.0276.1`, job + [87320220060](https://github.com/stablyai/orca/actions/runs/29405619251/job/87320220060), PASS + in 7m33s; tuple suite 7/7 in 4,779 ms; artifact contracts 39 files / 224 tests in 28.49s. + - macOS arm64: `macos15` image `20260706.0213.1`, job + [87320220121](https://github.com/stablyai/orca/actions/runs/29405619251/job/87320220121), PASS + in 4m28s; tuple suite 7/7 in 611 ms; artifact contracts 39 files / 224 tests in 6.44s. + - Windows x64: `win22` image `20260706.237.1`, job + [87320220042](https://github.com/stablyai/orca/actions/runs/29405619251/job/87320220042), PASS + in 5m21s; tuple suite 6 passed / 1 POSIX-only symlink case skipped in 2,789 ms; artifact contracts + 40 files / 219 passed / 9 platform-skipped in 10.24s. + - Windows arm64: `win11-arm64` image `20260706.102.1`, job + [87320220035](https://github.com/stablyai/orca/actions/runs/29405619251/job/87320220035), PASS + in 10m08s; tuple suite 6 passed / 1 POSIX-only symlink case skipped in 3,664 ms; artifact contracts + 40 files / 219 passed / 9 platform-skipped in 11.38s. +- Exact workflow command: both POSIX and Windows native paths syntax-check the tuple producer and + suite, then include `config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs` in the purpose-named + Vitest artifact-contract command before building, inspecting, smoking, exact-comparing, and + uploading unpublished runtime evidence. +- Supplemental exact-head results: + - Linux x64 oldest-userland job + [87321748114](https://github.com/stablyai/orca/actions/runs/29405619251/job/87321748114) and Linux + arm64 job + [87321748173](https://github.com/stablyai/orca/actions/runs/29405619251/job/87321748173) pass. + - Windows x64 oldest-floor job + [87322254816](https://github.com/stablyai/orca/actions/runs/29405619251/job/87322254816) passes on + build 20348; smoke is 5,368.9343 ms, RSS is 49,872,896 bytes, and total verification is + 6,606.8635 ms. + - Windows arm64 oldest-floor job + [87322254810](https://github.com/stablyai/orca/actions/runs/29405619251/job/87322254810) fails only + the declared OS-build oracle because hosted image `win11-arm64` is build 26200 instead of the + required 26100. Before that rejection, content ID + `sha256:02edf462be83c2864a89546d5344d348f9e07ce10964660342608a8c614e47db` passes full smoke in + 6,054.4899 ms with 49,721,344-byte RSS and 8,402.627 ms total verification. +- Broad exact-head regressions: PR Checks + [29405619196](https://github.com/stablyai/orca/actions/runs/29405619196) job + [87320219351](https://github.com/stablyai/orca/actions/runs/29405619196/job/87320219351) passes; + Golden E2E [29405619242](https://github.com/stablyai/orca/actions/runs/29405619242) passes on Linux + job [87320219767](https://github.com/stablyai/orca/actions/runs/29405619242/job/87320219767) and + macOS job [87320219784](https://github.com/stablyai/orca/actions/runs/29405619242/job/87320219784). +- Oracle proved: syntax and the complete credential-free descriptor/native-verification contract run + under repository Node 24 on all six native target families without regressing runtime construction, + exact clean-build equality, bundled Node, PTY/watcher smoke, unpublished artifact upload, PR + verification, or Golden E2E. The expected workflow failure is solely the pre-existing Windows + arm64 exact-floor gap after its native build and tuple suite pass. +- Does not prove: semantic regeneration or validation of post-sign SBOM/provenance, a report from + real Apple/SignPath returned bytes, native trust, protected signing, publication/read-back, + desktop embedding, SSH transfer/install, or any enabled tuple. +- Checklist items satisfied: all-six exact-head CI gate for the credential-free post-sign tuple + producer and release/desktop exact-role parity. Broader Work Package 3 and Milestone 4 remain open. +- Follow-up: regenerate and semantically verify SBOM/provenance from the verified final runtime tree + before connecting signing credentials, publication, or a desktop consumer. + +### E-M4-POST-SIGN-METADATA-LOCAL-RED-001 — Post-sign metadata boundary is absent + +- Date: 2026-07-15 +- Commit SHA / PR: uncommitted purpose-named test atop + `cfea521c7c5f63a22c9895ac58931bc75577bb00`; draft PR #8741 +- Runner: macOS 26.2 arm64, native local worktree; repository Node/pnpm toolchain +- Remote: not applicable; disconnected credential-free release boundary +- Transport/network: local filesystem only; no signer, credential, release, SSH host, publication, + desktop consumer, or production/default call +- Command: + `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs` +- Result: expected FAIL in 728 ms; one suite fails before collection because + `./ssh-relay-runtime-post-sign-metadata.mjs` does not exist. +- Oracle proved: the purpose-named writer/verifier for regenerating SBOM/provenance from the verified + final runtime tree and rejecting stale semantic bindings is absent; the test cannot pass through + the existing pre-sign metadata writer or tuple producer. +- Does not prove: implementation behavior, metadata semantics, Node 24/native-runner execution, + native trust, publication, desktop embedding, SSH behavior, or an enabled tuple. +- Checklist items satisfied: RED prerequisite for the active semantic post-sign metadata package. +- Follow-up: implement the disconnected boundary, integrate its semantic verifier before tuple + descriptor assembly, then run focused and broad regressions before exact-head native CI. + +### E-M4-POST-SIGN-METADATA-LOCAL-001 — Final-tree metadata regeneration and semantic gate pass + +- Date: 2026-07-15 +- Commit SHA / PR: uncommitted package atop + `cfea521c7c5f63a22c9895ac58931bc75577bb00`; draft PR #8741 +- Runner: macOS 26.2 arm64, native local worktree; Node v26.0.0 and pnpm 10.24.0 +- Remote: not applicable; disconnected credential-free release boundary +- Transport/network: local filesystem and generated runtime archives only; no signer, credential, + release, SSH host, publication, desktop consumer, or production/default call +- Commands and results: + - Purpose-named post-sign metadata, provenance, SBOM, tuple-consumer, and workflow suite: + `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs config/scripts/ssh-relay-runtime-provenance.test.mjs config/scripts/ssh-relay-runtime-sbom.test.mjs config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` + — PASS, 5 files / 22 tests. Repeated focused confirmation after strict provenance validation passes + 3 files / 18 tests in 10.11 seconds test duration. + - `node --check` for the post-sign metadata module and suite — PASS. +- Oracle proved: + - one exclusive archive input and physically disjoint verified final tree produce only bounded, + exclusive SBOM and provenance assets; cancellation, extra input, tree mutation, and archive + mutation fail without leaving partial metadata; + - the SBOM is exactly reconstructed from the final identity and archive, including every file, + package owner, checksum, final content ID, and immutable archive-derived namespace; + - SLSA provenance binds the exact archive, tuple, Node version, final content ID, source epoch, + complete final native-file hashes, builder, runner, resolved dependencies, and exact validated + toolchain shape; + - stale SBOM content/file/archive data and stale provenance archive/content/native data fail before + tuple descriptor creation; the final tree and archive are reverified after metadata emission. +- Does not prove: Node 24/native-runner execution, a real Apple/SignPath returned tree, native trust, + protected signing, publication/read-back, desktop embedding, SSH transfer/install, or an enabled + tuple. +- Checklist items satisfied: local functional/semantic/workflow gate for the active post-sign + metadata package. +- Follow-up: run broad release, desktop parity, and repository static gates, then require all-six + exact-head Node 24 CI. + +### E-M4-POST-SIGN-METADATA-LOCAL-002 — Broad post-sign metadata regressions and static gates pass + +- Date: 2026-07-15 +- Commit SHA / PR: same uncommitted package atop + `cfea521c7c5f63a22c9895ac58931bc75577bb00`; draft PR #8741 +- Runner: macOS 26.2 arm64, native local worktree; Node v26.0.0 and pnpm 10.24.0 +- Commands and results: + - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay*.test.mjs` + — PASS, 41 files / 233 tests; final observed 80.39-second Vitest duration / 86.12-second wall, + 188,612,608-byte maximum RSS, 96,228,800-byte peak memory footprint, and zero swaps. An earlier + identical pass completed in 16.57 seconds / 19.66 seconds wall; the final run and lint were + heavily CPU-starved locally, so exact-head native CI is the timing oracle for this artifact-only + suite. + - `/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; 131,694,592-byte maximum RSS and zero swaps. + - `/usr/bin/time -l pnpm run typecheck` — PASS with 1,259,520,000-byte maximum RSS and zero swaps. + - `/usr/bin/time -l pnpm run lint` — PASS; 41 reliability gates, the 355-entry max-lines ratchet, + bundled-skill guides, localization catalog/parity, and localization coverage pass. All 26 + warnings are in untouched existing files. The final run took 293.54 seconds wall with only 70.17 + seconds user CPU and 701,821 involuntary context switches, confirming severe local scheduling + contention rather than a product-path latency measurement. + - Focused `pnpm exec oxfmt --check` for all eight code/workflow files — PASS. + - `git diff --check` — PASS with no output; both user-owned Node/npm resolver files have zero diff. +- Oracle proved: the post-sign metadata boundary and tuple semantic gate preserve all release-side + SSH-relay contracts, desktop schema/signature parity, repository type/static/reliability gates, + localization, formatting, and file line budgets without a bypass. +- Does not prove: repository Node 24/native-runner execution, real post-sign signatures/native trust, + publication, desktop embedding, SSH behavior, or an enabled tuple. Local wall time is not a + production latency baseline. +- Checklist items satisfied: broad local regression/static gate for the active package. +- Follow-up: commit and push, then require all six exact-head Node 24 native jobs before marking this + package complete. Keep every production/default gate unchanged and do not merge to `main`. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -9763,11 +9929,9 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the authorized, locally green post-sign tuple package, then collect exact-head Node -24 proof from all six native build jobs. Do not check the package complete before that CI exists and -do not merge to `main`. Retain every production/default gate. The next independent artifact-only slice after -CI is semantic regeneration and verification of the post-sign SBOM/provenance bytes consumed here; -do not connect release publication, production signing credentials, or a desktop consumer. In +Commit and push the locally green semantic post-sign metadata package, then collect exact-head Node +24 proof from all six native build jobs; do not merge to `main`. Retain every production/default +gate and do not connect release publication, production signing credentials, 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. From 9e81229274a3aa9a0fdb4db537e853e1cc9699ca Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 06:44:48 -0400 Subject: [PATCH 116/267] test(ssh): use native runtime metadata fixtures Co-authored-by: Orca --- ...-relay-runtime-post-sign-metadata.test.mjs | 80 +++++++++----- ...elease-implementation-checklist-summary.md | 19 +++- ...github-release-implementation-checklist.md | 101 ++++++++++++++++-- 3 files changed, 160 insertions(+), 40 deletions(-) diff --git a/config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs b/config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs index 596418d154a..5a3d3594b1d 100644 --- a/config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs +++ b/config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs @@ -22,7 +22,47 @@ function sha256(bytes) { return `sha256:${createHash('sha256').update(bytes).digest('hex')}` } -function toolchain() { +function fixtureTarget() { + if (!['darwin', 'linux', 'win32'].includes(process.platform)) { + throw new Error(`Unsupported runtime metadata fixture platform: ${process.platform}`) + } + if (!['arm64', 'x64'].includes(process.arch)) { + throw new Error(`Unsupported runtime metadata fixture architecture: ${process.arch}`) + } + const architecture = process.arch + const tupleId = + process.platform === 'linux' + ? `linux-${architecture}-glibc` + : `${process.platform}-${architecture}` + const labels = { + 'darwin-arm64': 'macos-15', + 'darwin-x64': 'macos-15-intel', + 'linux-arm64-glibc': 'ubuntu-24.04-arm', + 'linux-x64-glibc': 'ubuntu-24.04', + 'win32-arm64': 'windows-11-arm', + 'win32-x64': 'windows-2022' + } + return { + tupleId, + os: process.platform, + architecture, + runner: { + os: + process.platform === 'win32' + ? 'Windows' + : process.platform === 'darwin' + ? 'macOS' + : 'Linux', + architecture: architecture.toUpperCase(), + environment: 'github-hosted', + requestedLabel: labels[tupleId], + image: { os: `fixture-${process.platform}`, version: 'fixture-version' } + } + } +} + +function toolchain(tupleId) { + const platformTools = tupleId.startsWith('win32-') ? ['linker'] : ['strip'] return Object.fromEntries( [ 'buildNode', @@ -33,7 +73,7 @@ function toolchain() { 'archive', 'nodeAddonApi', 'nodeGyp', - 'strip' + ...platformTools ].map((name, index) => [ name, { version: `${name} version`, sha256: `sha256:${(index + 6).toString(16).repeat(64)}` } @@ -47,7 +87,9 @@ async function runtimeFixture() { const runtimeRoot = join(root, 'runtime') const outputDirectory = join(root, 'output') await Promise.all([mkdir(runtimeRoot), mkdir(outputDirectory)]) - const tupleId = 'linux-x64-glibc' + // Why: Windows cannot materialize POSIX executable bits; its native ZIP writer carries them. + const target = fixtureTarget() + const { tupleId } = target const entries = [] for (const entry of expectedSshRelayRuntimeClosureEntries(tupleId)) { const path = join(runtimeRoot, ...entry.path.split('/')) @@ -64,8 +106,8 @@ async function runtimeFixture() { const base = { identitySchemaVersion: 1, tupleId, - os: 'linux', - architecture: 'x64', + os: target.os, + architecture: target.architecture, compatibility: sshRelayRuntimeCompatibility[tupleId], nodeVersion: '24.18.0', dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, @@ -90,30 +132,12 @@ async function runtimeFixture() { outputDirectory, finalIdentity, archive, - nodeRelease: { - baseUrl: 'https://nodejs.org/dist/v24.18.0', - archives: { - [tupleId]: { - name: 'node-v24.18.0-linux-x64.tar.xz', - sha256: 'b'.repeat(64) - } - }, - signature: { - key: { - sourceUrl: 'https://github.com/nodejs/release-keys/raw/commit/gpg/key.asc', - sha256: 'c'.repeat(64) - } - } - }, + nodeRelease: JSON.parse( + await readFile(new URL('../ssh-relay-node-release-v24.18.0.json', import.meta.url), 'utf8') + ), builder: `https://github.com/stablyai/orca/blob/${GIT_COMMIT}/.github/workflows/ssh-relay-runtime-artifacts.yml`, - runner: { - os: 'Linux', - architecture: 'X64', - environment: 'github-hosted', - requestedLabel: 'ubuntu-24.04', - image: { os: 'ubuntu24', version: '20260705.232.1' } - }, - toolchain: toolchain() + runner: target.runner, + toolchain: toolchain(tupleId) } } 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 01458e2f5c8..4e29ab7c268 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 @@ -91,7 +91,15 @@ complete a box. [29405619242](https://github.com/stablyai/orca/actions/runs/29405619242) are green. The artifact run is red only for the declared Windows arm64 floor mismatch (hosted build 26200 versus required 26100). Next, regenerate and semantically verify post-sign SBOM/provenance from the - verified final tree. Merging to `main` remains prohibited without separate user action. + verified final tree. That implementation is locally green, but exact-head run + [29408355188](https://github.com/stablyai/orca/actions/runs/29408355188) exposed a Windows x64 + test-fixture portability defect: the hardcoded Linux tar fixture cannot obtain its declared + executable mode from a Windows filesystem, so strict archive inspection correctly rejects + `bin/node` (`E-M4-POST-SIGN-METADATA-CI-RED-001`). The platform-native fixture correction is + locally green across the 22-test focused suite, 233-test release suite, 48-test desktop parity + suite, typecheck, and full lint without weakening production mode validation + (`E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001`). Replacement all-six exact-head evidence is + required. Merging to `main` remains prohibited. ## Work packages, in required order @@ -164,7 +172,14 @@ complete a box. `E-M4-POST-SIGN-METADATA-LOCAL-RED-001`. The implementation, semantic tuple-consumer gate, 233-test release suite, 48-test desktop parity suite, and static gates are locally green under `E-M4-POST-SIGN-METADATA-LOCAL-001` and `E-M4-POST-SIGN-METADATA-LOCAL-002`; exact-head Node 24 - execution on all six native jobs is required before this item closes. + execution on all six native jobs is required before this item closes. Windows x64 job + [87329210635](https://github.com/stablyai/orca/actions/runs/29408355188/job/87329210635) + is the required CI RED: its filesystem cannot create the hardcoded Linux tar fixture's + executable mode, and strict archive inspection rejects `bin/node` + (`E-M4-POST-SIGN-METADATA-CI-RED-001`). The test-fixture-only correction now selects the exact + native tuple/archive family on each runner and is locally green under + `E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001`; production type/mode validation is unchanged. + Replacement exact-head execution on all six native jobs is still required. - [ ] 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 75418f07646..294edef78f2 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 semantic post-sign SBOM/provenance regeneration — **In progress — 2026-07-15, Codex implementation owner**. The preceding tuple-specific native-role correction and credential-free descriptor producer are closed locally and on all six exact-head Node 24 native jobs under E-M4-MANIFEST-TUPLE-LOCAL-001, E-M4-MANIFEST-TUPLE-LOCAL-002, and E-M4-MANIFEST-TUPLE-CI-001. The active slice must regenerate bounded SBOM and provenance bytes from the verified final runtime tree and semantically bind them to the final content identity before tuple assembly. Its purpose-named missing-module RED is recorded under E-M4-POST-SIGN-METADATA-LOCAL-RED-001. It may not publish, connect desktop consumers, use production signing credentials, 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** — semantic post-sign metadata regeneration and tuple-consumer verification are locally green under E-M4-POST-SIGN-METADATA-LOCAL-001 and E-M4-POST-SIGN-METADATA-LOCAL-002: focused 5 files / 22 tests, release-side 41 files / 233 tests, desktop parity 3 files / 48 tests, typecheck, full lint, formatting, syntax, max-lines, and diff gates pass. The writer verifies the final tree and archive before and after exclusive bounded emission; SBOM and provenance bind the final content ID, exact archive, complete file/native inventory, builder/toolchain context, and tuple. Tuple assembly rejects stale semantic bindings before writing a descriptor. Exact-head Node 24 execution on all six native jobs remains required. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, publication wiring, desktop embedding, and native trust remain separately gated. Nothing is published or enabled, legacy remains the production default, and merge to `main` remains prohibited.
      +Current phase: Milestone 4 / Work Package 3 semantic post-sign SBOM/provenance regeneration — **In progress — 2026-07-15, Codex implementation owner**. The preceding tuple-specific native-role correction and credential-free descriptor producer are closed locally and on all six exact-head Node 24 native jobs under E-M4-MANIFEST-TUPLE-LOCAL-001, E-M4-MANIFEST-TUPLE-LOCAL-002, and E-M4-MANIFEST-TUPLE-CI-001. The active slice must regenerate bounded SBOM and provenance bytes from the verified final runtime tree and semantically bind them to the final content identity before tuple assembly. Its purpose-named missing-module RED is recorded under E-M4-POST-SIGN-METADATA-LOCAL-RED-001. Exact-head Windows x64 execution exposed a test-fixture-only POSIX-mode portability defect under E-M4-POST-SIGN-METADATA-CI-RED-001. The correction now selects each runner's native tuple/archive family and is locally green without weakening archive validation under E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001; replacement all-six exact-head evidence remains required. This slice may not publish, connect desktop consumers, use production signing credentials, 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** — semantic post-sign metadata regeneration and tuple-consumer verification are locally green under E-M4-POST-SIGN-METADATA-LOCAL-001 and E-M4-POST-SIGN-METADATA-LOCAL-002. Exact-head run 29408355188 at `1989e801af4a922a646d62ffd13e235615462975` fails only the new metadata fixture on Windows x64: the hardcoded Linux tar fixture cannot obtain its declared executable mode from a Windows filesystem, so the strict production archive inspector correctly rejects `bin/node` (`E-M4-POST-SIGN-METADATA-CI-RED-001`). The platform-native fixture correction passes 5 files / 22 focused tests, 41 files / 233 release-side tests, 3 files / 48 desktop parity tests, typecheck, full lint, formatting, syntax, max-lines, and diff gates under E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001. Production archive type/mode enforcement is unchanged. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, publication wiring, desktop embedding, and native trust remain separately gated. Nothing is published or enabled, 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) @@ -87,8 +87,13 @@ same change as the work it records. - Active package: Work Package 3 semantic regeneration and verification of post-sign SBOM and provenance from the verified final runtime tree. E-M4-POST-SIGN-METADATA-LOCAL-RED-001 proves the purpose-named boundary was absent before implementation. The implementation is locally green under - E-M4-POST-SIGN-METADATA-LOCAL-001 and E-M4-POST-SIGN-METADATA-LOCAL-002; exact-head Node 24 native - CI remains open. Production signing credentials, publication, desktop consumers, and tuple + E-M4-POST-SIGN-METADATA-LOCAL-001 and E-M4-POST-SIGN-METADATA-LOCAL-002. Exact-head run + 29408355188 exposes a Windows x64 test-fixture portability failure under + E-M4-POST-SIGN-METADATA-CI-RED-001; production archive type/mode validation remains correct and + must not be relaxed. The fixture now selects the exact platform/architecture tuple, archive + family, release inputs, runner identity, and toolchain shape and is locally green under + E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001. Replace all-six exact-head Node 24 evidence before + closing the package. Production signing credentials, publication, desktop consumers, and tuple enablement remain outside this slice. No broader Milestone 4 box is checked until its evidence exists. - Completed Work Package 2 gate: target-native Windows source-signature reports from exact-head @@ -9872,6 +9877,81 @@ diff --check`. - Follow-up: commit and push, then require all six exact-head Node 24 native jobs before marking this package complete. Keep every production/default gate unchanged and do not merge to `main`. +### E-M4-POST-SIGN-METADATA-CI-RED-001 — Windows rejects host-incompatible Linux tar fixture + +- Date: 2026-07-15 +- Commit SHA / PR: `1989e801af4a922a646d62ffd13e235615462975`; draft PR #8741 +- Workflow/run: SSH relay runtime artifacts run + [29408355188](https://github.com/stablyai/orca/actions/runs/29408355188), Windows x64 job + [87329210635](https://github.com/stablyai/orca/actions/runs/29408355188/job/87329210635) +- Runner: GitHub-hosted `windows-2022` x64 image `20260706.237.1`, runner + `GitHub Actions 1000055651`; Node 24 workflow toolchain +- Remote: not applicable; disconnected credential-free artifact construction and tests +- Transport/network: GitHub dependency/source downloads used by the artifact workflow; no signer, + SSH host, publication, desktop consumer, production/default call, or enabled tuple +- Command: native job's release-side contract suite, + `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay*.test.mjs` +- Result: expected evidence-gating FAIL; 40 files passed and 1 failed, 222 tests passed, 2 failed, + and 9 skipped in 11.23 seconds. Both failures are in the post-sign metadata suite: + “regenerates exact SBOM and provenance from the verified final tree” and “makes tuple consumers + reject stale SBOM and provenance bindings.” Strict archive inspection reports + `Runtime archive type or mode mismatch: bin/node`. +- Oracle proved: the new test fixture hardcodes a `linux-x64-glibc` tar identity but creates its + executable through the Windows filesystem, where `writeFile(..., { mode: 0o755 })` cannot supply + POSIX executable mode bits. The production archive inspector fails closed on the mismatch as + designed; this is not evidence to weaken archive type/mode validation. +- Does not prove: corrected cross-platform fixture behavior, all-six exact-head pass, real + post-sign signatures/native trust, publication, desktop embedding, SSH behavior, or an enabled + tuple. +- Checklist items satisfied: required exact-head CI RED and root-cause evidence for the active + correction; the semantic post-sign metadata package remains open. +- Follow-up: make the fixture select a platform-native tuple/archive/closure, rerun focused and broad + local gates, push the correction, and replace the exact-head all-six native evidence. + +### E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001 — Platform-native metadata fixture passes locally + +- Date: 2026-07-15 +- Commit SHA / PR: uncommitted correction atop + `1989e801af4a922a646d62ffd13e235615462975`; draft PR #8741 +- Runner: macOS 26.2 arm64, native local worktree; Node v26.0.0 and pnpm 10.24.0 +- Remote: not applicable; disconnected credential-free artifact contract tests +- Transport/network: local filesystem and generated Darwin tar archives only; no signer, SSH host, + publication, desktop consumer, production/default call, or enabled tuple +- Commands and results: + - Purpose-named correction: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs` + — PASS, 1 file / 4 tests in 2.67 seconds test duration / 5.84 seconds wall; + 132,399,104-byte maximum RSS, 96,589,128-byte peak memory footprint, and zero swaps. + - Semantic post-sign focused suite: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs config/scripts/ssh-relay-runtime-provenance.test.mjs config/scripts/ssh-relay-runtime-sbom.test.mjs config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` + — PASS, 5 files / 22 tests in 12.70 seconds test duration / 18.96 seconds wall; + 134,725,632-byte maximum RSS, 96,310,600-byte peak memory footprint, and zero swaps. + - 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, 41 files / 233 tests in 35.93 seconds test duration / 39.61 seconds wall; + 189,087,744-byte maximum RSS, 96,998,824-byte peak memory footprint, and zero swaps. + - Desktop schema/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 2.02 seconds test duration / 3.70 seconds wall; + 132,562,944-byte maximum RSS, 96,343,440-byte peak memory footprint, and zero swaps. + - `/usr/bin/time -l pnpm run typecheck` — PASS in 6.53 seconds wall with + 1,215,283,200-byte maximum RSS and zero swaps. + - `/usr/bin/time -l pnpm run lint` — PASS in 63.09 seconds wall with the 41 reliability gates, + 355-entry max-lines ratchet, bundled-skill guides, localization catalog/parity, and localization + coverage green. All 26 warnings are in untouched existing files. + - Focused `pnpm exec oxfmt --check`, `node --check`, `git diff --check`, and zero diff in both + user-owned Node/npm resolver files — PASS. +- Oracle proved: the fixture now selects the exact native tuple, runtime closure, archive family, + immutable Node release inputs, runner identity, and toolchain shape for all six supported + platform/architecture combinations. POSIX runners retain strict tar mode proof; Windows runners + use the ZIP writer that records declared Unix modes independently of Windows filesystem modes. + Production archive inspection is unchanged. +- Does not prove: Windows execution or any other replacement native-runner cell, real post-sign + signatures/native trust, publication, desktop embedding, SSH behavior, or an enabled tuple. +- Checklist items satisfied: local correction gate for E-M4-POST-SIGN-METADATA-CI-RED-001; the + semantic post-sign metadata package remains open pending all-six replacement CI. +- Follow-up: commit and push the correction, then require all six exact-head Node 24 native jobs. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -9929,12 +10009,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the locally green semantic post-sign metadata package, then collect exact-head Node -24 proof from all six native build jobs; do not merge to `main`. Retain every production/default -gate and do not connect release publication, production signing credentials, 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. +Commit and push the locally green platform-native fixture correction under +E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001, then collect replacement exact-head Node 24 proof from +all six native build jobs; do not merge to `main`. Retain every production/default gate and do not +connect release publication, production signing credentials, 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 From e7f4474908af895cd9fbaccfc575d89f7ed6a6b2 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 07:04:31 -0400 Subject: [PATCH 117/267] docs(ssh): record native metadata proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 20 +++- ...github-release-implementation-checklist.md | 97 ++++++++++++++++--- 2 files changed, 97 insertions(+), 20 deletions(-) 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 4e29ab7c268..b3a6d38246d 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 @@ -99,7 +99,15 @@ complete a box. locally green across the 22-test focused suite, 233-test release suite, 48-test desktop parity suite, typecheck, and full lint without weakening production mode validation (`E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001`). Replacement all-six exact-head evidence is - required. Merging to `main` remains prohibited. + complete in run + [29409257513](https://github.com/stablyai/orca/actions/runs/29409257513): all six native build + jobs pass the corrected four-test metadata suite and full artifact construction under + `E-M4-POST-SIGN-METADATA-CI-001`. PR Checks + [29409257568](https://github.com/stablyai/orca/actions/runs/29409257568) and Golden E2E + [29409257636](https://github.com/stablyai/orca/actions/runs/29409257636) are green. The artifact + run is red only for the separately declared Windows arm64 floor mismatch. Next, add a + disconnected reusable build-prerequisite contract without connecting release-cut, signing, + publication, desktop consumers, or any tuple. Merging to `main` remains prohibited. ## Work packages, in required order @@ -165,7 +173,7 @@ complete a box. local proof are complete under `E-M4-MANIFEST-TUPLE-LOCAL-001` and `E-M4-MANIFEST-TUPLE-LOCAL-002`; all-six exact-head Node 24 proof is recorded under `E-M4-MANIFEST-TUPLE-CI-001`. -- [ ] **In progress — 2026-07-15, Codex implementation owner:** regenerate SBOM and provenance from +- [x] Regenerate SBOM and provenance from the verified post-sign runtime tree and semantically bind both outputs to the final content identity before tuple assembly. Keep credentials, publication, desktop consumers, and every tuple disconnected. The purpose-named missing-module RED is recorded under @@ -179,8 +187,12 @@ complete a box. (`E-M4-POST-SIGN-METADATA-CI-RED-001`). The test-fixture-only correction now selects the exact native tuple/archive family on each runner and is locally green under `E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001`; production type/mode validation is unchanged. - Replacement exact-head execution on all six native jobs is still required. -- [ ] Add target-native runtime jobs as desktop release prerequisites. + Exact-head execution passes on all six native jobs under + `E-M4-POST-SIGN-METADATA-CI-001`. +- [ ] **In progress — 2026-07-15, Codex implementation owner:** add a reusable target-native runtime + build prerequisite contract. Keep it disconnected from release-cut and every desktop build + until exact floors, native signing/trust, aggregate, and publication gates are complete; no + tuple is enabled by this slice. - [ ] Add native signing jobs; hash only the returned signed bytes. - [ ] Add a fail-closed aggregate and immutable manifest-signing job. - [ ] Embed the exact signed manifest and accepted keys in each desktop build. 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 294edef78f2..99b787fff58 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 semantic post-sign SBOM/provenance regeneration — **In progress — 2026-07-15, Codex implementation owner**. The preceding tuple-specific native-role correction and credential-free descriptor producer are closed locally and on all six exact-head Node 24 native jobs under E-M4-MANIFEST-TUPLE-LOCAL-001, E-M4-MANIFEST-TUPLE-LOCAL-002, and E-M4-MANIFEST-TUPLE-CI-001. The active slice must regenerate bounded SBOM and provenance bytes from the verified final runtime tree and semantically bind them to the final content identity before tuple assembly. Its purpose-named missing-module RED is recorded under E-M4-POST-SIGN-METADATA-LOCAL-RED-001. Exact-head Windows x64 execution exposed a test-fixture-only POSIX-mode portability defect under E-M4-POST-SIGN-METADATA-CI-RED-001. The correction now selects each runner's native tuple/archive family and is locally green without weakening archive validation under E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001; replacement all-six exact-head evidence remains required. This slice may not publish, connect desktop consumers, use production signing credentials, 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** — semantic post-sign metadata regeneration and tuple-consumer verification are locally green under E-M4-POST-SIGN-METADATA-LOCAL-001 and E-M4-POST-SIGN-METADATA-LOCAL-002. Exact-head run 29408355188 at `1989e801af4a922a646d62ffd13e235615462975` fails only the new metadata fixture on Windows x64: the hardcoded Linux tar fixture cannot obtain its declared executable mode from a Windows filesystem, so the strict production archive inspector correctly rejects `bin/node` (`E-M4-POST-SIGN-METADATA-CI-RED-001`). The platform-native fixture correction passes 5 files / 22 focused tests, 41 files / 233 release-side tests, 3 files / 48 desktop parity tests, typecheck, full lint, formatting, syntax, max-lines, and diff gates under E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001. Production archive type/mode enforcement is unchanged. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, publication wiring, desktop embedding, and native trust remain separately gated. Nothing is published or enabled, legacy remains the production default, and merge to `main` remains prohibited.
      +Current phase: Milestone 4 / Work Package 3 reusable target-native build-prerequisite contract — **In progress — 2026-07-15, Codex implementation owner**. Semantic post-sign SBOM/provenance regeneration is closed locally and on all six exact-head Node 24 native build jobs under E-M4-POST-SIGN-METADATA-LOCAL-001, E-M4-POST-SIGN-METADATA-LOCAL-002, E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001, and E-M4-POST-SIGN-METADATA-CI-001. The active slice may define and test only a credential-free reusable target-native build-prerequisite interface. It must remain disconnected from release-cut and every desktop build until exact floors, native signing/trust, aggregate, publication/read-back, and embedding gates are complete. It may not publish, use production signing credentials, connect a desktop consumer, 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 run 29409257513 at `9e81229274a3aa9a0fdb4db537e853e1cc9699ca` passes the corrected four-test post-sign metadata suite, full release-side contract suite, native build-twice/equality, archive inspection, Node/PTY/watcher smoke, and unpublished evidence upload on all six native build jobs under E-M4-POST-SIGN-METADATA-CI-001. PR Checks 29409257568 and Golden E2E 29409257636 are green. The artifact run is red only for the declared Windows arm64 oldest-floor mismatch: the hosted image is build 26200 instead of required build 26100. The next slice is a disconnected reusable build-prerequisite contract, not release workflow activation. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, publication wiring, desktop embedding, and native trust remain separately gated. Nothing is published or enabled, 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) @@ -84,7 +84,7 @@ same change as the work it records. closure. The correction and producer are green locally and on all six exact-head Node 24 native jobs under E-M4-MANIFEST-TUPLE-LOCAL-001, E-M4-MANIFEST-TUPLE-LOCAL-002, and E-M4-MANIFEST-TUPLE-CI-001. -- Active package: Work Package 3 semantic regeneration and verification of post-sign SBOM and +- Completed Work Package 3 gate: semantic regeneration and verification of post-sign SBOM and provenance from the verified final runtime tree. E-M4-POST-SIGN-METADATA-LOCAL-RED-001 proves the purpose-named boundary was absent before implementation. The implementation is locally green under E-M4-POST-SIGN-METADATA-LOCAL-001 and E-M4-POST-SIGN-METADATA-LOCAL-002. Exact-head run @@ -92,10 +92,14 @@ same change as the work it records. E-M4-POST-SIGN-METADATA-CI-RED-001; production archive type/mode validation remains correct and must not be relaxed. The fixture now selects the exact platform/architecture tuple, archive family, release inputs, runner identity, and toolchain shape and is locally green under - E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001. Replace all-six exact-head Node 24 evidence before - closing the package. Production signing credentials, publication, desktop consumers, and tuple - enablement remain outside this slice. No broader Milestone 4 box is checked until its evidence - exists. + E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001. Replacement exact-head Node 24 evidence passes all + six native build jobs under E-M4-POST-SIGN-METADATA-CI-001. Production signing credentials, + publication, desktop consumers, and tuple enablement remain outside this slice. +- Active package: Work Package 3 reusable target-native build-prerequisite contract. Define and + test a credential-free reusable interface over the existing target-native jobs, but do not connect + release-cut or a desktop build until exact-floor, native signing/trust, aggregate, + publication/read-back, and embedding gates are complete. No broader Milestone 4 box is checked + until its complete evidence exists. - 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 @@ -941,8 +945,11 @@ Rules: ## Milestone 4 — Make Relay Artifacts Prerequisites of Desktop Builds -- [ ] Add target-native runtime build jobs for every enabled tuple; do not treat a cross-build alone - as native execution evidence. +- [ ] **In progress — 2026-07-15, Codex implementation owner:** add a reusable target-native runtime + build prerequisite contract for every candidate tuple; do not treat a cross-build alone as + native execution evidence. Keep release-cut and desktop builds disconnected until the + remaining exact-floor, signing/trust, aggregate, publication/read-back, and embedding gates + pass. - [ ] Add platform-native signing jobs. Signed macOS and Windows runtime bytes must return as immutable outputs before final hashes are computed. - [ ] Add one fail-closed aggregate job that waits for all required native build/signing outputs, @@ -1570,6 +1577,9 @@ focused commands as their scripts/tests are introduced. `config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs` on all six runner families (E-M4-MANIFEST-TUPLE-CI-001) - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs config/scripts/ssh-relay-runtime-provenance.test.mjs config/scripts/ssh-relay-runtime-sbom.test.mjs config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` (E-M4-POST-SIGN-METADATA-LOCAL-001) +- [x] Exact-head Node 24 native workflow execution of + `config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs` on all six runner families + (E-M4-POST-SIGN-METADATA-CI-001) - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay*.test.mjs` (E-M4-MANIFEST-HANDOFF-LOCAL-002; all release-side SSH-relay contracts) - [x] `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` (E-M4-MANIFEST-HANDOFF-LOCAL-002; desktop schema/signature parity) @@ -9952,6 +9962,61 @@ diff --check`. semantic post-sign metadata package remains open pending all-six replacement CI. - Follow-up: commit and push the correction, then require all six exact-head Node 24 native jobs. +### E-M4-POST-SIGN-METADATA-CI-001 — Platform-native post-sign metadata passes all six build jobs + +- Date: 2026-07-15 +- Commit SHA / PR: `9e81229274a3aa9a0fdb4db537e853e1cc9699ca`; draft PR #8741 +- Workflow/run: SSH Relay Runtime Artifacts run + [29409257513](https://github.com/stablyai/orca/actions/runs/29409257513) +- Exact native build jobs and release-side suite results: + - Darwin arm64 job + [87332153920](https://github.com/stablyai/orca/actions/runs/29409257513/job/87332153920): + `macos-15` label, `macos-15-arm64` image `20260706.0213.1`, native ARM64; post-sign + metadata 4/4 in 623 ms; 40 files / 229 tests in 8.34 seconds; full build job PASS in 3m35s. + - Darwin x64 job + [87332153864](https://github.com/stablyai/orca/actions/runs/29409257513/job/87332153864): + `macos-15-intel` label, `macos-15` image `20260629.0276.1`, native X64; post-sign + metadata 4/4 in 1,673 ms; 40 files / 229 tests in 19.98 seconds; full build job PASS in 6m15s. + - Linux arm64 job + [87332153919](https://github.com/stablyai/orca/actions/runs/29409257513/job/87332153919): + `ubuntu-24.04-arm` image `20260714.61.1`, native ARM64; post-sign metadata 4/4 in 614 ms; + 40 files / 229 tests in 4.90 seconds; full build job PASS in 4m33s. + - Linux x64 job + [87332153898](https://github.com/stablyai/orca/actions/runs/29409257513/job/87332153898): + `ubuntu-24.04` image `20260705.232.1`, native X64; post-sign metadata 4/4 in 880 ms; + 40 files / 229 tests in 6.31 seconds; full build job PASS in 4m2s. + - Windows arm64 job + [87332153840](https://github.com/stablyai/orca/actions/runs/29409257513/job/87332153840): + `windows-11-arm` label, `windows-11-arm64` image `20260706.102.1`, native ARM64; + post-sign metadata 4/4 in 2,235 ms; 41 files / 224 passed / 9 skipped in 10.92 seconds; + full build job PASS in 8m37s. + - Windows x64 job + [87332153931](https://github.com/stablyai/orca/actions/runs/29409257513/job/87332153931): + `windows-2022` image `20260706.237.1`, native X64; post-sign metadata 4/4 in 1,773 ms; + 41 files / 224 passed / 9 skipped in 9.93 seconds; full build job PASS in 5m16s. +- Every build job also completed exact Node input verification, two clean native builds, archive + inspection, byte-for-byte reproducibility, bundled Node execution, real PTY input/resize/exit, + watcher lifecycle smoke, and unpublished Actions artifact evidence upload. +- Adjacent exact-head regressions: PR Checks + [29409257568](https://github.com/stablyai/orca/actions/runs/29409257568) PASS in 14m51s; + Golden E2E [29409257636](https://github.com/stablyai/orca/actions/runs/29409257636) PASS. +- Overall artifact-run result: expected FAIL only in Windows arm64 oldest-floor job + [87333789968](https://github.com/stablyai/orca/actions/runs/29409257513/job/87333789968): + hosted build 26200 does not equal required build 26100. Windows x64 oldest-floor and both Linux + oldest-userland supplemental jobs pass. This declared floor gap is independent of the six green + metadata/build jobs and still prevents tuple enablement. +- Oracle proved: each platform/architecture pair creates and semantically verifies post-sign SBOM + and provenance using its native runtime closure, archive family, immutable Node inputs, runner + identity, and exact platform toolchain. Both Windows ZIP fixtures pass strict declared type/mode + inspection without weakening production validation, replacing E-M4-POST-SIGN-METADATA-CI-RED-001. +- Does not prove: real Apple/SignPath returned bytes, native trust, exact macOS 13.5 or Windows arm64 + build-26100 execution, Linux kernel 4.18, protected manifest signing, publication/read-back, + desktop embedding, SSH transfer/install, or an enabled tuple. +- Checklist items satisfied: all-six exact-head native CI gate for semantic post-sign metadata. The + metadata package is closed; broader Work Package 3 and Milestone 4 remain open. +- Follow-up: add a credential-free reusable target-native build-prerequisite contract while keeping + release-cut, desktop builds, signing credentials, publication, and every tuple disconnected. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -10009,13 +10074,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the locally green platform-native fixture correction under -E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001, then collect replacement exact-head Node 24 proof from -all six native build jobs; do not merge to `main`. Retain every production/default gate and do not -connect release publication, production signing credentials, 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. +Add and RED/GREEN-test a credential-free reusable target-native build-prerequisite contract over +the existing six native jobs. Do not connect release-cut or any desktop build in this slice; the +Windows arm64 build-26100, macOS 13.5, Linux kernel 4.18, native signing/trust, aggregate, +publication/read-back, and embedding gates remain prerequisites to that connection. Do not merge to +`main`, publish, use production signing credentials, or enable a tuple. Retain every +production/default gate. In parallel, provision the qualifying exact-floor and real macOS/Windows +signing/trust environments. 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 From 994daefabcf9fba8d8d614bb6246d189c34c6b3f Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 07:31:33 -0400 Subject: [PATCH 118/267] ci(ssh): expose native runtime prerequisite Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 7 ++ ...-relay-runtime-build-prerequisite.test.mjs | 44 +++++++++ ...elease-implementation-checklist-summary.md | 5 +- ...github-release-implementation-checklist.md | 90 ++++++++++++++++--- 4 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 6baac3d15ed..2c522175769 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -15,6 +15,8 @@ on: - 'src/main/ipc/parcel-watcher-process-entry.ts' - 'src/relay/**' workflow_dispatch: + # Why: callers may gate on the native graph only after separate release wiring is reviewed. + workflow_call: {} concurrency: group: ssh-relay-runtime-artifacts-${{ github.event.pull_request.number || github.ref }} @@ -164,6 +166,7 @@ jobs: node --check config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs node --check config/scripts/ssh-relay-runtime-post-sign-metadata.mjs node --check config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs + node --check config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs pnpm exec vitest run --config config/vitest.config.ts \ config/scripts/ssh-relay-runtime-compatibility.test.mjs \ config/scripts/ssh-relay-node-release-verification.test.mjs \ @@ -195,6 +198,7 @@ jobs: config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs \ config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs \ config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs \ + config/scripts/ssh-relay-runtime-build-prerequisite.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 \ @@ -552,6 +556,8 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime post-sign metadata syntax check failed' } node --check config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime post-sign metadata test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime build prerequisite test syntax check failed' } pnpm exec vitest run --config config/vitest.config.ts ` config/scripts/ssh-relay-runtime-compatibility.test.mjs ` config/scripts/ssh-relay-node-release-verification.test.mjs ` @@ -583,6 +589,7 @@ jobs: config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs ` config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs ` config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs ` + config/scripts/ssh-relay-runtime-build-prerequisite.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-build-prerequisite.test.mjs b/config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs new file mode 100644 index 00000000000..415a585e5c6 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs @@ -0,0 +1,44 @@ +import { readFile } from 'node:fs/promises' + +import { parse } from 'yaml' +import { describe, expect, it } from 'vitest' + +const workflowUrl = new URL( + '../../.github/workflows/ssh-relay-runtime-artifacts.yml', + import.meta.url +) +const releaseCutUrl = new URL('../../.github/workflows/release-cut.yml', import.meta.url) +const releaseMacUrl = new URL('../../.github/workflows/release-mac-build.yml', import.meta.url) + +describe('SSH relay runtime reusable build prerequisite', () => { + it('exposes the exact credential-free native graph without a release consumer', async () => { + const [source, releaseCut, releaseMac] = await Promise.all([ + readFile(workflowUrl, 'utf8'), + readFile(releaseCutUrl, 'utf8'), + readFile(releaseMacUrl, 'utf8') + ]) + const workflow = parse(source) + + expect(workflow.on.workflow_call).toEqual({}) + expect(workflow.permissions).toEqual({ contents: 'read' }) + expect(Object.keys(workflow.jobs)).toEqual([ + 'build-posix-runtime', + 'build-windows-runtime', + 'verify-linux-runtime-baseline-userland', + 'verify-windows-runtime-baseline' + ]) + expect(workflow.jobs['verify-linux-runtime-baseline-userland'].needs).toBe( + 'build-posix-runtime' + ) + expect(workflow.jobs['verify-windows-runtime-baseline'].needs).toBe('build-windows-runtime') + for (const job of Object.values(workflow.jobs)) { + expect(job.steps[0].with.ref).toBe('${{ github.event.pull_request.head.sha || github.sha }}') + } + expect(source).not.toMatch(/\$\{\{\s*secrets\./u) + // Why: this slice proves a callable build graph without changing the production release DAG. + for (const consumer of [releaseCut, releaseMac]) { + expect(consumer).not.toContain('ssh-relay-runtime-artifacts.yml') + expect(consumer).not.toContain('ssh-relay-runtime-') + } + }) +}) 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 b3a6d38246d..eac76583e69 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 @@ -192,7 +192,10 @@ complete a box. - [ ] **In progress — 2026-07-15, Codex implementation owner:** add a reusable target-native runtime build prerequisite contract. Keep it disconnected from release-cut and every desktop build until exact floors, native signing/trust, aggregate, and publication gates are complete; no - tuple is enabled by this slice. + tuple is enabled by this slice. The purpose-named RED proves `workflow_call` was absent; the + credential-free interface and both native test-family integrations are locally green while + both release workflows remain disconnected (`E-M4-BUILD-PREREQUISITE-LOCAL-RED-001`, + `E-M4-BUILD-PREREQUISITE-LOCAL-001`). Exact-head all-six native CI remains required. - [ ] Add native signing jobs; hash only the returned signed bytes. - [ ] Add a fail-closed aggregate and immutable manifest-signing job. - [ ] Embed the exact signed manifest and accepted keys in each desktop build. 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 99b787fff58..aa6d5489fa7 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 reusable target-native build-prerequisite contract — **In progress — 2026-07-15, Codex implementation owner**. Semantic post-sign SBOM/provenance regeneration is closed locally and on all six exact-head Node 24 native build jobs under E-M4-POST-SIGN-METADATA-LOCAL-001, E-M4-POST-SIGN-METADATA-LOCAL-002, E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001, and E-M4-POST-SIGN-METADATA-CI-001. The active slice may define and test only a credential-free reusable target-native build-prerequisite interface. It must remain disconnected from release-cut and every desktop build until exact floors, native signing/trust, aggregate, publication/read-back, and embedding gates are complete. It may not publish, use production signing credentials, connect a desktop consumer, 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 run 29409257513 at `9e81229274a3aa9a0fdb4db537e853e1cc9699ca` passes the corrected four-test post-sign metadata suite, full release-side contract suite, native build-twice/equality, archive inspection, Node/PTY/watcher smoke, and unpublished evidence upload on all six native build jobs under E-M4-POST-SIGN-METADATA-CI-001. PR Checks 29409257568 and Golden E2E 29409257636 are green. The artifact run is red only for the declared Windows arm64 oldest-floor mismatch: the hosted image is build 26200 instead of required build 26100. The next slice is a disconnected reusable build-prerequisite contract, not release workflow activation. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, missing exact-floor snapshots, protected manifest signing, publication wiring, desktop embedding, and native trust remain separately gated. Nothing is published or enabled, legacy remains the production default, and merge to `main` remains prohibited.
      +Current phase: Milestone 4 / Work Package 3 reusable target-native build-prerequisite contract — **In progress — 2026-07-15, Codex implementation owner**. Semantic post-sign SBOM/provenance regeneration is closed locally and on all six exact-head Node 24 native build jobs under E-M4-POST-SIGN-METADATA-LOCAL-001, E-M4-POST-SIGN-METADATA-LOCAL-002, E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001, and E-M4-POST-SIGN-METADATA-CI-001. The active slice defines and tests only a credential-free reusable target-native build-prerequisite interface. Its purpose-named missing-interface RED and local GREEN are recorded under E-M4-BUILD-PREREQUISITE-LOCAL-RED-001 and E-M4-BUILD-PREREQUISITE-LOCAL-001; exact-head proof on all six native build jobs is still required. It must remain disconnected from release-cut and every desktop build until exact floors, native signing/trust, aggregate, publication/read-back, and embedding gates are complete. It may not publish, use production signing credentials, connect a desktop consumer, 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** — the credential-free `workflow_call` interface, exact four-job graph, read-only permissions, secret-free boundary, exact checkout revision, and release-workflow disconnection contract are locally green under E-M4-BUILD-PREREQUISITE-LOCAL-001. The full 234-test release suite, 48-test desktop parity suite, typecheck, lint/reliability/localization gates, formatting, syntax, and diff checks pass; both user-owned Node/npm resolver files have zero diff. The package remains open pending exact-head all-six native CI. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, exact macOS 13.5/Linux kernel 4.18/Windows arm64 build-26100 runners, protected manifest signing, publication wiring, desktop embedding, and native trust remain separately gated. Nothing is published or enabled, release-cut and both desktop release consumers remain disconnected, 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) @@ -99,7 +99,10 @@ same change as the work it records. test a credential-free reusable interface over the existing target-native jobs, but do not connect release-cut or a desktop build until exact-floor, native signing/trust, aggregate, publication/read-back, and embedding gates are complete. No broader Milestone 4 box is checked - until its complete evidence exists. + until its complete evidence exists. E-M4-BUILD-PREREQUISITE-LOCAL-RED-001 proves the callable + interface was absent; E-M4-BUILD-PREREQUISITE-LOCAL-001 proves the local credential-free + implementation and both native test-family integrations while release workflows stay disconnected. + Exact-head all-six native CI remains required. - 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 @@ -10017,6 +10020,73 @@ diff --check`. - Follow-up: add a credential-free reusable target-native build-prerequisite contract while keeping release-cut, desktop builds, signing credentials, publication, and every tuple disconnected. +### E-M4-BUILD-PREREQUISITE-LOCAL-RED-001 — Reusable native build interface is absent + +- Date: 2026-07-15 +- Commit SHA / PR: uncommitted purpose-named test atop + `e7f447490`; draft PR #8741 +- Runner: macOS 26.2 arm64, native local worktree; Node v26.0.0 and pnpm 10.24.0 +- Remote: not applicable; static credential-free workflow contract +- Transport/network: local workflow files only; no Actions invocation, release-cut call, signer, + SSH host, publication, desktop consumer, production/default call, or enabled tuple +- Command: + `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs` +- Result: expected FAIL, 1 file / 1 test in 4.52 seconds. The exact assertion receives `undefined` + instead of an empty `workflow_call` contract. +- Oracle proved: the purpose-named reusable interface is absent, while the test source also defines + the required exact four-job native/baseline graph, read-only permissions, secret-free boundary, + exact source checkout, and release-cut/macOS-release disconnection contract. +- Does not prove: implementation, Actions schema acceptance, native-runner execution, production + release prerequisite behavior, signing, publication, desktop embedding, SSH behavior, or an + enabled tuple. +- Checklist items satisfied: RED prerequisite for the active reusable build-prerequisite package. +- Follow-up: expose the existing workflow through a credential-free `workflow_call`, wire this test + into both native contract-test families, and run focused/broad local gates before exact-head CI. + +### E-M4-BUILD-PREREQUISITE-LOCAL-001 — Reusable native build interface is locally green + +- Date: 2026-07-15 +- Commit SHA / PR: uncommitted implementation atop + `e7f4474908af895cd9fbaccfc575d89f7ed6a6b2`; draft PR #8741 +- Runner: macOS 26.2 arm64, native local worktree; Node v26.0.0 and pnpm 10.24.0 +- Remote: not applicable; static credential-free workflow contract and local regression suites +- Transport/network: local workflow/source files only; no Actions invocation, release-cut call, + signer, SSH host, publication, desktop consumer, production/default call, or enabled tuple +- Commands and results: + - Focused prerequisite/workflow contract: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` + — PASS, 2 files / 7 tests in 12.55 seconds test duration / 24.25 seconds wall; + 131,104,768-byte maximum RSS, 96,540,048-byte peak memory footprint, and zero swaps. + - 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, 42 files / 234 tests in 245.04 seconds test duration / 278.86 seconds wall; + 185,352,192-byte maximum RSS and zero swaps. The unusually long local duration coincided with + severe host CPU contention; the exact-head native jobs remain the authoritative timing cells. + - Desktop schema/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 7.69 seconds test duration / 28.07 seconds wall; + 124,895,232-byte maximum RSS, 95,950,248-byte peak memory footprint, and zero swaps. + - `/usr/bin/time -l pnpm run typecheck` — PASS in 20.82 seconds wall with + 1,253,785,600-byte maximum RSS. + - `/usr/bin/time -l pnpm run lint` — PASS in 401.52 seconds wall with + 1,685,323,776-byte maximum RSS and zero swaps; all 41 reliability gates, the 355-entry max-lines + ratchet, bundled-skill guides, localization catalog/parity, and localization coverage pass. All + 26 warnings are in untouched existing files. + - Focused `pnpm exec oxfmt --check`, `node --check`, `git diff --check`, and zero diff in both + user-owned Node/npm resolver files — PASS. +- Oracle proved: the existing exact four-job native/baseline graph is callable with an empty, + credential-free `workflow_call` interface; workflow permissions remain read-only, no job consumes + a secret, every job checks out the exact triggering SHA, and the test executes from both POSIX and + Windows native contract-test lists. `release-cut.yml` and `release-mac-build.yml` remain + disconnected, so this package cannot alter desktop release or product behavior. +- Does not prove: GitHub Actions schema acceptance, execution on any native runner, production + release prerequisite behavior, signing, exact oldest-floor execution, publication, desktop + embedding, SSH behavior, or an enabled tuple. +- Checklist items satisfied: local implementation/static/regression gate for the active reusable + build-prerequisite package. The package remains open pending all-six exact-head native CI. +- Follow-up: commit and push, then require the new prerequisite contract to pass in every native + build job; retain the known Windows arm64 build-26100 floor failure as a separate enablement gap. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -10074,13 +10144,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Add and RED/GREEN-test a credential-free reusable target-native build-prerequisite contract over -the existing six native jobs. Do not connect release-cut or any desktop build in this slice; the -Windows arm64 build-26100, macOS 13.5, Linux kernel 4.18, native signing/trust, aggregate, -publication/read-back, and embedding gates remain prerequisites to that connection. Do not merge to -`main`, publish, use production signing credentials, or enable a tuple. Retain every -production/default gate. In parallel, provision the qualifying exact-floor and real macOS/Windows -signing/trust environments. +Commit and push the locally green credential-free reusable build-prerequisite package, then require +its purpose-named contract to pass on all six exact-head native build jobs plus adjacent PR Checks +and Golden E2E. Do not connect release-cut or any desktop build in this slice; the Windows arm64 +build-26100, macOS 13.5, Linux kernel 4.18, native signing/trust, aggregate, publication/read-back, +and embedding gates remain prerequisites to that connection. Do not merge to `main`, publish, use +production signing credentials, or enable a tuple. Retain every production/default gate. In +parallel, provision the qualifying exact-floor and real macOS/Windows signing/trust environments. 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 From 48ee5f5bbf4ce2d0cb64a9a1657cfafa27463b84 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 07:51:54 -0400 Subject: [PATCH 119/267] docs(ssh): record native prerequisite proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 8 +- ...github-release-implementation-checklist.md | 118 ++++++++++++++---- 2 files changed, 102 insertions(+), 24 deletions(-) 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 eac76583e69..40c8bfb135e 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 @@ -189,14 +189,16 @@ complete a box. `E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001`; production type/mode validation is unchanged. Exact-head execution passes on all six native jobs under `E-M4-POST-SIGN-METADATA-CI-001`. -- [ ] **In progress — 2026-07-15, Codex implementation owner:** add a reusable target-native runtime +- [x] Add a reusable target-native runtime build prerequisite contract. Keep it disconnected from release-cut and every desktop build until exact floors, native signing/trust, aggregate, and publication gates are complete; no tuple is enabled by this slice. The purpose-named RED proves `workflow_call` was absent; the credential-free interface and both native test-family integrations are locally green while both release workflows remain disconnected (`E-M4-BUILD-PREREQUISITE-LOCAL-RED-001`, - `E-M4-BUILD-PREREQUISITE-LOCAL-001`). Exact-head all-six native CI remains required. -- [ ] Add native signing jobs; hash only the returned signed bytes. + `E-M4-BUILD-PREREQUISITE-LOCAL-001`, `E-M4-BUILD-PREREQUISITE-CI-001`). +- [ ] **In progress — 2026-07-15, Codex implementation owner:** add platform-native signing jobs; + hash only the returned signed bytes. Begin with a purpose-named disconnected workflow RED; + no test double counts as real signing/native-trust evidence. - [ ] Add a fail-closed aggregate and immutable manifest-signing job. - [ ] 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. 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 aa6d5489fa7..0527aa52a56 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 reusable target-native build-prerequisite contract — **In progress — 2026-07-15, Codex implementation owner**. Semantic post-sign SBOM/provenance regeneration is closed locally and on all six exact-head Node 24 native build jobs under E-M4-POST-SIGN-METADATA-LOCAL-001, E-M4-POST-SIGN-METADATA-LOCAL-002, E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001, and E-M4-POST-SIGN-METADATA-CI-001. The active slice defines and tests only a credential-free reusable target-native build-prerequisite interface. Its purpose-named missing-interface RED and local GREEN are recorded under E-M4-BUILD-PREREQUISITE-LOCAL-RED-001 and E-M4-BUILD-PREREQUISITE-LOCAL-001; exact-head proof on all six native build jobs is still required. It must remain disconnected from release-cut and every desktop build until exact floors, native signing/trust, aggregate, publication/read-back, and embedding gates are complete. It may not publish, use production signing credentials, connect a desktop consumer, 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** — the credential-free `workflow_call` interface, exact four-job graph, read-only permissions, secret-free boundary, exact checkout revision, and release-workflow disconnection contract are locally green under E-M4-BUILD-PREREQUISITE-LOCAL-001. The full 234-test release suite, 48-test desktop parity suite, typecheck, lint/reliability/localization gates, formatting, syntax, and diff checks pass; both user-owned Node/npm resolver files have zero diff. The package remains open pending exact-head all-six native CI. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, exact macOS 13.5/Linux kernel 4.18/Windows arm64 build-26100 runners, protected manifest signing, publication wiring, desktop embedding, and native trust remain separately gated. Nothing is published or enabled, release-cut and both desktop release consumers remain disconnected, legacy remains the production default, and merge to `main` remains prohibited.
      +Current phase: Milestone 4 / Work Package 3 platform-native signing-job workflow contract — **In progress — 2026-07-15, Codex implementation owner**. The reusable credential-free target-native build-prerequisite package is closed locally and on all six exact-head Node 24 native build jobs under E-M4-BUILD-PREREQUISITE-LOCAL-RED-001, E-M4-BUILD-PREREQUISITE-LOCAL-001, and E-M4-BUILD-PREREQUISITE-CI-001. The active slice begins with a purpose-named RED for disconnected platform-native signing jobs that consume exact native build outputs and return immutable signed bytes before final hashing. A credential-free test path may prove workflow shape and failure behavior but cannot satisfy real Apple/SignPath signing, returned production signatures, or native-trust evidence. Release-cut and every desktop build remain disconnected until exact floors, real signing/trust, aggregate, publication/read-back, and embedding gates are complete. The slice may not publish, connect a desktop consumer, 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 run 29412010582 at `994daefabcf9fba8d8d614bb6246d189c34c6b3f` passes the new reusable prerequisite contract, full release-side suite, native build-twice/equality, archive inspection, bundled Node/PTY/watcher smoke, and unpublished evidence upload on all six native build jobs under E-M4-BUILD-PREREQUISITE-CI-001. PR Checks 29412010603 pass. Golden E2E 29412010624 passes on macOS first-attempt and Linux retry; the first Linux attempt failed before tests while compiling `node-pty`, then the unchanged retry passed dependency install, app build, and Golden tests. The artifact run is red only for the declared Windows arm64 oldest-floor mismatch: hosted build 26200 does not equal required build 26100. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, exact macOS 13.5/Linux kernel 4.18/Windows arm64 build-26100 runners, protected manifest signing, publication wiring, desktop embedding, and native trust remain separately gated. Nothing is published or enabled, release-cut and both desktop release consumers remain disconnected, 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) @@ -95,14 +95,15 @@ same change as the work it records. E-M4-POST-SIGN-METADATA-CORRECTION-LOCAL-001. Replacement exact-head Node 24 evidence passes all six native build jobs under E-M4-POST-SIGN-METADATA-CI-001. Production signing credentials, publication, desktop consumers, and tuple enablement remain outside this slice. -- Active package: Work Package 3 reusable target-native build-prerequisite contract. Define and - test a credential-free reusable interface over the existing target-native jobs, but do not connect - release-cut or a desktop build until exact-floor, native signing/trust, aggregate, - publication/read-back, and embedding gates are complete. No broader Milestone 4 box is checked - until its complete evidence exists. E-M4-BUILD-PREREQUISITE-LOCAL-RED-001 proves the callable - interface was absent; E-M4-BUILD-PREREQUISITE-LOCAL-001 proves the local credential-free - implementation and both native test-family integrations while release workflows stay disconnected. - Exact-head all-six native CI remains required. +- Completed package: Work Package 3 reusable target-native build-prerequisite contract. The + credential-free callable interface, both native test-family integrations, and exact-head all-six + native CI pass under E-M4-BUILD-PREREQUISITE-LOCAL-RED-001, + E-M4-BUILD-PREREQUISITE-LOCAL-001, and E-M4-BUILD-PREREQUISITE-CI-001. Release workflows remain + disconnected. +- Active package: Work Package 3 platform-native signing-job workflow contract. Begin with a + purpose-named RED for exact native build-input consumption, immutable returned signed bytes, + post-return hashing, bounded failure behavior, and release-consumer disconnection. Credential-free + tests do not count as real Apple/SignPath signing or native-trust evidence; keep those gates open. - 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 @@ -948,13 +949,16 @@ Rules: ## Milestone 4 — Make Relay Artifacts Prerequisites of Desktop Builds -- [ ] **In progress — 2026-07-15, Codex implementation owner:** add a reusable target-native runtime +- [x] Add a reusable target-native runtime build prerequisite contract for every candidate tuple; do not treat a cross-build alone as native execution evidence. Keep release-cut and desktop builds disconnected until the remaining exact-floor, signing/trust, aggregate, publication/read-back, and embedding gates - pass. -- [ ] Add platform-native signing jobs. Signed macOS and Windows runtime bytes must return as - immutable outputs before final hashes are computed. + pass. (E-M4-BUILD-PREREQUISITE-LOCAL-RED-001, + E-M4-BUILD-PREREQUISITE-LOCAL-001, E-M4-BUILD-PREREQUISITE-CI-001) +- [ ] **In progress — 2026-07-15, Codex implementation owner:** add platform-native signing jobs. + Signed macOS and Windows runtime bytes must return as immutable outputs before final hashes + are computed. Start with a disconnected purpose-named RED; real Apple/SignPath and native + trust remain required before this box can close. - [ ] Add one fail-closed aggregate job that waits for all required native build/signing outputs, validates them, computes final hashes, builds the canonical manifest, and signs it. - [ ] Make Linux, Windows, and macOS desktop build jobs depend on the successful aggregate output. @@ -1583,6 +1587,10 @@ focused commands as their scripts/tests are introduced. - [x] Exact-head Node 24 native workflow execution of `config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs` on all six runner families (E-M4-POST-SIGN-METADATA-CI-001) +- [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` (E-M4-BUILD-PREREQUISITE-LOCAL-RED-001, E-M4-BUILD-PREREQUISITE-LOCAL-001) +- [x] Exact-head Node 24 native workflow execution of + `config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs` on all six runner families + (E-M4-BUILD-PREREQUISITE-CI-001) - [x] `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay*.test.mjs` (E-M4-MANIFEST-HANDOFF-LOCAL-002; all release-side SSH-relay contracts) - [x] `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` (E-M4-MANIFEST-HANDOFF-LOCAL-002; desktop schema/signature parity) @@ -10087,6 +10095,72 @@ diff --check`. - Follow-up: commit and push, then require the new prerequisite contract to pass in every native build job; retain the known Windows arm64 build-26100 floor failure as a separate enablement gap. +### E-M4-BUILD-PREREQUISITE-CI-001 — Reusable prerequisite passes all six native build jobs + +- Date: 2026-07-15 +- Commit SHA / PR: `994daefabcf9fba8d8d614bb6246d189c34c6b3f`; draft PR #8741 +- Workflow/run: SSH Relay Runtime Artifacts run + [29412010582](https://github.com/stablyai/orca/actions/runs/29412010582) +- Exact native build jobs and prerequisite/release-suite results: + - Darwin arm64 job + [87341066776](https://github.com/stablyai/orca/actions/runs/29412010582/job/87341066776): + `macos-15` label, `macos-15-arm64` image `20260706.0213.1`, native ARM64; prerequisite + 1/1 in 24 ms; 41 files / 230 tests in 7.96 seconds; full build job PASS in 3m7s. + - Darwin x64 job + [87341066764](https://github.com/stablyai/orca/actions/runs/29412010582/job/87341066764): + `macos-15-intel` label, `macos-15` image `20260629.0276.1`, native X64; prerequisite + 1/1 in 46 ms; 41 files / 230 tests in 24.02 seconds; full build job PASS in 6m56s. + - Linux arm64 job + [87341066762](https://github.com/stablyai/orca/actions/runs/29412010582/job/87341066762): + `ubuntu-24.04-arm` label, image `20260714.61.1`, native ARM64; prerequisite 1/1 in 25 ms; + 41 files / 230 tests in 4.57 seconds; full build job PASS in 4m31s. + - Linux x64 job + [87341066775](https://github.com/stablyai/orca/actions/runs/29412010582/job/87341066775): + `ubuntu-24.04` label, image `20260705.232.1`, native X64; prerequisite 1/1 in 58 ms; + 41 files / 230 tests in 6.11 seconds; full build job PASS in 3m39s. + - Windows arm64 job + [87341066730](https://github.com/stablyai/orca/actions/runs/29412010582/job/87341066730): + `windows-11-arm` label, `windows-11-arm64` image `20260706.102.1`, native ARM64; + prerequisite 1/1 in 31 ms; 42 files / 225 passed / 9 skipped in 13.29 seconds; full build job + PASS in 8m51s. + - Windows x64 job + [87341066835](https://github.com/stablyai/orca/actions/runs/29412010582/job/87341066835): + `windows-2022` image `20260706.237.1`, native X64; prerequisite 1/1 in 48 ms; 42 files / + 225 passed / 9 skipped in 11.55 seconds; full build job PASS in 5m32s. +- Every build job checked out the exact source SHA and completed exact Node input verification, two + clean native builds, archive inspection, byte-for-byte reproducibility, bundled Node execution, + real PTY input/resize/exit, watcher lifecycle smoke, and unpublished evidence upload. The two + Linux oldest-userland supplemental jobs also pass: arm64 job + [87342399226](https://github.com/stablyai/orca/actions/runs/29412010582/job/87342399226) + in 58 seconds and x64 job + [87342399251](https://github.com/stablyai/orca/actions/runs/29412010582/job/87342399251) + in 1m2s. Windows x64 oldest-floor job + [87342761575](https://github.com/stablyai/orca/actions/runs/29412010582/job/87342761575) + passes in 1m35s. +- Adjacent exact-head regressions: PR Checks + [29412010603](https://github.com/stablyai/orca/actions/runs/29412010603) PASS in 13m49s. + Golden E2E [29412010624](https://github.com/stablyai/orca/actions/runs/29412010624) PASS after one + failed-job retry: macOS job 87341066363 passed first-attempt in 3m55s; Linux first-attempt job + 87341066356 failed during dependency installation while compiling `node-pty`, before app build or + tests, while the immediately preceding exact head was green. The unchanged retry job 87343161455 + passed dependency installation, app build, and Linux Golden tests in 4m29s. +- Overall artifact-run result: expected FAIL only in Windows arm64 oldest-floor job + [87342761223](https://github.com/stablyai/orca/actions/runs/29412010582/job/87342761223): + the contract requires build 26100 while the hosted runner reports `10.0.26200`. Platform and + architecture checks pass; the OS-build check fails closed. This declared floor gap remains a + tuple-enablement blocker independent of the six green build/prerequisite jobs. +- Oracle proved: GitHub accepts the credential-free callable interface and all six native jobs + execute its purpose-named contract. The exact four-job graph, read-only permissions, secret-free + boundary, exact source checkout, and release-workflow disconnection invariants hold on macOS, + Linux, and Windows x64/arm64. The callable graph still has no release or desktop consumer. +- Does not prove: a production release caller, real Apple/SignPath returned bytes, native trust, + exact macOS 13.5/Linux kernel 4.18/Windows arm64 build-26100 execution, protected manifest signing, + publication/read-back, desktop embedding, SSH transfer/install, or an enabled tuple. +- Checklist items satisfied: reusable target-native runtime build-prerequisite contract. Broader + Work Package 3 and Milestone 4 remain open. +- Follow-up: add a disconnected purpose-named platform-native signing-job workflow RED while + preserving every real-signing/native-trust, release-consumer, publication, and tuple gate. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -10144,13 +10218,15 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the locally green credential-free reusable build-prerequisite package, then require -its purpose-named contract to pass on all six exact-head native build jobs plus adjacent PR Checks -and Golden E2E. Do not connect release-cut or any desktop build in this slice; the Windows arm64 -build-26100, macOS 13.5, Linux kernel 4.18, native signing/trust, aggregate, publication/read-back, -and embedding gates remain prerequisites to that connection. Do not merge to `main`, publish, use -production signing credentials, or enable a tuple. Retain every production/default gate. In -parallel, provision the qualifying exact-floor and real macOS/Windows signing/trust environments. +Add a purpose-named RED for disconnected platform-native signing jobs that consume the exact native +build outputs, return immutable signed bytes before final hashes, settle bounded failure/timeout +paths, and remain absent from release-cut and desktop consumers. Credential-free test doubles may +prove workflow shape but cannot satisfy real Apple/SignPath signing or native trust. Do not connect +release-cut or any desktop build; Windows arm64 build-26100, macOS 13.5, Linux kernel 4.18, real +signing/trust, aggregate, publication/read-back, and embedding remain prerequisites. Do not merge to +`main`, publish, use production signing credentials, or enable a tuple. Retain every +production/default gate. In parallel, provision the qualifying exact-floor and real macOS/Windows +signing/trust environments. 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 From d8d74d21442778e9bac672112528067542d19817 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 08:23:00 -0400 Subject: [PATCH 120/267] feat(ssh): finalize signed runtime artifacts Co-authored-by: Orca --- .../ssh-relay-runtime-archive-extraction.mjs | 228 +++++++++++ ...-relay-runtime-archive-extraction.test.mjs | 134 +++++++ .../ssh-relay-runtime-macos-signing.mjs | 115 ++++++ .../ssh-relay-runtime-macos-signing.test.mjs | 135 +++++++ ...ay-runtime-native-signing-finalization.mjs | 363 ++++++++++++++++++ ...ntime-native-signing-finalization.test.mjs | 192 +++++++++ ...h-relay-runtime-native-signing-payload.mjs | 34 ++ ...ay-runtime-native-signing-stage-report.mjs | 73 ++++ config/scripts/ssh-relay-runtime-zip.mjs | 57 ++- 9 files changed, 1330 insertions(+), 1 deletion(-) create mode 100644 config/scripts/ssh-relay-runtime-archive-extraction.mjs create mode 100644 config/scripts/ssh-relay-runtime-archive-extraction.test.mjs create mode 100644 config/scripts/ssh-relay-runtime-macos-signing.mjs create mode 100644 config/scripts/ssh-relay-runtime-macos-signing.test.mjs create mode 100644 config/scripts/ssh-relay-runtime-native-signing-finalization.mjs create mode 100644 config/scripts/ssh-relay-runtime-native-signing-finalization.test.mjs create mode 100644 config/scripts/ssh-relay-runtime-native-signing-stage-report.mjs diff --git a/config/scripts/ssh-relay-runtime-archive-extraction.mjs b/config/scripts/ssh-relay-runtime-archive-extraction.mjs new file mode 100644 index 00000000000..cd07a2b3a0a --- /dev/null +++ b/config/scripts/ssh-relay-runtime-archive-extraction.mjs @@ -0,0 +1,228 @@ +import { spawn } from 'node:child_process' +import { createHash } from 'node:crypto' +import { createReadStream } from 'node:fs' +import { lstat, mkdir, realpath, rm } from 'node:fs/promises' +import { basename, dirname, resolve } from 'node:path' +import { pipeline } from 'node:stream/promises' +import { pathToFileURL } from 'node:url' + +import { extract } from 'tar' + +import { inspectSshRelayRuntimeArchive } from './ssh-relay-runtime-archive.mjs' +import { assertSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' +import { readSshRelayRuntimeNativeSigningIdentity } from './ssh-relay-runtime-native-signing-plan.mjs' +import { extractSshRelayRuntimeZip } from './ssh-relay-runtime-zip.mjs' +import { verifyRuntimeTree } from './verify-ssh-relay-runtime.mjs' + +const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024 +const EXTRACTION_TIMEOUT_MS = 5 * 60_000 +const MAX_DIAGNOSTIC_BYTES = 64 * 1024 + +function expectedArchiveName(identity) { + const extension = identity.os === 'win32' ? 'zip' : 'tar.xz' + return `orca-ssh-relay-runtime-v1-${identity.tupleId}-${identity.contentId.slice('sha256:'.length)}.${extension}` +} + +function sourceIdentity(identity) { + const { archive: _archive, ...candidate } = identity + return candidate +} + +function assertIdentityArchive(identity, archivePath) { + assertSshRelayRuntimeClosureEntries(identity) + const candidate = sourceIdentity(identity) + if ( + computeSshRelayRuntimeContentId(candidate) !== identity.contentId || + identity.archive?.name !== expectedArchiveName(identity) || + basename(archivePath) !== identity.archive.name || + !Number.isSafeInteger(identity.archive.size) || + identity.archive.size <= 0 || + identity.archive.size > MAX_ARCHIVE_BYTES || + identity.archive.expandedSize !== identity.expandedSize || + identity.archive.fileCount !== identity.fileCount || + !/^sha256:[0-9a-f]{64}$/u.test(identity.archive.sha256 ?? '') + ) { + throw new Error('Runtime archive extraction identity or archive reference is inconsistent') + } + return candidate +} + +function sameFileState(left, right) { + return ( + left.dev === right.dev && + left.ino === right.ino && + left.size === right.size && + left.mtimeNs === right.mtimeNs && + left.ctimeNs === right.ctimeNs + ) +} + +async function describeArchive(path, maximumBytes, signal) { + signal.throwIfAborted() + const before = await lstat(path, { bigint: true }) + if ( + !before.isFile() || + before.isSymbolicLink() || + before.size <= 0n || + before.size > BigInt(maximumBytes) + ) { + throw new Error('Runtime archive extraction input must be one bounded regular file') + } + const hash = createHash('sha256') + let size = 0 + for await (const chunk of createReadStream(path, { signal })) { + size += chunk.length + if (size > maximumBytes) { + throw new Error('Runtime archive extraction input exceeds its size bound') + } + hash.update(chunk) + } + const after = await lstat(path, { bigint: true }) + if (!sameFileState(before, after) || BigInt(size) !== before.size) { + throw new Error('Runtime archive extraction input changed while hashing') + } + return { size, sha256: `sha256:${hash.digest('hex')}`, state: after } +} + +async function assertOutputAbsent(outputDirectory) { + try { + await lstat(outputDirectory) + } catch (error) { + if (error.code === 'ENOENT') { + return + } + throw error + } + throw new Error('Runtime archive extraction requires an exclusive output directory') +} + +async function extractTarXz(archivePath, outputDirectory, signal) { + const child = spawn('xz', ['--decompress', '--stdout', '--single-stream', '--', archivePath], { + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + signal + }) + let stderr = '' + child.stderr.setEncoding('utf8') + child.stderr.on('data', (chunk) => { + stderr = `${stderr}${chunk}`.slice(-MAX_DIAGNOSTIC_BYTES) + }) + const completion = new Promise((resolvePromise, reject) => { + child.once('error', reject) + child.once('close', (code, closeSignal) => { + if (code === 0) { + resolvePromise() + } else { + reject(new Error(`xz extraction failed (${code ?? closeSignal ?? 'unknown'}): ${stderr}`)) + } + }) + }) + const unpack = extract({ + cwd: outputDirectory, + strict: true, + preserveOwner: false, + noChmod: false, + unlink: false + }) + try { + await Promise.all([pipeline(child.stdout, unpack, { signal }), completion]) + } catch (error) { + if (child.exitCode === null && child.signalCode === null) { + child.kill() + } + await completion.catch(() => {}) + throw error + } +} + +export async function extractSshRelayRuntimeArchive({ + archivePath, + outputDirectory, + identity, + signal +}) { + const effectiveSignal = signal + ? AbortSignal.any([signal, AbortSignal.timeout(EXTRACTION_TIMEOUT_MS)]) + : AbortSignal.timeout(EXTRACTION_TIMEOUT_MS) + effectiveSignal.throwIfAborted() + const absoluteArchive = resolve(archivePath) + const absoluteOutput = resolve(outputDirectory) + const finalIdentity = assertIdentityArchive(identity, absoluteArchive) + const physicalParent = await realpath(dirname(absoluteOutput)) + const physicalOutput = resolve(physicalParent, basename(absoluteOutput)) + await assertOutputAbsent(physicalOutput) + const before = await describeArchive(absoluteArchive, identity.archive.size, effectiveSignal) + if (before.size !== identity.archive.size || before.sha256 !== identity.archive.sha256) { + throw new Error('Runtime archive extraction input size or digest mismatch') + } + await inspectSshRelayRuntimeArchive(absoluteArchive, finalIdentity, { signal: effectiveSignal }) + + let outputCreated = false + try { + await mkdir(physicalOutput) + outputCreated = true + await (identity.os === 'win32' + ? extractSshRelayRuntimeZip({ + archivePath: absoluteArchive, + outputDirectory: physicalOutput, + identity: finalIdentity, + signal: effectiveSignal + }) + : extractTarXz(absoluteArchive, physicalOutput, effectiveSignal)) + const tree = await verifyRuntimeTree(physicalOutput, finalIdentity) + const after = await describeArchive(absoluteArchive, identity.archive.size, effectiveSignal) + if (!sameFileState(before.state, after.state) || before.sha256 !== after.sha256) { + throw new Error('Runtime archive extraction input changed during extraction') + } + return { tupleId: identity.tupleId, runtimeRoot: physicalOutput, tree } + } catch (error) { + if (outputCreated) { + // Why: only a completely verified reconstruction may cross the native-signing boundary. + await rm(physicalOutput, { recursive: true, force: true }) + } + throw error + } +} + +const ARGUMENT_FIELDS = new Map([ + ['--identity', 'identityPath'], + ['--archive', 'archivePath'], + ['--output-directory', 'outputDirectory'] +]) + +export function parseSshRelayRuntimeArchiveExtractionArguments(argv) { + const result = {} + for (let index = 0; index < argv.length; index += 2) { + const flag = argv[index] + const field = ARGUMENT_FIELDS.get(flag) + const value = argv[index + 1] + if (!field || !value || value.startsWith('--') || result[field]) { + throw new Error(`Invalid runtime archive extraction argument: ${flag}`) + } + result[field] = resolve(value) + } + if (Object.keys(result).length !== ARGUMENT_FIELDS.size) { + throw new Error('Runtime archive extraction requires identity, archive, and output directory') + } + return result +} + +async function main() { + const options = parseSshRelayRuntimeArchiveExtractionArguments(process.argv.slice(2)) + const identity = await readSshRelayRuntimeNativeSigningIdentity(options.identityPath) + const result = await extractSshRelayRuntimeArchive({ ...options, identity }) + 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 archive extraction failed: ${error.stack ?? error}\n`) + process.exitCode = 1 + }) +} + +export const SSH_RELAY_RUNTIME_ARCHIVE_EXTRACTION_LIMITS = Object.freeze({ + maximumArchiveBytes: MAX_ARCHIVE_BYTES, + timeoutMs: EXTRACTION_TIMEOUT_MS +}) diff --git a/config/scripts/ssh-relay-runtime-archive-extraction.test.mjs b/config/scripts/ssh-relay-runtime-archive-extraction.test.mjs new file mode 100644 index 00000000000..c5843413d2c --- /dev/null +++ b/config/scripts/ssh-relay-runtime-archive-extraction.test.mjs @@ -0,0 +1,134 @@ +import { createHash } from 'node:crypto' +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { createSshRelayRuntimeArchive } from './ssh-relay-runtime-archive.mjs' +import { extractSshRelayRuntimeArchive } from './ssh-relay-runtime-archive-extraction.mjs' +import { expectedSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' + +const temporaryDirectories = [] +const SOURCE_DATE_EPOCH = 1_788_739_200 + +function sha256(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +async function fixture(tupleId) { + const root = await mkdtemp(join(tmpdir(), 'ssh-relay-runtime-extraction-')) + temporaryDirectories.push(root) + const runtimeRoot = join(root, 'source-runtime') + const archiveRoot = join(root, 'archive') + await Promise.all([mkdir(runtimeRoot), mkdir(archiveRoot)]) + const entries = [] + for (const entry of expectedSshRelayRuntimeClosureEntries(tupleId)) { + const path = join(runtimeRoot, ...entry.path.split('/')) + if (entry.type === 'directory') { + await mkdir(path, { recursive: true, mode: entry.mode }) + entries.push(entry) + continue + } + const bytes = Buffer.from(`runtime extraction fixture:${tupleId}:${entry.path}`) + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, bytes, { mode: entry.mode }) + entries.push({ ...entry, size: bytes.length, sha256: sha256(bytes) }) + } + const os = tupleId.startsWith('win32-') ? 'win32' : 'darwin' + const base = { + identitySchemaVersion: 1, + tupleId, + os, + architecture: tupleId.includes('arm64') ? 'arm64' : 'x64', + compatibility: sshRelayRuntimeCompatibility[tupleId], + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries + } + const files = entries.filter((entry) => entry.type === 'file') + const identity = { + ...base, + contentId: computeSshRelayRuntimeContentId(base), + fileCount: files.length, + expandedSize: files.reduce((total, entry) => total + entry.size, 0) + } + const archive = await createSshRelayRuntimeArchive({ + runtimeRoot, + outputDirectory: archiveRoot, + identity, + sourceDateEpoch: SOURCE_DATE_EPOCH + }) + return { + root, + runtimeRoot, + archive, + identity: { + ...identity, + archive: { + name: archive.name, + size: archive.size, + expandedSize: identity.expandedSize, + fileCount: identity.fileCount, + sha256: archive.sha256 + } + } + } +} + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })) + ) +}) + +describe('SSH relay runtime archive extraction', () => { + it('reconstructs exact POSIX and Windows runtime trees into exclusive directories', async () => { + for (const tupleId of ['darwin-arm64', 'win32-x64']) { + const value = await fixture(tupleId) + const outputDirectory = join(value.root, 'reconstructed') + const result = await extractSshRelayRuntimeArchive({ + archivePath: value.archive.path, + outputDirectory, + identity: value.identity + }) + + expect(result.tupleId).toBe(tupleId) + expect(result.tree.contentId).toBe(value.identity.contentId) + expect(await readFile(join(outputDirectory, 'relay.js'))).toEqual( + await readFile(join(value.runtimeRoot, 'relay.js')) + ) + } + }) + + it('rejects modified archives and existing outputs without retaining partial trees', async () => { + const modified = await fixture('darwin-x64') + await writeFile(modified.archive.path, 'not the authenticated archive') + const rejectedOutput = join(modified.root, 'rejected') + await expect( + extractSshRelayRuntimeArchive({ + archivePath: modified.archive.path, + outputDirectory: rejectedOutput, + identity: modified.identity + }) + ).rejects.toThrow(/size|digest|archive/i) + await expect(readFile(join(rejectedOutput, 'relay.js'))).rejects.toMatchObject({ + code: 'ENOENT' + }) + + const existing = await fixture('win32-x64') + const existingOutput = join(existing.root, 'existing') + await mkdir(existingOutput) + await expect( + extractSshRelayRuntimeArchive({ + archivePath: existing.archive.path, + outputDirectory: existingOutput, + identity: existing.identity + }) + ).rejects.toThrow(/exclusive/i) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-macos-signing.mjs b/config/scripts/ssh-relay-runtime-macos-signing.mjs new file mode 100644 index 00000000000..5d2e8631614 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-macos-signing.mjs @@ -0,0 +1,115 @@ +import { spawnSync } from 'node:child_process' +import { resolve } from 'node:path' +import { pathToFileURL } from 'node:url' + +import { + verifySshRelayRuntimeNativeSigningInput, + verifySshRelayRuntimeNativeSigningReturn +} from './ssh-relay-runtime-native-signing-payload.mjs' +import { readSshRelayRuntimeNativeSigningIdentity } from './ssh-relay-runtime-native-signing-plan.mjs' +import { readSshRelayRuntimeNativeSigningStageReport } from './ssh-relay-runtime-native-signing-stage-report.mjs' + +const CODESIGN_PATH = '/usr/bin/codesign' +const CODESIGN_TIMEOUT_MS = 2 * 60_000 +const CODESIGN_OUTPUT_BYTES = 64 * 1024 +const SIGNING_IDENTITY_PATTERN = /^Developer ID Application: .+ \([A-Z0-9]{10}\)$/u + +function localPath(root, portablePath) { + return resolve(root, ...portablePath.split('/')) +} + +function outputBytes(result) { + return Buffer.byteLength(result?.stdout ?? '') + Buffer.byteLength(result?.stderr ?? '') +} + +export async function signSshRelayRuntimeMacosPayload({ + stagingRoot, + selection, + signingIdentity, + platform = process.platform, + spawnSyncImpl = spawnSync +}) { + if (platform !== 'darwin' || selection.platform !== 'darwin') { + throw new Error('Runtime macOS signing requires a target-native Darwin payload') + } + if (!SIGNING_IDENTITY_PATTERN.test(signingIdentity ?? '')) { + throw new Error('Runtime macOS signing requires an exact Developer ID Application identity') + } + await verifySshRelayRuntimeNativeSigningInput({ stagingRoot, selection }) + for (const entry of selection.signingFiles) { + const result = spawnSyncImpl( + CODESIGN_PATH, + [ + '--force', + '--sign', + signingIdentity, + '--options', + 'runtime', + '--timestamp', + localPath(stagingRoot, entry.path) + ], + { + encoding: 'utf8', + maxBuffer: CODESIGN_OUTPUT_BYTES, + timeout: CODESIGN_TIMEOUT_MS, + windowsHide: true + } + ) + if (result?.error) { + throw new Error(`Runtime macOS signing command failed: ${result.error.message}`) + } + if (result?.status !== 0 || outputBytes(result) > CODESIGN_OUTPUT_BYTES) { + throw new Error( + `Runtime macOS signing command failed for ${entry.path}: ${result?.status ?? ''}` + ) + } + } + return verifySshRelayRuntimeNativeSigningReturn({ returnedRoot: stagingRoot, selection }) +} + +const ARGUMENT_FIELDS = new Map([ + ['--identity', 'identityPath'], + ['--signing-stage-report', 'stageReportPath'], + ['--staging-directory', 'stagingRoot'], + ['--signing-identity', 'signingIdentity'] +]) + +export function parseSshRelayRuntimeMacosSigningArguments(argv) { + const result = {} + for (let index = 0; index < argv.length; index += 2) { + const flag = argv[index] + const field = ARGUMENT_FIELDS.get(flag) + const value = argv[index + 1] + if (!field || !value || value.startsWith('--') || result[field]) { + throw new Error(`Invalid runtime macOS signing argument: ${flag}`) + } + result[field] = field === 'signingIdentity' ? value : resolve(value) + } + if (Object.keys(result).length !== ARGUMENT_FIELDS.size) { + throw new Error('Runtime macOS signing requires identity, report, staging, and signer') + } + return result +} + +async function main() { + const options = parseSshRelayRuntimeMacosSigningArguments(process.argv.slice(2)) + const identity = await readSshRelayRuntimeNativeSigningIdentity(options.identityPath) + const { selection } = await readSshRelayRuntimeNativeSigningStageReport( + options.stageReportPath, + identity + ) + const result = await signSshRelayRuntimeMacosPayload({ ...options, selection }) + 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 macOS signing failed: ${error.stack ?? error}\n`) + process.exitCode = 1 + }) +} + +export const SSH_RELAY_RUNTIME_MACOS_SIGNING_LIMITS = Object.freeze({ + commandTimeoutMs: CODESIGN_TIMEOUT_MS, + maximumCommandOutputBytes: CODESIGN_OUTPUT_BYTES +}) diff --git a/config/scripts/ssh-relay-runtime-macos-signing.test.mjs b/config/scripts/ssh-relay-runtime-macos-signing.test.mjs new file mode 100644 index 00000000000..da5032329f5 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-macos-signing.test.mjs @@ -0,0 +1,135 @@ +import { createHash } from 'node:crypto' +import { appendFileSync } from 'node:fs' +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { expectedSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' +import { signSshRelayRuntimeMacosPayload } from './ssh-relay-runtime-macos-signing.mjs' +import { prepareSshRelayRuntimeNativeSigningStage } from './ssh-relay-runtime-native-signing-stage.mjs' +import { parseSshRelayRuntimeNativeSigningStageReport } from './ssh-relay-runtime-native-signing-stage-report.mjs' + +const temporaryDirectories = [] + +function digest(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +async function fixture() { + const root = await mkdtemp(join(tmpdir(), 'ssh-relay-macos-signing-')) + temporaryDirectories.push(root) + const runtimeRoot = join(root, 'runtime') + await mkdir(runtimeRoot) + const entries = [] + for (const entry of expectedSshRelayRuntimeClosureEntries('darwin-arm64')) { + const path = join(runtimeRoot, ...entry.path.split('/')) + if (entry.type === 'directory') { + await mkdir(path, { recursive: true, mode: entry.mode }) + entries.push(entry) + continue + } + const bytes = Buffer.from(`macOS signing fixture:${entry.path}`) + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, bytes, { mode: entry.mode }) + entries.push({ ...entry, size: bytes.length, sha256: digest(bytes) }) + } + const base = { + identitySchemaVersion: 1, + tupleId: 'darwin-arm64', + os: 'darwin', + architecture: 'arm64', + compatibility: sshRelayRuntimeCompatibility['darwin-arm64'], + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries + } + const files = entries.filter((entry) => entry.type === 'file') + const identity = { + ...base, + contentId: computeSshRelayRuntimeContentId(base), + fileCount: files.length, + expandedSize: files.reduce((total, entry) => total + entry.size, 0) + } + const stagingRoot = join(root, 'stage') + const report = await prepareSshRelayRuntimeNativeSigningStage({ + identity, + runtimeRoot, + stagingRoot, + platform: 'darwin' + }) + return { identity, report, stagingRoot } +} + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })) + ) +}) + +describe('SSH relay runtime macOS signing boundary', () => { + it('validates the hash-bound report and signs every exact staged file', async () => { + const value = await fixture() + const { selection } = parseSshRelayRuntimeNativeSigningStageReport(value.identity, value.report) + const calls = [] + const result = await signSshRelayRuntimeMacosPayload({ + stagingRoot: value.stagingRoot, + selection, + signingIdentity: 'Developer ID Application: Orca Test (ABCDEFGHIJ)', + platform: 'darwin', + spawnSyncImpl: (command, args) => { + calls.push({ command, args }) + appendFileSync(args.at(-1), ':signed') + return { status: 0, stdout: '', stderr: '' } + } + }) + + expect(calls).toHaveLength(3) + expect(calls.every((call) => call.command === '/usr/bin/codesign')).toBe(true) + expect(calls.every((call) => call.args.includes('--timestamp'))).toBe(true) + expect(result.returnedFiles).toHaveLength(3) + }) + + it('rejects report drift and unchanged or failed signing commands', async () => { + const drift = await fixture() + drift.report.signingFiles[0].sourceSha256 = `sha256:${'0'.repeat(64)}` + expect(() => + parseSshRelayRuntimeNativeSigningStageReport(drift.identity, drift.report) + ).toThrow(/disagrees/i) + + const unchanged = await fixture() + const { selection } = parseSshRelayRuntimeNativeSigningStageReport( + unchanged.identity, + unchanged.report + ) + await expect( + signSshRelayRuntimeMacosPayload({ + stagingRoot: unchanged.stagingRoot, + selection, + signingIdentity: 'Developer ID Application: Orca Test (ABCDEFGHIJ)', + platform: 'darwin', + spawnSyncImpl: () => ({ status: 0, stdout: '', stderr: '' }) + }) + ).rejects.toThrow(/did not change/i) + + const failed = await fixture() + const failedSelection = parseSshRelayRuntimeNativeSigningStageReport( + failed.identity, + failed.report + ).selection + await expect( + signSshRelayRuntimeMacosPayload({ + stagingRoot: failed.stagingRoot, + selection: failedSelection, + signingIdentity: 'Developer ID Application: Orca Test (ABCDEFGHIJ)', + platform: 'darwin', + spawnSyncImpl: () => ({ status: 1, stdout: '', stderr: 'denied' }) + }) + ).rejects.toThrow(/failed/i) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-native-signing-finalization.mjs b/config/scripts/ssh-relay-runtime-native-signing-finalization.mjs new file mode 100644 index 00000000000..7fd4f3a62f5 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-native-signing-finalization.mjs @@ -0,0 +1,363 @@ +import { lstat, mkdir, readFile, realpath, rm, writeFile } from 'node:fs/promises' +import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path' +import { pathToFileURL } from 'node:url' + +import { createSshRelayRuntimeArchive } from './ssh-relay-runtime-archive.mjs' +import { writeSshRelayRuntimeManifestTupleDescriptor } from './ssh-relay-runtime-manifest-tuple.mjs' +import { verifySshRelayRuntimeMacosSignatures } from './ssh-relay-runtime-macos-signature-verification.mjs' +import { applySshRelayRuntimeNativeSigningReturn } from './ssh-relay-runtime-native-signing-apply.mjs' +import { readSshRelayRuntimeNativeSigningIdentity } from './ssh-relay-runtime-native-signing-plan.mjs' +import { readSshRelayRuntimeNativeSigningStageReport } from './ssh-relay-runtime-native-signing-stage-report.mjs' +import { + verifySshRelayRuntimePostSignMetadata, + writeSshRelayRuntimePostSignMetadata +} from './ssh-relay-runtime-post-sign-metadata.mjs' +import { sshRelayRuntimeRunnerIdentity } from './ssh-relay-runtime-toolchain.mjs' +import { verifySshRelayRuntimeWindowsSignatures } from './ssh-relay-runtime-windows-signature-verification.mjs' +import { verifySshRelayRuntime } from './verify-ssh-relay-runtime.mjs' + +const FINALIZATION_TIMEOUT_MS = 20 * 60_000 +const MAX_JSON_BYTES = 32 * 1024 * 1024 +const GIT_COMMIT_PATTERN = /^[0-9a-f]{40}$/u + +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 native signing 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 native signing finalization requires an exclusive output directory') +} + +async function finalizationRoots(sourceRuntimeRoot, returnedRoot, outputDirectory) { + const absoluteOutput = resolve(outputDirectory) + const [source, returned, outputParent] = await Promise.all([ + physicalDirectory(resolve(sourceRuntimeRoot), 'source root'), + physicalDirectory(resolve(returnedRoot), 'returned root'), + physicalDirectory(dirname(absoluteOutput), 'output parent') + ]) + const output = resolve(outputParent, basename(absoluteOutput)) + for (const root of [source, returned]) { + if (containsPath(root, output) || containsPath(output, root)) { + throw new Error('Runtime native signing finalization roots must be physically disjoint') + } + } + await assertAbsent(output) + return { source, returned, output } +} + +async function writeJsonExclusive(path, value, signal) { + const bytes = Buffer.from(`${JSON.stringify(value, null, 2)}\n`, 'utf8') + if (bytes.length === 0 || bytes.length > MAX_JSON_BYTES) { + throw new Error('Runtime native signing finalization JSON exceeds its size bound') + } + await writeFile(path, bytes, { flag: 'wx', mode: 0o600, signal }) +} + +async function defaultNativeVerification({ + runtimeRoot, + sourceIdentity, + finalIdentity, + selection, + expectedOrcaTeamIdentifier +}) { + if (sourceIdentity.os === 'darwin') { + return verifySshRelayRuntimeMacosSignatures({ + runtimeRoot, + sourceIdentity, + finalIdentity, + selection, + expectedOrcaTeamIdentifier + }) + } + if (sourceIdentity.os === 'win32') { + return verifySshRelayRuntimeWindowsSignatures({ + runtimeRoot, + sourceIdentity, + finalIdentity, + selection + }) + } + throw new Error( + `Runtime native signing finalization rejects unsigned platform: ${sourceIdentity.os}` + ) +} + +export async function finalizeSshRelayRuntimeNativeSigning({ + sourceRuntimeRoot, + returnedRoot, + outputDirectory, + sourceIdentity, + selection, + expectedOrcaTeamIdentifier, + nodeRelease, + sourceDateEpoch, + gitCommit, + builder, + runner, + toolchain, + nativeVerificationTool, + verifiedAt, + signal, + verifyNativeImpl = defaultNativeVerification, + smokeImpl = verifySshRelayRuntime +}) { + const effectiveSignal = signal + ? AbortSignal.any([signal, AbortSignal.timeout(FINALIZATION_TIMEOUT_MS)]) + : AbortSignal.timeout(FINALIZATION_TIMEOUT_MS) + effectiveSignal.throwIfAborted() + if (!GIT_COMMIT_PATTERN.test(gitCommit ?? '')) { + throw new Error('Runtime native signing finalization requires an exact source commit') + } + const roots = await finalizationRoots(sourceRuntimeRoot, returnedRoot, outputDirectory) + let outputCreated = false + try { + await mkdir(roots.output) + outputCreated = true + const runtimeRoot = join(roots.output, 'runtime') + const evidenceRoot = join(roots.output, 'evidence') + const assetsRoot = join(roots.output, 'assets') + await mkdir(evidenceRoot) + const applied = await applySshRelayRuntimeNativeSigningReturn({ + sourceRuntimeRoot: roots.source, + returnedRoot: roots.returned, + outputRuntimeRoot: runtimeRoot, + identity: sourceIdentity, + selection + }) + const identityPath = join(evidenceRoot, `${sourceIdentity.tupleId}.final-identity.json`) + await writeJsonExclusive(identityPath, applied.identity, effectiveSignal) + const nativeVerification = await verifyNativeImpl({ + runtimeRoot, + sourceIdentity, + finalIdentity: applied.identity, + selection, + expectedOrcaTeamIdentifier + }) + // Why: native code is trusted only after full-tree hashing and before PTY/watcher execution. + const smoke = await smokeImpl({ runtimeDirectory: runtimeRoot, identityPath }) + await mkdir(assetsRoot) + const archive = await createSshRelayRuntimeArchive({ + runtimeRoot, + outputDirectory: assetsRoot, + identity: applied.identity, + sourceDateEpoch, + signal: effectiveSignal + }) + const metadata = await writeSshRelayRuntimePostSignMetadata({ + runtimeRoot, + outputDirectory: assetsRoot, + finalIdentity: applied.identity, + archive, + nodeRelease, + sourceDateEpoch, + gitCommit, + builder, + runner, + toolchain, + signal: effectiveSignal + }) + const tuple = await writeSshRelayRuntimeManifestTupleDescriptor({ + runtimeRoot, + inputDirectory: assetsRoot, + finalIdentity: applied.identity, + verificationReport: nativeVerification, + nativeVerificationTool, + verifiedAt, + signal: effectiveSignal + }) + const result = { + tupleId: sourceIdentity.tupleId, + sourceContentId: sourceIdentity.contentId, + finalContentId: applied.identity.contentId, + returnedFiles: applied.returnedFiles, + nativeVerification, + smoke, + metadata, + aggregateInput: tuple.input + } + await writeJsonExclusive( + join(evidenceRoot, `${sourceIdentity.tupleId}.native-verification.json`), + nativeVerification, + effectiveSignal + ) + await writeJsonExclusive( + join(evidenceRoot, `${sourceIdentity.tupleId}.finalization.json`), + result, + effectiveSignal + ) + return { ...result, runtimeRoot, assetsRoot, evidenceRoot } + } catch (error) { + if (outputCreated) { + // Why: partial signed outputs must never survive for aggregation or retry reuse. + await rm(roots.output, { recursive: true, force: true }) + } + throw error + } +} + +async function readJson(path, label) { + const metadata = await lstat(path) + if ( + !metadata.isFile() || + metadata.isSymbolicLink() || + metadata.size <= 0 || + metadata.size > MAX_JSON_BYTES + ) { + throw new Error(`Runtime native signing finalization ${label} must be bounded JSON`) + } + try { + return JSON.parse(await readFile(path, 'utf8')) + } catch (error) { + throw new Error( + `Runtime native signing finalization ${label} is invalid JSON: ${error.message}` + ) + } +} + +function sourceProvenanceInputs(provenance, sourceIdentity, gitCommit) { + const definition = provenance.predicate?.buildDefinition + const parameters = definition?.externalParameters + const dependencies = definition?.resolvedDependencies + const expectedGit = `git+https://github.com/stablyai/orca@${gitCommit}` + const gitInputs = Array.isArray(dependencies) + ? dependencies.filter( + (entry) => entry?.uri === expectedGit && entry?.digest?.gitCommit === gitCommit + ) + : [] + if ( + parameters?.contentId !== sourceIdentity.contentId || + !Number.isSafeInteger(parameters?.sourceDateEpoch) || + gitInputs.length !== 1 + ) { + throw new Error('Runtime native signing finalization source provenance is inconsistent') + } + return { + sourceDateEpoch: parameters.sourceDateEpoch, + toolchain: definition.internalParameters?.toolchain + } +} + +function signingBuilder(gitCommit, env = process.env) { + if (env.GITHUB_ACTIONS !== 'true') { + return `local://ssh-relay-runtime-native-signing/${process.platform}/${process.arch}` + } + if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(env.GITHUB_REPOSITORY ?? '')) { + throw new Error('Runtime native signing finalization GitHub repository is invalid') + } + return `https://github.com/${env.GITHUB_REPOSITORY}/blob/${gitCommit}/.github/workflows/ssh-relay-runtime-native-signing.yml` +} + +const PATH_ARGUMENTS = new Map([ + ['--identity', 'identityPath'], + ['--source-runtime-directory', 'sourceRuntimeRoot'], + ['--returned-directory', 'returnedRoot'], + ['--signing-stage-report', 'stageReportPath'], + ['--source-archive', 'sourceArchivePath'], + ['--source-sbom', 'sourceSbomPath'], + ['--source-provenance', 'sourceProvenancePath'], + ['--output-directory', 'outputDirectory'], + ['--node-release', 'nodeReleasePath'] +]) +const VALUE_ARGUMENTS = new Map([ + ['--git-commit', 'gitCommit'], + ['--expected-macos-team-identifier', 'expectedOrcaTeamIdentifier'], + ['--native-verification-tool-version', 'nativeVerificationToolVersion'], + ['--verified-at', 'verifiedAt'] +]) + +export function parseSshRelayRuntimeNativeSigningFinalizationArguments(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 native signing finalization argument: ${flag}`) + } + result[field] = PATH_ARGUMENTS.has(flag) ? resolve(value) : value + } + const required = [ + ...PATH_ARGUMENTS.values(), + 'gitCommit', + 'nativeVerificationToolVersion', + 'verifiedAt' + ] + for (const field of required) { + if (!result[field]) { + throw new Error(`Missing runtime native signing finalization argument: ${field}`) + } + } + return result +} + +async function main() { + const options = parseSshRelayRuntimeNativeSigningFinalizationArguments(process.argv.slice(2)) + const sourceIdentity = await readSshRelayRuntimeNativeSigningIdentity(options.identityPath) + if (sourceIdentity.os === 'darwin' && !options.expectedOrcaTeamIdentifier) { + throw new Error('Runtime macOS finalization requires the expected Orca team identifier') + } + const [{ selection }, nodeRelease, sourceProvenance] = await Promise.all([ + readSshRelayRuntimeNativeSigningStageReport(options.stageReportPath, sourceIdentity), + readJson(options.nodeReleasePath, 'Node release'), + readJson(options.sourceProvenancePath, 'source provenance') + ]) + const finalIdentity = { ...sourceIdentity } + delete finalIdentity.archive + await verifySshRelayRuntimePostSignMetadata({ + finalIdentity, + archive: { ...sourceIdentity.archive, path: options.sourceArchivePath }, + sbomPath: options.sourceSbomPath, + provenancePath: options.sourceProvenancePath + }) + const provenanceInputs = sourceProvenanceInputs( + sourceProvenance, + sourceIdentity, + options.gitCommit + ) + const platformTool = sourceIdentity.os === 'darwin' ? 'codesign' : 'Get-AuthenticodeSignature' + const result = await finalizeSshRelayRuntimeNativeSigning({ + ...options, + ...provenanceInputs, + sourceIdentity, + selection, + nodeRelease, + builder: signingBuilder(options.gitCommit), + runner: sshRelayRuntimeRunnerIdentity(), + nativeVerificationTool: { + name: platformTool, + 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 native signing finalization failed: ${error.stack ?? error}\n` + ) + process.exitCode = 1 + }) +} + +export const SSH_RELAY_RUNTIME_NATIVE_SIGNING_FINALIZATION_LIMITS = Object.freeze({ + maximumJsonBytes: MAX_JSON_BYTES, + timeoutMs: FINALIZATION_TIMEOUT_MS +}) diff --git a/config/scripts/ssh-relay-runtime-native-signing-finalization.test.mjs b/config/scripts/ssh-relay-runtime-native-signing-finalization.test.mjs new file mode 100644 index 00000000000..a06f5f95e8b --- /dev/null +++ b/config/scripts/ssh-relay-runtime-native-signing-finalization.test.mjs @@ -0,0 +1,192 @@ +import { createHash } from 'node:crypto' +import { appendFile, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { expectedSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' +import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' +import { finalizeSshRelayRuntimeNativeSigning } from './ssh-relay-runtime-native-signing-finalization.mjs' +import { prepareSshRelayRuntimeNativeSigningStage } from './ssh-relay-runtime-native-signing-stage.mjs' + +const temporaryDirectories = [] +const SOURCE_DATE_EPOCH = 1_788_739_200 +const GIT_COMMIT = '1'.repeat(40) + +function digest(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +function toolchain() { + return Object.fromEntries( + [ + 'buildNode', + 'bundledNode', + 'compiler', + 'buildSystem', + 'python', + 'archive', + 'nodeAddonApi', + 'nodeGyp', + 'strip' + ].map((name, index) => [ + name, + { version: `${name} fixture`, sha256: `sha256:${(index + 1).toString(16).repeat(64)}` } + ]) + ) +} + +async function fixture() { + const root = await mkdtemp(join(tmpdir(), 'ssh-relay-signing-finalization-')) + temporaryDirectories.push(root) + const sourceRuntimeRoot = join(root, 'source-runtime') + await mkdir(sourceRuntimeRoot) + const entries = [] + for (const entry of expectedSshRelayRuntimeClosureEntries('darwin-arm64')) { + const path = join(sourceRuntimeRoot, ...entry.path.split('/')) + if (entry.type === 'directory') { + await mkdir(path, { recursive: true, mode: entry.mode }) + entries.push(entry) + continue + } + const bytes = Buffer.from(`native finalization fixture:${entry.path}`) + await mkdir(dirname(path), { recursive: true }) + await writeFile(path, bytes, { mode: entry.mode }) + entries.push({ ...entry, size: bytes.length, sha256: digest(bytes) }) + } + const base = { + identitySchemaVersion: 1, + tupleId: 'darwin-arm64', + os: 'darwin', + architecture: 'arm64', + compatibility: sshRelayRuntimeCompatibility['darwin-arm64'], + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries + } + const files = entries.filter((entry) => entry.type === 'file') + const sourceIdentity = { + ...base, + contentId: computeSshRelayRuntimeContentId(base), + fileCount: files.length, + expandedSize: files.reduce((total, entry) => total + entry.size, 0) + } + const returnedRoot = join(root, 'returned') + const report = await prepareSshRelayRuntimeNativeSigningStage({ + identity: sourceIdentity, + runtimeRoot: sourceRuntimeRoot, + stagingRoot: returnedRoot, + platform: 'darwin' + }) + for (const entry of report.signingFiles) { + await appendFile(join(returnedRoot, ...entry.path.split('/')), ':signed') + } + const nodeRelease = JSON.parse( + await readFile(new URL('../ssh-relay-node-release-v24.18.0.json', import.meta.url), 'utf8') + ) + const selection = { + tupleId: report.tupleId, + platform: report.platform, + policy: report.policy, + immutableVendorFiles: report.immutableVendorFiles, + signingFiles: report.signingFiles, + preservedUpstreamFiles: report.preservedUpstreamFiles, + verificationFiles: [...report.immutableVendorFiles, ...report.signingFiles].map( + ({ path, role, sourceSha256, sourceSize }) => ({ path, role, sourceSha256, sourceSize }) + ) + } + return { root, sourceRuntimeRoot, returnedRoot, sourceIdentity, selection, nodeRelease } +} + +function nativeReport(sourceIdentity, finalIdentity, selection) { + const finalFiles = new Map( + finalIdentity.entries + .filter((entry) => entry.type === 'file') + .map((entry) => [entry.path, entry]) + ) + return { + tupleId: finalIdentity.tupleId, + sourceContentId: sourceIdentity.contentId, + finalContentId: finalIdentity.contentId, + verifiedFiles: selection.verificationFiles.map((entry) => ({ + path: entry.path, + role: entry.role, + sha256: finalFiles.get(entry.path).sha256, + signerKind: entry.role === 'node' ? 'official-node' : 'orca-built', + authority: `Developer ID Application: Fixture (${entry.role === 'node' ? 'HX7739G8FX' : 'ABCDEFGHIJ'})`, + teamIdentifier: entry.role === 'node' ? 'HX7739G8FX' : 'ABCDEFGHIJ' + })) + } +} + +function finalizationInput(value, outputDirectory) { + return { + ...value, + outputDirectory, + expectedOrcaTeamIdentifier: 'ABCDEFGHIJ', + sourceDateEpoch: SOURCE_DATE_EPOCH, + gitCommit: GIT_COMMIT, + builder: 'https://github.com/stablyai/orca/ssh-relay-runtime-native-signing-fixture', + runner: { + os: 'macOS', + architecture: 'ARM64', + environment: 'fixture', + requestedLabel: 'macos-15', + image: { os: 'macos', version: 'fixture' } + }, + toolchain: toolchain(), + nativeVerificationTool: { name: 'codesign', version: 'fixture-v1' }, + verifiedAt: '2026-07-15T12:00:00.000Z', + verifyNativeImpl: ({ sourceIdentity, finalIdentity, selection }) => + nativeReport(sourceIdentity, finalIdentity, selection), + smokeImpl: async () => ({ + tree: { verified: true }, + smoke: { nodeVersion: 'v24.18.0', pty: 'passed', watcher: 'passed' } + }) + } +} + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })) + ) +}) + +describe('SSH relay runtime native signing finalization', () => { + it('emits only final signed-byte assets after native verification and smoke', async () => { + const value = await fixture() + const outputDirectory = join(value.root, 'final') + const result = await finalizeSshRelayRuntimeNativeSigning( + finalizationInput(value, outputDirectory) + ) + + expect(result.finalContentId).not.toBe(value.sourceIdentity.contentId) + expect(result.returnedFiles).toHaveLength(3) + expect(await readdir(result.assetsRoot)).toHaveLength(4) + expect( + (await readdir(result.assetsRoot)).some((name) => name.endsWith('.manifest-tuple.json')) + ).toBe(true) + expect(await readdir(result.evidenceRoot)).toEqual( + expect.arrayContaining([ + 'darwin-arm64.final-identity.json', + 'darwin-arm64.finalization.json', + 'darwin-arm64.native-verification.json' + ]) + ) + }) + + it('removes the complete output when native trust fails', async () => { + const value = await fixture() + const outputDirectory = join(value.root, 'rejected') + const input = finalizationInput(value, outputDirectory) + input.verifyNativeImpl = async () => { + throw new Error('fixture native trust denied') + } + await expect(finalizeSshRelayRuntimeNativeSigning(input)).rejects.toThrow(/trust denied/i) + await expect(readdir(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' }) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-native-signing-payload.mjs b/config/scripts/ssh-relay-runtime-native-signing-payload.mjs index f3ea08177f3..9d00f74570d 100644 --- a/config/scripts/ssh-relay-runtime-native-signing-payload.mjs +++ b/config/scripts/ssh-relay-runtime-native-signing-payload.mjs @@ -198,6 +198,40 @@ function assertExactReturnedClosure(actual, expectedFiles) { } } +export async function verifySshRelayRuntimeNativeSigningInput({ stagingRoot, selection }) { + if (selection.signingFiles.length === 0) { + throw new Error('Runtime native signing selection has no input payload') + } + const absoluteStagingRoot = resolve(stagingRoot) + const rootMetadata = await lstat(absoluteStagingRoot) + if (rootMetadata.isSymbolicLink() || !rootMetadata.isDirectory()) { + throw new Error('Runtime native signing input root must be a real directory') + } + const actual = await walkReturnedTree(absoluteStagingRoot) + assertExactReturnedClosure(actual, selection.signingFiles) + let stagedSize = 0 + const stagedFiles = [] + for (const entry of selection.signingFiles) { + const staged = actual.files.get(entry.path) + if ( + staged.metadata.size !== entry.sourceSize || + (await sha256File(staged.path)) !== entry.sourceSha256 + ) { + throw new Error(`Runtime native signing input changed after staging: ${entry.path}`) + } + stagedSize += staged.metadata.size + if (stagedSize > MAX_RETURNED_PAYLOAD_BYTES) { + throw new Error('Runtime native signing input exceeds total size bound') + } + stagedFiles.push({ + path: entry.path, + sourceSha256: entry.sourceSha256, + sourceSize: entry.sourceSize + }) + } + return { tupleId: selection.tupleId, stagedFiles, stagedSize } +} + export async function verifySshRelayRuntimeNativeSigningReturn({ returnedRoot, selection }) { if (selection.signingFiles.length === 0) { throw new Error('Runtime native signing selection has no returned payload') diff --git a/config/scripts/ssh-relay-runtime-native-signing-stage-report.mjs b/config/scripts/ssh-relay-runtime-native-signing-stage-report.mjs new file mode 100644 index 00000000000..766de443a87 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-native-signing-stage-report.mjs @@ -0,0 +1,73 @@ +import { readFile, stat } from 'node:fs/promises' +import { isDeepStrictEqual } from 'node:util' + +import { buildSshRelayRuntimeNativeSigningSelection } from './ssh-relay-runtime-native-signing-selection.mjs' + +const MAX_STAGE_REPORT_BYTES = 4 * 1024 * 1024 + +function expectedReport(identity, report) { + const selection = buildSshRelayRuntimeNativeSigningSelection(identity, report.assessments) + const stagedFiles = selection.signingFiles.map(({ path, sourceSha256, sourceSize }) => ({ + path, + sourceSha256, + sourceSize + })) + return { + tupleId: selection.tupleId, + platform: selection.platform, + policy: selection.policy, + assessments: report.assessments, + immutableVendorFiles: selection.immutableVendorFiles, + signingFiles: selection.signingFiles, + preservedUpstreamFiles: selection.preservedUpstreamFiles, + payload: { + tupleId: selection.tupleId, + stagingRequired: selection.signingFiles.length > 0, + stagedFiles, + stagedSize: stagedFiles.reduce((total, entry) => total + entry.sourceSize, 0) + } + } +} + +export function parseSshRelayRuntimeNativeSigningStageReport(identity, report) { + if (!report || typeof report !== 'object' || Array.isArray(report)) { + throw new Error('Runtime native signing stage report must be an object') + } + const expected = expectedReport(identity, report) + if (!isDeepStrictEqual(report, expected)) { + // Why: the signing stage crosses credentialed jobs and must remain bound to source hashes. + throw new Error('Runtime native signing stage report disagrees with its authenticated identity') + } + return { + selection: { + tupleId: expected.tupleId, + platform: expected.platform, + policy: expected.policy, + immutableVendorFiles: expected.immutableVendorFiles, + signingFiles: expected.signingFiles, + preservedUpstreamFiles: expected.preservedUpstreamFiles, + verificationFiles: buildSshRelayRuntimeNativeSigningSelection(identity, expected.assessments) + .verificationFiles + }, + payload: expected.payload, + assessments: expected.assessments + } +} + +export async function readSshRelayRuntimeNativeSigningStageReport(path, identity) { + const metadata = await stat(path) + if (!metadata.isFile() || metadata.size <= 0 || metadata.size > MAX_STAGE_REPORT_BYTES) { + throw new Error('Runtime native signing stage report must be one bounded regular file') + } + let report + try { + report = JSON.parse(await readFile(path, 'utf8')) + } catch (error) { + throw new Error(`Runtime native signing stage report is not valid JSON: ${error.message}`) + } + return parseSshRelayRuntimeNativeSigningStageReport(identity, report) +} + +export const SSH_RELAY_RUNTIME_NATIVE_SIGNING_STAGE_REPORT_LIMITS = Object.freeze({ + maximumBytes: MAX_STAGE_REPORT_BYTES +}) diff --git a/config/scripts/ssh-relay-runtime-zip.mjs b/config/scripts/ssh-relay-runtime-zip.mjs index cb23bdeda18..6cc6f9c043d 100644 --- a/config/scripts/ssh-relay-runtime-zip.mjs +++ b/config/scripts/ssh-relay-runtime-zip.mjs @@ -1,6 +1,6 @@ import { createHash } from 'node:crypto' import { createReadStream, createWriteStream } from 'node:fs' -import { rm, stat } from 'node:fs/promises' +import { chmod, mkdir, rm, stat } from 'node:fs/promises' import { join } from 'node:path' import { pipeline } from 'node:stream/promises' @@ -136,3 +136,58 @@ export async function inspectSshRelayRuntimeZip(archivePath, identity, { signal } return result } + +export async function extractSshRelayRuntimeZip({ + archivePath, + outputDirectory, + identity, + signal +}) { + const expected = new Map(identity.entries.map((entry) => [entry.path, entry])) + const seen = new Set() + const result = await visitSshRelayZip( + archivePath, + ZIP_LIMITS, + async (actual, consume) => { + const declared = expected.get(actual.path) + if (!declared || seen.has(actual.path)) { + throw new Error(`Runtime ZIP has extra or duplicate entry: ${actual.path}`) + } + seen.add(actual.path) + if ( + actual.type !== declared.type || + actual.unixMode === null || + (actual.unixMode & 0o777) !== declared.mode + ) { + throw new Error(`Runtime ZIP type or mode mismatch: ${actual.path}`) + } + const outputPath = join(outputDirectory, ...actual.path.split('/')) + if (declared.type === 'directory') { + await mkdir(outputPath, { recursive: true, mode: declared.mode }) + if (process.platform !== 'win32') { + await chmod(outputPath, declared.mode) + } + return + } + if (actual.size !== declared.size) { + throw new Error(`Runtime ZIP size mismatch: ${actual.path}`) + } + const extracted = await consume({ outputPath, mode: declared.mode }) + if (extracted.sha256 !== declared.sha256) { + throw new Error(`Runtime ZIP file integrity mismatch: ${actual.path}`) + } + if (process.platform !== 'win32') { + await chmod(outputPath, declared.mode) + } + }, + { signal } + ) + const missing = [...expected.keys()].find((path) => !seen.has(path)) + if (missing) { + throw new Error(`Runtime ZIP is missing declared entry: ${missing}`) + } + if (result.files !== identity.fileCount || result.expandedBytes !== identity.expandedSize) { + throw new Error('Runtime ZIP aggregate size or file-count mismatch') + } + return result +} From 2a7083d278e7a14b5b159555df90b1e84a2a41e7 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 08:23:17 -0400 Subject: [PATCH 121/267] ci(ssh): add disconnected native signing workflow Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 32 ++ .../ssh-relay-runtime-native-signing.yml | 359 ++++++++++++++++++ ...y-runtime-native-signing-workflow.test.mjs | 96 +++++ .../ssh-relay-runtime-workflow.test.mjs | 11 +- 4 files changed, 497 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ssh-relay-runtime-native-signing.yml create mode 100644 config/scripts/ssh-relay-runtime-native-signing-workflow.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 2c522175769..48c52d78826 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -166,6 +166,14 @@ jobs: node --check config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs node --check config/scripts/ssh-relay-runtime-post-sign-metadata.mjs node --check config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs + node --check config/scripts/ssh-relay-runtime-archive-extraction.mjs + node --check config/scripts/ssh-relay-runtime-archive-extraction.test.mjs + node --check config/scripts/ssh-relay-runtime-native-signing-stage-report.mjs + node --check config/scripts/ssh-relay-runtime-macos-signing.mjs + 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-native-signing-workflow.test.mjs node --check config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs pnpm exec vitest run --config config/vitest.config.ts \ config/scripts/ssh-relay-runtime-compatibility.test.mjs \ @@ -198,6 +206,10 @@ jobs: config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs \ config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs \ config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs \ + 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-native-signing-workflow.test.mjs \ config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs \ config/scripts/ssh-relay-runtime-closure.test.mjs \ config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ @@ -556,6 +568,22 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime post-sign metadata syntax check failed' } node --check config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime post-sign metadata test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-archive-extraction.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime archive extraction syntax check failed' } + node --check config/scripts/ssh-relay-runtime-archive-extraction.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime archive extraction test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-native-signing-stage-report.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime signing stage report syntax check failed' } + node --check config/scripts/ssh-relay-runtime-macos-signing.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime macOS signing syntax check failed' } + node --check config/scripts/ssh-relay-runtime-macos-signing.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime macOS signing test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-native-signing-finalization.mjs + 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-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-build-prerequisite.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime build prerequisite test syntax check failed' } pnpm exec vitest run --config config/vitest.config.ts ` @@ -589,6 +617,10 @@ jobs: config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs ` config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs ` config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs ` + 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-native-signing-workflow.test.mjs ` config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs ` config/scripts/ssh-relay-runtime-closure.test.mjs ` config/scripts/ssh-relay-runtime-pty-smoke.test.mjs ` diff --git a/.github/workflows/ssh-relay-runtime-native-signing.yml b/.github/workflows/ssh-relay-runtime-native-signing.yml new file mode 100644 index 00000000000..b0d45edd69a --- /dev/null +++ b/.github/workflows/ssh-relay-runtime-native-signing.yml @@ -0,0 +1,359 @@ +name: SSH Relay Runtime Native Signing + +on: + workflow_call: + inputs: + source-sha: + required: true + type: string + secrets: + MAC_CERTS: + required: true + MAC_CERTS_PASSWORD: + required: true + SIGNPATH_API_TOKEN: + required: true + SLACK_WEBHOOK_URL: + required: true + +permissions: + actions: read + contents: read + +jobs: + sign-macos-runtime: + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - runner: macos-15 + tuple: darwin-arm64 + - runner: macos-15-intel + tuple: darwin-x64 + runs-on: ${{ matrix.runner }} + env: + ORCA_RUNTIME_REQUESTED_RUNNER: ${{ matrix.runner }} + steps: + - name: Checkout exact source revision + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + ref: ${{ inputs.source-sha }} + persist-credentials: false + + - name: Setup pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6 + with: + run_install: false + + - name: Setup Node 24.18.0 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 24.18.0 + cache: pnpm + + - name: Install credential-free finalization dependencies + shell: bash + run: | + set -euo pipefail + pnpm install --frozen-lockfile --ignore-scripts + git diff --exit-code package.json pnpm-lock.yaml + + - name: Verify native archive and signing tools + shell: bash + run: | + set -euo pipefail + brew list xz >/dev/null 2>&1 || brew install xz + command -v xz + test -x /usr/bin/codesign + security help >/dev/null + + - name: Download exact native build output + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ssh-relay-runtime-${{ matrix.tuple }} + path: ${{ runner.temp }}/ssh-relay-runtime-build/${{ matrix.tuple }} + + - name: Reconstruct authenticated unsigned runtime + shell: bash + run: | + set -euo pipefail + evidence="$RUNNER_TEMP/ssh-relay-runtime-build/${{ matrix.tuple }}" + identity="$evidence/orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json" + archive=$(find "$evidence" -maxdepth 1 -type f -name 'orca-ssh-relay-runtime-v1-*.tar.xz' -print) + test "$(printf '%s\n' "$archive" | sed '/^$/d' | wc -l | tr -d ' ')" = 1 + runtime="$RUNNER_TEMP/ssh-relay-runtime-source/${{ matrix.tuple }}" + node config/scripts/ssh-relay-runtime-archive-extraction.mjs \ + --identity "$identity" \ + --archive "$archive" \ + --output-directory "$runtime" + printf 'ORCA_RUNTIME_EVIDENCE=%s\n' "$evidence" >> "$GITHUB_ENV" + printf 'ORCA_RUNTIME_IDENTITY=%s\n' "$identity" >> "$GITHUB_ENV" + printf 'ORCA_RUNTIME_ARCHIVE=%s\n' "$archive" >> "$GITHUB_ENV" + printf 'ORCA_RUNTIME_SOURCE=%s\n' "$runtime" >> "$GITHUB_ENV" + + - name: Stage exact native signing payload + shell: bash + run: | + set -euo pipefail + stage="$RUNNER_TEMP/ssh-relay-runtime-signing-stage/${{ matrix.tuple }}" + report="$RUNNER_TEMP/${{ matrix.tuple }}.signing-stage.json" + node config/scripts/ssh-relay-runtime-native-signing-stage.mjs \ + --identity "$ORCA_RUNTIME_IDENTITY" \ + --runtime-directory "$ORCA_RUNTIME_SOURCE" \ + --staging-directory "$stage" > "$report" + printf 'ORCA_RUNTIME_SIGNING_STAGE=%s\n' "$stage" >> "$GITHUB_ENV" + printf 'ORCA_RUNTIME_SIGNING_REPORT=%s\n' "$report" >> "$GITHUB_ENV" + + - name: Import ephemeral Developer ID certificate + shell: bash + env: + MAC_CERTS: ${{ secrets.MAC_CERTS }} + MAC_CERTS_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }} + run: | + set -euo pipefail + keychain="$RUNNER_TEMP/orca-ssh-relay-signing.keychain-db" + certificate="$RUNNER_TEMP/orca-ssh-relay-signing.p12" + password=$(openssl rand -base64 24) + printf '%s' "$MAC_CERTS" | base64 --decode > "$certificate" + security create-keychain -p "$password" "$keychain" + security set-keychain-settings -lut 21600 "$keychain" + security unlock-keychain -p "$password" "$keychain" + security import "$certificate" -P "$MAC_CERTS_PASSWORD" -A -t cert -f pkcs12 -k "$keychain" + security set-key-partition-list -S apple-tool:,apple: -k "$password" "$keychain" >/dev/null + identities=$(security find-identity -v -p codesigning "$keychain" | \ + sed -nE 's/^[[:space:]]*[0-9]+\) [0-9A-F]+ "(Developer ID Application: .+ \([A-Z0-9]{10}\))"$/\1/p') + test "$(printf '%s\n' "$identities" | sed '/^$/d' | wc -l | tr -d ' ')" = 1 + team_id=$(printf '%s\n' "$identities" | sed -nE 's/^.*\(([A-Z0-9]{10})\)$/\1/p') + test ${#team_id} = 10 + printf 'ORCA_MAC_SIGNING_IDENTITY=%s\n' "$identities" >> "$GITHUB_ENV" + printf 'ORCA_MAC_TEAM_ID=%s\n' "$team_id" >> "$GITHUB_ENV" + printf 'ORCA_MAC_KEYCHAIN=%s\n' "$keychain" >> "$GITHUB_ENV" + rm -f "$certificate" + + - name: Sign exact macOS native payload + shell: bash + run: | + set -euo pipefail + node config/scripts/ssh-relay-runtime-macos-signing.mjs \ + --identity "$ORCA_RUNTIME_IDENTITY" \ + --signing-stage-report "$ORCA_RUNTIME_SIGNING_REPORT" \ + --staging-directory "$ORCA_RUNTIME_SIGNING_STAGE" \ + --signing-identity "$ORCA_MAC_SIGNING_IDENTITY" + + - name: Finalize verified signed runtime + shell: bash + run: | + set -euo pipefail + verified_at=$(node -e 'process.stdout.write(new Date().toISOString())') + node config/scripts/ssh-relay-runtime-native-signing-finalization.mjs \ + --identity "$ORCA_RUNTIME_IDENTITY" \ + --source-runtime-directory "$ORCA_RUNTIME_SOURCE" \ + --returned-directory "$ORCA_RUNTIME_SIGNING_STAGE" \ + --signing-stage-report "$ORCA_RUNTIME_SIGNING_REPORT" \ + --source-archive "$ORCA_RUNTIME_ARCHIVE" \ + --source-sbom "$ORCA_RUNTIME_EVIDENCE/orca-ssh-relay-runtime-${{ matrix.tuple }}.spdx.json" \ + --source-provenance "$ORCA_RUNTIME_EVIDENCE/orca-ssh-relay-runtime-${{ matrix.tuple }}.provenance.json" \ + --output-directory "$RUNNER_TEMP/ssh-relay-runtime-final/${{ matrix.tuple }}" \ + --node-release config/ssh-relay-node-release-v24.18.0.json \ + --git-commit '${{ inputs.source-sha }}' \ + --expected-macos-team-identifier "$ORCA_MAC_TEAM_ID" \ + --native-verification-tool-version "codesign-${ImageVersion}" \ + --verified-at "$verified_at" + + - name: Remove ephemeral Developer ID keychain + if: always() + shell: bash + run: | + set -euo pipefail + if [[ -n "${ORCA_MAC_KEYCHAIN:-}" && -e "$ORCA_MAC_KEYCHAIN" ]]; then + security delete-keychain "$ORCA_MAC_KEYCHAIN" + fi + + - name: Upload immutable signed runtime output + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: ssh-relay-runtime-signed-${{ matrix.tuple }} + path: | + ${{ runner.temp }}/ssh-relay-runtime-final/${{ matrix.tuple }}/assets/ + ${{ runner.temp }}/ssh-relay-runtime-final/${{ matrix.tuple }}/evidence/ + if-no-files-found: error + retention-days: 7 + + sign-windows-runtime: + timeout-minutes: 330 + strategy: + fail-fast: false + matrix: + include: + - runner: windows-11-arm + tuple: win32-arm64 + - runner: windows-2022 + tuple: win32-x64 + runs-on: ${{ matrix.runner }} + env: + ORCA_RUNTIME_REQUESTED_RUNNER: ${{ matrix.runner }} + steps: + - name: Checkout exact source revision + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + ref: ${{ inputs.source-sha }} + persist-credentials: false + + - name: Setup pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6 + with: + run_install: false + + - name: Setup Node 24.18.0 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 24.18.0 + cache: pnpm + + - name: Install credential-free finalization dependencies + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + pnpm install --frozen-lockfile --ignore-scripts + if ($LASTEXITCODE -ne 0) { throw 'pnpm install failed' } + git diff --exit-code package.json pnpm-lock.yaml + if ($LASTEXITCODE -ne 0) { throw 'frozen install changed dependency files' } + + - name: Download exact native build output + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ssh-relay-runtime-${{ matrix.tuple }} + path: ${{ runner.temp }}/ssh-relay-runtime-build/${{ matrix.tuple }} + + - name: Reconstruct authenticated unsigned runtime + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $evidence = Join-Path $env:RUNNER_TEMP 'ssh-relay-runtime-build/${{ matrix.tuple }}' + $identity = Join-Path $evidence 'orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json' + $archives = @(Get-ChildItem -LiteralPath $evidence -File -Filter 'orca-ssh-relay-runtime-v1-*.zip') + if ($archives.Count -ne 1) { throw 'Expected exactly one authenticated Windows archive' } + $runtime = Join-Path $env:RUNNER_TEMP 'ssh-relay-runtime-source/${{ matrix.tuple }}' + node config/scripts/ssh-relay-runtime-archive-extraction.mjs ` + --identity $identity ` + --archive $archives[0].FullName ` + --output-directory $runtime + if ($LASTEXITCODE -ne 0) { throw 'authenticated runtime reconstruction failed' } + "ORCA_RUNTIME_EVIDENCE=$evidence" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append + "ORCA_RUNTIME_IDENTITY=$identity" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append + "ORCA_RUNTIME_ARCHIVE=$($archives[0].FullName)" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append + "ORCA_RUNTIME_SOURCE=$runtime" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Stage exact native signing payload + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $stage = Join-Path $env:RUNNER_TEMP 'ssh-relay-runtime-signing-stage/${{ matrix.tuple }}' + $report = Join-Path $env:RUNNER_TEMP '${{ matrix.tuple }}.signing-stage.json' + node config/scripts/ssh-relay-runtime-native-signing-stage.mjs ` + --identity $env:ORCA_RUNTIME_IDENTITY ` + --runtime-directory $env:ORCA_RUNTIME_SOURCE ` + --staging-directory $stage | Out-File $report -Encoding utf8 + if ($LASTEXITCODE -ne 0) { throw 'native signing stage failed' } + "ORCA_RUNTIME_SIGNING_STAGE=$stage" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append + "ORCA_RUNTIME_SIGNING_REPORT=$report" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Upload exact unsigned payload for SignPath + id: upload-unsigned-runtime + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: ssh-relay-runtime-unsigned-${{ matrix.tuple }}-${{ github.run_id }} + path: ${{ env.ORCA_RUNTIME_SIGNING_STAGE }}/** + if-no-files-found: error + retention-days: 1 + + - name: Install bounded SignPath client + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + if ($null -eq (Get-PSResourceRepository -Name PSGallery -ErrorAction SilentlyContinue)) { + Register-PSResourceRepository -PSGallery -Trusted + } else { + Set-PSResourceRepository -Name PSGallery -Trusted + } + Install-PSResource -Name SignPath -Version '[4.0.0,5.0.0)' -Repository PSGallery -Scope CurrentUser -TrustRepository -Reinstall -ErrorAction Stop + Import-Module SignPath -ErrorAction Stop + Get-Command -Name Get-SignedArtifact -Module SignPath -ErrorAction Stop + + - name: Submit exact SignPath request + id: submit-runtime-signing + uses: signpath/github-action-submit-signing-request@b9d91eadd323de506c0c81cf0c7fe7438f3360fd # v2 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: c37aa192-a27a-4377-9c90-5d6c95912dc0 + project-slug: orca + signing-policy-slug: release-signing + artifact-configuration-slug: windows-inner-binaries-zip + github-artifact-id: ${{ steps.upload-unsigned-runtime.outputs.artifact-id }} + wait-for-completion: false + + - name: Notify SignPath approvers + shell: pwsh + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + SIGNPATH_REQUEST_URL: ${{ steps.submit-runtime-signing.outputs.signing-request-web-url }} + run: | + $ErrorActionPreference = 'Stop' + if ([string]::IsNullOrWhiteSpace($env:SLACK_WEBHOOK_URL)) { + throw 'SLACK_WEBHOOK_URL is required for bounded approval handling' + } + $message = "SSH relay runtime ${{ matrix.tuple }} is awaiting SignPath approval.`n<$env:SIGNPATH_REQUEST_URL|Open signing request>`n" + $payload = @{ text = $message } | ConvertTo-Json + Invoke-RestMethod -Method Post -Uri $env:SLACK_WEBHOOK_URL -ContentType 'application/json' -Body $payload + + - name: Download exact signed payload from SignPath + shell: pwsh + env: + SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} + SIGNPATH_REQUEST_ID: ${{ steps.submit-runtime-signing.outputs.signing-request-id }} + run: | + $ErrorActionPreference = 'Stop' + $archive = Join-Path $env:RUNNER_TEMP '${{ matrix.tuple }}.signed.zip' + $returned = Join-Path $env:RUNNER_TEMP 'ssh-relay-runtime-returned/${{ matrix.tuple }}' + Get-SignedArtifact ` + -OrganizationId c37aa192-a27a-4377-9c90-5d6c95912dc0 ` + -ApiToken $env:SIGNPATH_API_TOKEN ` + -SigningRequestId $env:SIGNPATH_REQUEST_ID ` + -OutputArtifactPath $archive ` + -Force ` + -WaitForCompletionTimeoutInSeconds 14400 + Expand-Archive -LiteralPath $archive -DestinationPath $returned + "ORCA_RUNTIME_RETURNED=$returned" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Finalize verified signed runtime + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $verifiedAt = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ss.fffZ') + $toolVersion = "PowerShell-$($PSVersionTable.PSVersion)" + node config/scripts/ssh-relay-runtime-native-signing-finalization.mjs ` + --identity $env:ORCA_RUNTIME_IDENTITY ` + --source-runtime-directory $env:ORCA_RUNTIME_SOURCE ` + --returned-directory $env:ORCA_RUNTIME_RETURNED ` + --signing-stage-report $env:ORCA_RUNTIME_SIGNING_REPORT ` + --source-archive $env:ORCA_RUNTIME_ARCHIVE ` + --source-sbom "$env:ORCA_RUNTIME_EVIDENCE/orca-ssh-relay-runtime-${{ matrix.tuple }}.spdx.json" ` + --source-provenance "$env:ORCA_RUNTIME_EVIDENCE/orca-ssh-relay-runtime-${{ matrix.tuple }}.provenance.json" ` + --output-directory "$env:RUNNER_TEMP/ssh-relay-runtime-final/${{ matrix.tuple }}" ` + --node-release config/ssh-relay-node-release-v24.18.0.json ` + --git-commit '${{ inputs.source-sha }}' ` + --native-verification-tool-version $toolVersion ` + --verified-at $verifiedAt + if ($LASTEXITCODE -ne 0) { throw 'signed runtime finalization failed' } + + - name: Upload immutable signed runtime output + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: ssh-relay-runtime-signed-${{ matrix.tuple }} + path: | + ${{ runner.temp }}/ssh-relay-runtime-final/${{ matrix.tuple }}/assets/ + ${{ runner.temp }}/ssh-relay-runtime-final/${{ matrix.tuple }}/evidence/ + if-no-files-found: error + retention-days: 7 diff --git a/config/scripts/ssh-relay-runtime-native-signing-workflow.test.mjs b/config/scripts/ssh-relay-runtime-native-signing-workflow.test.mjs new file mode 100644 index 00000000000..cab01b06900 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-native-signing-workflow.test.mjs @@ -0,0 +1,96 @@ +import { readFile } from 'node:fs/promises' + +import { parse } from 'yaml' +import { describe, expect, it } from 'vitest' + +const signingWorkflowUrl = new URL( + '../../.github/workflows/ssh-relay-runtime-native-signing.yml', + import.meta.url +) +const buildWorkflowUrl = new URL( + '../../.github/workflows/ssh-relay-runtime-artifacts.yml', + import.meta.url +) +const releaseCutUrl = new URL('../../.github/workflows/release-cut.yml', import.meta.url) +const releaseMacUrl = new URL('../../.github/workflows/release-mac-build.yml', import.meta.url) + +const EXPECTED_SECRETS = [ + 'MAC_CERTS', + 'MAC_CERTS_PASSWORD', + 'SIGNPATH_API_TOKEN', + 'SLACK_WEBHOOK_URL' +] + +function actionRefs(source) { + return [...source.matchAll(/^\s*uses:\s+[^\s#]+@([^\s#]+)/gmu)].map((match) => match[1]) +} + +describe('SSH relay runtime native signing workflow', () => { + it('is callable only and keeps credentialed jobs disconnected from release consumers', async () => { + const [source, build, releaseCut, releaseMac] = await Promise.all([ + readFile(signingWorkflowUrl, 'utf8'), + readFile(buildWorkflowUrl, 'utf8'), + readFile(releaseCutUrl, 'utf8'), + readFile(releaseMacUrl, 'utf8') + ]) + const workflow = parse(source) + + expect(Object.keys(workflow.on)).toEqual(['workflow_call']) + expect(workflow.on.workflow_call.inputs).toEqual({ + 'source-sha': { required: true, type: 'string' } + }) + expect(Object.keys(workflow.on.workflow_call.secrets)).toEqual(EXPECTED_SECRETS) + expect(workflow.permissions).toEqual({ actions: 'read', contents: 'read' }) + expect(Object.keys(workflow.jobs)).toEqual(['sign-macos-runtime', 'sign-windows-runtime']) + expect(workflow.jobs['sign-macos-runtime']).toMatchObject({ + 'timeout-minutes': 30, + strategy: { + 'fail-fast': false, + matrix: { + include: [ + { runner: 'macos-15', tuple: 'darwin-arm64' }, + { runner: 'macos-15-intel', tuple: 'darwin-x64' } + ] + } + } + }) + expect(workflow.jobs['sign-windows-runtime']).toMatchObject({ + 'timeout-minutes': 330, + strategy: { + 'fail-fast': false, + matrix: { + include: [ + { runner: 'windows-11-arm', tuple: 'win32-arm64' }, + { runner: 'windows-2022', tuple: 'win32-x64' } + ] + } + } + }) + expect(actionRefs(source).every((ref) => /^[0-9a-f]{40}$/u.test(ref))).toBe(true) + expect(source).not.toContain('continue-on-error') + expect(source).not.toMatch(/\b(?:gh\s+release|softprops\/action-gh-release)\b/u) + expect(source.match(/ssh-relay-runtime-archive-extraction\.mjs/g)).toHaveLength(2) + expect(source.match(/ssh-relay-runtime-native-signing-stage\.mjs/g)).toHaveLength(2) + expect(source.match(/ssh-relay-runtime-native-signing-finalization\.mjs/g)).toHaveLength(2) + expect(source).toContain('ssh-relay-runtime-macos-signing.mjs') + expect(source).toContain( + 'signpath/github-action-submit-signing-request@b9d91eadd323de506c0c81cf0c7fe7438f3360fd' + ) + for (const job of Object.values(workflow.jobs)) { + const names = job.steps.map((step) => step.name) + expect(names.indexOf('Reconstruct authenticated unsigned runtime')).toBeGreaterThan( + names.indexOf('Download exact native build output') + ) + expect(names.indexOf('Finalize verified signed runtime')).toBeGreaterThan( + names.indexOf('Stage exact native signing payload') + ) + expect(names.indexOf('Upload immutable signed runtime output')).toBeGreaterThan( + names.indexOf('Finalize verified signed runtime') + ) + } + // Why: credentialed jobs stay inert until the separately gated release DAG is reviewed. + for (const consumer of [build, releaseCut, releaseMac]) { + expect(consumer).not.toContain('ssh-relay-runtime-native-signing.yml') + } + }) +}) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 6e800b58fa3..b6c680e47e7 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -115,12 +115,21 @@ describe('SSH relay runtime artifact workflow', () => { 'manifest-signing-handoff', 'manifest-aggregate', 'manifest-tuple', - 'post-sign-metadata' + 'post-sign-metadata', + 'archive-extraction', + 'macos-signing', + 'native-signing-finalization' ]) { 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-stage-report\.mjs/g + ) + ).toHaveLength(2) + expect(source.match(/ssh-relay-runtime-native-signing-workflow\.test\.mjs/g)).toHaveLength(4) expect( source.match(/node --check config\/scripts\/ssh-relay-runtime-native-signing-plan\.mjs/g) ).toHaveLength(2) From 0c43b83e8260a252def164fe62d135177e79f56b Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 08:23:47 -0400 Subject: [PATCH 122/267] docs(ssh): record signing workflow local proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 21 +++- ...github-release-implementation-checklist.md | 105 ++++++++++++++++-- 2 files changed, 110 insertions(+), 16 deletions(-) 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 40c8bfb135e..7f523978611 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 @@ -60,7 +60,7 @@ complete a box. 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. -- [ ] **WP3 active implementation — Tuple descriptors proven; regenerate post-sign metadata next.** +- [ ] **WP3 active implementation — disconnected native signing workflow locally proven.** Windows compatibility-kind parity is closed locally and on all six target-native jobs in exact-head run [29393022768](https://github.com/stablyai/orca/actions/runs/29393022768) under @@ -105,9 +105,16 @@ complete a box. `E-M4-POST-SIGN-METADATA-CI-001`. PR Checks [29409257568](https://github.com/stablyai/orca/actions/runs/29409257568) and Golden E2E [29409257636](https://github.com/stablyai/orca/actions/runs/29409257636) are green. The artifact - run is red only for the separately declared Windows arm64 floor mismatch. Next, add a - disconnected reusable build-prerequisite contract without connecting release-cut, signing, - publication, desktop consumers, or any tuple. Merging to `main` remains prohibited. + run is red only for the separately declared Windows arm64 floor mismatch. The reusable native + build prerequisite is closed locally and on all six build jobs under + `E-M4-BUILD-PREREQUISITE-CI-001`. The next disconnected package reconstructs authenticated + unsigned archives, signs only the exact native payload, applies returned bytes into an + exclusive full tree, verifies native policy before PTY/watcher smoke, and regenerates the final + archive, SBOM, provenance, and tuple descriptor. Its purpose-named RED and locally green + implementation are recorded under `E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-RED-001` and + `E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-001`. Exact-head native schema/contract execution and real + Apple/SignPath returned-byte/native-trust rehearsals remain open. Release-cut, publication, + desktop consumers, and every tuple remain disconnected. Merging to `main` remains prohibited. ## Work packages, in required order @@ -198,7 +205,11 @@ complete a box. `E-M4-BUILD-PREREQUISITE-LOCAL-001`, `E-M4-BUILD-PREREQUISITE-CI-001`). - [ ] **In progress — 2026-07-15, Codex implementation owner:** add platform-native signing jobs; hash only the returned signed bytes. Begin with a purpose-named disconnected workflow RED; - no test double counts as real signing/native-trust evidence. + no test double counts as real signing/native-trust evidence. The workflow, authenticated + reconstruction, bounded signing boundary, transactional finalization, and credential-free + regression gates are locally green (`E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-RED-001`, + `E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-001`). Exact-head native contract proof and real protected + Apple/SignPath/native-trust evidence remain required before this item can close. - [ ] Add a fail-closed aggregate and immutable manifest-signing job. - [ ] 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. 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 0527aa52a56..1374046b717 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 platform-native signing-job workflow contract — **In progress — 2026-07-15, Codex implementation owner**. The reusable credential-free target-native build-prerequisite package is closed locally and on all six exact-head Node 24 native build jobs under E-M4-BUILD-PREREQUISITE-LOCAL-RED-001, E-M4-BUILD-PREREQUISITE-LOCAL-001, and E-M4-BUILD-PREREQUISITE-CI-001. The active slice begins with a purpose-named RED for disconnected platform-native signing jobs that consume exact native build outputs and return immutable signed bytes before final hashing. A credential-free test path may prove workflow shape and failure behavior but cannot satisfy real Apple/SignPath signing, returned production signatures, or native-trust evidence. Release-cut and every desktop build remain disconnected until exact floors, real signing/trust, aggregate, publication/read-back, and embedding gates are complete. The slice may not publish, connect a desktop consumer, 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 run 29412010582 at `994daefabcf9fba8d8d614bb6246d189c34c6b3f` passes the new reusable prerequisite contract, full release-side suite, native build-twice/equality, archive inspection, bundled Node/PTY/watcher smoke, and unpublished evidence upload on all six native build jobs under E-M4-BUILD-PREREQUISITE-CI-001. PR Checks 29412010603 pass. Golden E2E 29412010624 passes on macOS first-attempt and Linux retry; the first Linux attempt failed before tests while compiling `node-pty`, then the unchanged retry passed dependency install, app build, and Golden tests. The artifact run is red only for the declared Windows arm64 oldest-floor mismatch: hosted build 26200 does not equal required build 26100. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, exact macOS 13.5/Linux kernel 4.18/Windows arm64 build-26100 runners, protected manifest signing, publication wiring, desktop embedding, and native trust remain separately gated. Nothing is published or enabled, release-cut and both desktop release consumers remain disconnected, legacy remains the production default, and merge to `main` remains prohibited.
      +Current phase: Milestone 4 / Work Package 3 platform-native signing-job workflow contract — **In progress — 2026-07-15, Codex implementation owner**. The reusable credential-free target-native build-prerequisite package is closed locally and on all six exact-head Node 24 native build jobs under E-M4-BUILD-PREREQUISITE-LOCAL-RED-001, E-M4-BUILD-PREREQUISITE-LOCAL-001, and E-M4-BUILD-PREREQUISITE-CI-001. The active disconnected signing slice now reconstructs the authenticated unsigned archive, binds the exact staged native payload to the source identity, returns changed signed bytes through platform-native Apple/SignPath boundaries, applies them into an exclusive complete runtime, verifies the complete final tree and native policy before PTY/watcher smoke, and only then emits the final archive, SBOM, provenance, and tuple descriptor. Credential-free local proof is complete under E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-001, but it cannot satisfy real Apple/SignPath signing, returned production signatures, or native-trust evidence. Exact-head native contract execution is the next internal gate. Release-cut and every desktop build remain disconnected until exact floors, real signing/trust, aggregate, publication/read-back, and embedding gates are complete. The slice may not publish, connect a desktop consumer, 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** — the purpose-named signing-workflow RED is now green locally through authenticated archive reconstruction, exact stage-report validation, bounded macOS codesign invocation, SignPath workflow wiring, transactional full-tree finalization, target-native verification ordering, PTY/watcher smoke handoff, and regenerated final metadata/tuple assets under E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-001. The callable workflow has only read permissions, all actions are SHA-pinned, and release-cut, release-mac-build, the reusable native builder, and desktop consumers remain disconnected. No production signing credential or real returned signature was used. Exact-head credential-free execution on all six native build jobs remains next. The preceding exact-head artifact run 29412010582, PR Checks 29412010603, and Golden E2E 29412010624 retain their recorded status; the artifact run is red only for the declared Windows arm64 oldest-floor mismatch (hosted build 26200 versus required build 26100). Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, exact macOS 13.5/Linux kernel 4.18/Windows arm64 build-26100 runners, protected manifest signing, publication wiring, desktop embedding, and native trust remain separately gated. Nothing is published or enabled, 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) @@ -10161,6 +10161,89 @@ diff --check`. - Follow-up: add a disconnected purpose-named platform-native signing-job workflow RED while preserving every real-signing/native-trust, release-consumer, publication, and tuple gate. +### E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-RED-001 — Native signing workflow is absent + +- Date: 2026-07-15 +- Commit SHA / PR: uncommitted purpose-named test atop + `48ee5f5bb`; draft PR #8741 +- Runner: macOS 26.2 arm64, native local worktree; Node v26.0.0 and pnpm 10.24.0 +- Remote: not applicable; disconnected workflow contract test only +- Transport/network: local workflow/source files only; no GitHub Actions invocation, credential, + signer, SSH host, publication, desktop consumer, production/default call, or enabled tuple +- Command: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-native-signing-workflow.test.mjs` +- Result: expected FAIL, 1 file / 1 test in 163 ms test duration / 0.98 seconds wall; + 131,710,976-byte maximum RSS, 95,917,480-byte peak memory footprint, and zero swaps. The failure + is `ENOENT` for `.github/workflows/ssh-relay-runtime-native-signing.yml`. +- Oracle proved: no callable signing workflow exists. The RED defines a callable-only, read-only, + action-SHA-pinned two-job boundary over native Darwin/Windows x64/arm64 runners with bounded + timeouts and exact existing credential names, while asserting build/release consumers remain + disconnected. +- Does not prove: workflow implementation or schema acceptance, signing-payload finalization, + credential availability, Apple/SignPath invocation/approval, returned signed bytes, native trust, + aggregate/publication behavior, SSH behavior, or an enabled tuple. +- Checklist items satisfied: RED prerequisite for the active platform-native signing-job package. +- Follow-up: implement the exact post-return finalization capability and disconnected callable + workflow without connecting any release consumer; then run focused/broad local and credential-free + exact-head schema tests. Real signing/native-trust evidence remains separately required. + +### E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-001 — Disconnected signing and finalization are locally green + +- Date: 2026-07-15 +- Commit SHA / PR: capability commit `d8d74d21442778e9bac672112528067542d19817` and disconnected + workflow commit `2a7083d278e7a14b5b159555df90b1e84a2a41e7`; draft PR #8741 +- Runner: macOS 26.2 arm64, native local worktree; Node v26.0.0 and pnpm 10.24.0 +- Remote: not applicable; credential-free local workflow, archive, signing-boundary, finalization, + metadata, and desktop-parity tests +- Transport/network: local files only. No GitHub Actions invocation, production signing credential, + Apple timestamp service, SignPath request, SSH host, publication, desktop consumer, + production/default call, or enabled tuple +- Commands and results: + - Focused signing/finalization/workflow suite: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 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-native-signing-workflow.test.mjs config/scripts/ssh-relay-runtime-native-signing-payload.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs` + — PASS, 6 files / 24 tests in 1.37 seconds test duration / 2.11 seconds wall; + 167,133,184-byte maximum RSS, 96,064,912-byte peak memory footprint, and zero swaps. + - 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, 46 files / 241 tests in 24.51 seconds test duration / 25.71 seconds wall; + 189,956,096-byte maximum RSS, 96,359,896-byte peak memory footprint, and zero swaps. + - Desktop schema/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 564 ms test duration / 1.27 seconds wall; + 131,874,816-byte maximum RSS, 95,966,464-byte peak memory footprint, and zero swaps. + - `/usr/bin/time -l pnpm run typecheck` — PASS in 2.57 seconds wall with + 1,225,637,888-byte maximum RSS, 96,114,016-byte peak memory footprint, and zero swaps. + - `/usr/bin/time -l pnpm run lint` — PASS in 27.02 seconds wall with + 2,008,678,400-byte maximum RSS, 96,097,584-byte peak memory footprint, and zero swaps; all 41 + reliability gates, the 355-entry max-lines ratchet, bundled-skill guides, localization + catalog/parity, and localization coverage pass. All 26 warnings are in untouched existing files. + - Focused `pnpm exec oxfmt --check`, `pnpm exec oxlint`, `node --check`, `git diff --check`, and + zero diff in both user-owned Node/npm resolver files — PASS. +- Oracle proved: a caller-only, read-only, SHA-pinned workflow defines exact Darwin and Windows + x64/arm64 signing jobs without a release or desktop consumer. The unsigned build archive is size- + and SHA-256-bound, strictly inspected, extracted into an exclusive directory, verified as a full + tree, and rechecked for mutation. The stage report is reconstructed from the authenticated source + identity; official Node never enters the signing payload. macOS invokes bounded argument-array + `codesign` over every exact non-Node native target from an ephemeral Developer ID keychain. + Windows uploads only the exact unsigned payload to the existing fixed SignPath project, + `release-signing` policy, and `windows-inner-binaries-zip` configuration, requires approver + notification, and bounds returned-artifact wait to four hours. Finalization accepts only the exact + changed returned tree, applies it into a new complete runtime, hashes the full tree, runs native + signature policy before bundled Node/PTY/watcher smoke, and only then creates final-content-ID + archive/SBOM/provenance/tuple assets. Any failure removes the complete candidate output. Both + native build test families execute the new credential-free contracts. +- Does not prove: GitHub Actions schema acceptance, Node 24 execution on any hosted runner, access to + Apple or SignPath secrets, a real Developer ID timestamp/signature, SignPath submission or manual + approval behavior, returned production bytes, notarization/Gatekeeper, Defender/WDAC, oldest + baselines, protected aggregate/manifest signing, publication/read-back, desktop embedding, SSH + transfer/install, or an enabled tuple. Synthetic codesign mutation and injected native reports in + local tests do not count as real signing or native-trust evidence. +- Checklist items satisfied: local implementation and credential-free regression prerequisite for + the active platform-native signing-job package. The Milestone 4 signing box remains open. +- Follow-up: commit and push this coherent package, require the purpose-named contracts on every + exact-head native build job, and record Actions schema/job evidence. Keep real protected + Apple/SignPath/native-trust rehearsals and every release/default consumer separately gated. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -10218,15 +10301,15 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Add a purpose-named RED for disconnected platform-native signing jobs that consume the exact native -build outputs, return immutable signed bytes before final hashes, settle bounded failure/timeout -paths, and remain absent from release-cut and desktop consumers. Credential-free test doubles may -prove workflow shape but cannot satisfy real Apple/SignPath signing or native trust. Do not connect -release-cut or any desktop build; Windows arm64 build-26100, macOS 13.5, Linux kernel 4.18, real -signing/trust, aggregate, publication/read-back, and embedding remain prerequisites. Do not merge to -`main`, publish, use production signing credentials, or enable a tuple. Retain every -production/default gate. In parallel, provision the qualifying exact-floor and real macOS/Windows -signing/trust environments. +Commit and push the locally green disconnected signing/finalization package recorded under +E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-001, then require its credential-free purpose-named contracts to +pass on every exact-head native build job. Record GitHub Actions schema acceptance, runner identity, +test counts, timings, and residual failures. Do not connect release-cut or any desktop build; +Windows arm64 build-26100, macOS 13.5, Linux kernel 4.18, real Apple/SignPath returned bytes and +native trust, protected aggregate/manifest signing, publication/read-back, and embedding remain +prerequisites. Do not merge to `main`, publish, use production signing credentials during ordinary +PR validation, or enable a tuple. Retain every production/default gate. In parallel, provision the +qualifying exact-floor and real macOS/Windows signing/trust environments. 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 From 85c70c5e902e6ff5725fa11de054d83f2b821850 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 08:32:20 -0400 Subject: [PATCH 123/267] test(ssh): use native signing archive fixtures Co-authored-by: Orca --- ...-relay-runtime-archive-extraction.test.mjs | 8 +- ...ntime-native-signing-finalization.test.mjs | 152 ++++++++++++------ 2 files changed, 105 insertions(+), 55 deletions(-) diff --git a/config/scripts/ssh-relay-runtime-archive-extraction.test.mjs b/config/scripts/ssh-relay-runtime-archive-extraction.test.mjs index c5843413d2c..7540b9e2eae 100644 --- a/config/scripts/ssh-relay-runtime-archive-extraction.test.mjs +++ b/config/scripts/ssh-relay-runtime-archive-extraction.test.mjs @@ -88,7 +88,10 @@ afterEach(async () => { describe('SSH relay runtime archive extraction', () => { it('reconstructs exact POSIX and Windows runtime trees into exclusive directories', async () => { - for (const tupleId of ['darwin-arm64', 'win32-x64']) { + // Why: NTFS cannot materialize the executable modes needed to construct a POSIX tar fixture. + const tuples = + process.platform === 'win32' ? [`win32-${process.arch}`] : ['darwin-arm64', 'win32-x64'] + for (const tupleId of tuples) { const value = await fixture(tupleId) const outputDirectory = join(value.root, 'reconstructed') const result = await extractSshRelayRuntimeArchive({ @@ -106,7 +109,8 @@ describe('SSH relay runtime archive extraction', () => { }) it('rejects modified archives and existing outputs without retaining partial trees', async () => { - const modified = await fixture('darwin-x64') + const modifiedTuple = process.platform === 'win32' ? `win32-${process.arch}` : 'darwin-x64' + const modified = await fixture(modifiedTuple) await writeFile(modified.archive.path, 'not the authenticated archive') const rejectedOutput = join(modified.root, 'rejected') await expect( diff --git a/config/scripts/ssh-relay-runtime-native-signing-finalization.test.mjs b/config/scripts/ssh-relay-runtime-native-signing-finalization.test.mjs index a06f5f95e8b..8dc8dada2ca 100644 --- a/config/scripts/ssh-relay-runtime-native-signing-finalization.test.mjs +++ b/config/scripts/ssh-relay-runtime-native-signing-finalization.test.mjs @@ -9,7 +9,9 @@ import { expectedSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closu import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' import { finalizeSshRelayRuntimeNativeSigning } from './ssh-relay-runtime-native-signing-finalization.mjs' +import { buildSshRelayRuntimeNativeSigningPlan } from './ssh-relay-runtime-native-signing-plan.mjs' import { prepareSshRelayRuntimeNativeSigningStage } from './ssh-relay-runtime-native-signing-stage.mjs' +import { parseSshRelayRuntimeNativeSigningStageReport } from './ssh-relay-runtime-native-signing-stage-report.mjs' const temporaryDirectories = [] const SOURCE_DATE_EPOCH = 1_788_739_200 @@ -19,7 +21,8 @@ function digest(bytes) { return `sha256:${createHash('sha256').update(bytes).digest('hex')}` } -function toolchain() { +function toolchain(tupleId) { + const platformTool = tupleId.startsWith('win32-') ? 'linker' : 'strip' return Object.fromEntries( [ 'buildNode', @@ -30,7 +33,7 @@ function toolchain() { 'archive', 'nodeAddonApi', 'nodeGyp', - 'strip' + platformTool ].map((name, index) => [ name, { version: `${name} fixture`, sha256: `sha256:${(index + 1).toString(16).repeat(64)}` } @@ -38,13 +41,17 @@ function toolchain() { ) } -async function fixture() { +async function fixture( + tupleId = process.platform === 'win32' ? `win32-${process.arch}` : 'darwin-arm64' +) { const root = await mkdtemp(join(tmpdir(), 'ssh-relay-signing-finalization-')) temporaryDirectories.push(root) const sourceRuntimeRoot = join(root, 'source-runtime') await mkdir(sourceRuntimeRoot) const entries = [] - for (const entry of expectedSshRelayRuntimeClosureEntries('darwin-arm64')) { + // Why: Windows finalization must create a ZIP because NTFS cannot supply POSIX tar modes. + const os = tupleId.startsWith('win32-') ? 'win32' : 'darwin' + for (const entry of expectedSshRelayRuntimeClosureEntries(tupleId)) { const path = join(sourceRuntimeRoot, ...entry.path.split('/')) if (entry.type === 'directory') { await mkdir(path, { recursive: true, mode: entry.mode }) @@ -58,10 +65,10 @@ async function fixture() { } const base = { identitySchemaVersion: 1, - tupleId: 'darwin-arm64', - os: 'darwin', - architecture: 'arm64', - compatibility: sshRelayRuntimeCompatibility['darwin-arm64'], + tupleId, + os, + architecture: tupleId.includes('arm64') ? 'arm64' : 'x64', + compatibility: sshRelayRuntimeCompatibility[tupleId], nodeVersion: '24.18.0', dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, entries @@ -74,11 +81,30 @@ async function fixture() { expandedSize: files.reduce((total, entry) => total + entry.size, 0) } const returnedRoot = join(root, 'returned') + const preserved = new Set([ + 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe', + 'node_modules/node-pty/build/Release/conpty/conpty.dll' + ]) + const assessments = + os === 'win32' + ? buildSshRelayRuntimeNativeSigningPlan(sourceIdentity).signingCandidates.map((entry) => + preserved.has(entry.path) + ? { + path: entry.path, + sourceSha256: entry.sourceSha256, + status: 'valid-upstream', + signerSubject: 'CN=Microsoft Corporation', + signerThumbprint: 'D'.repeat(40) + } + : { path: entry.path, sourceSha256: entry.sourceSha256, status: 'unsigned' } + ) + : [] const report = await prepareSshRelayRuntimeNativeSigningStage({ identity: sourceIdentity, runtimeRoot: sourceRuntimeRoot, stagingRoot: returnedRoot, - platform: 'darwin' + platform: os, + assessWindowsImpl: async () => assessments }) for (const entry of report.signingFiles) { await appendFile(join(returnedRoot, ...entry.path.split('/')), ':signed') @@ -86,18 +112,8 @@ async function fixture() { const nodeRelease = JSON.parse( await readFile(new URL('../ssh-relay-node-release-v24.18.0.json', import.meta.url), 'utf8') ) - const selection = { - tupleId: report.tupleId, - platform: report.platform, - policy: report.policy, - immutableVendorFiles: report.immutableVendorFiles, - signingFiles: report.signingFiles, - preservedUpstreamFiles: report.preservedUpstreamFiles, - verificationFiles: [...report.immutableVendorFiles, ...report.signingFiles].map( - ({ path, role, sourceSha256, sourceSize }) => ({ path, role, sourceSha256, sourceSize }) - ) - } - return { root, sourceRuntimeRoot, returnedRoot, sourceIdentity, selection, nodeRelease } + const { selection } = parseSshRelayRuntimeNativeSigningStageReport(sourceIdentity, report) + return { root, tupleId, sourceRuntimeRoot, returnedRoot, sourceIdentity, selection, nodeRelease } } function nativeReport(sourceIdentity, finalIdentity, selection) { @@ -106,18 +122,41 @@ function nativeReport(sourceIdentity, finalIdentity, selection) { .filter((entry) => entry.type === 'file') .map((entry) => [entry.path, entry]) ) + const immutable = new Set(selection.immutableVendorFiles.map((entry) => entry.path)) + const signing = new Set(selection.signingFiles.map((entry) => entry.path)) return { tupleId: finalIdentity.tupleId, sourceContentId: sourceIdentity.contentId, finalContentId: finalIdentity.contentId, - verifiedFiles: selection.verificationFiles.map((entry) => ({ - path: entry.path, - role: entry.role, - sha256: finalFiles.get(entry.path).sha256, - signerKind: entry.role === 'node' ? 'official-node' : 'orca-built', - authority: `Developer ID Application: Fixture (${entry.role === 'node' ? 'HX7739G8FX' : 'ABCDEFGHIJ'})`, - teamIdentifier: entry.role === 'node' ? 'HX7739G8FX' : 'ABCDEFGHIJ' - })) + verifiedFiles: selection.verificationFiles.map((entry) => { + const signerKind = immutable.has(entry.path) + ? 'official-node' + : signing.has(entry.path) + ? 'orca-built' + : 'preserved-upstream' + const base = { + path: entry.path, + role: entry.role, + sha256: finalFiles.get(entry.path).sha256, + signerKind + } + return sourceIdentity.os === 'win32' + ? { + ...base, + signerSubject: + signerKind === 'official-node' + ? 'CN=OpenJS Foundation' + : signerKind === 'orca-built' + ? 'CN=SignPath Foundation' + : 'CN=Microsoft Corporation', + signerThumbprint: (signerKind === 'preserved-upstream' ? 'D' : 'A').repeat(40) + } + : { + ...base, + authority: `Developer ID Application: Fixture (${signerKind === 'official-node' ? 'HX7739G8FX' : 'ABCDEFGHIJ'})`, + teamIdentifier: signerKind === 'official-node' ? 'HX7739G8FX' : 'ABCDEFGHIJ' + } + }) } } @@ -130,14 +169,17 @@ function finalizationInput(value, outputDirectory) { gitCommit: GIT_COMMIT, builder: 'https://github.com/stablyai/orca/ssh-relay-runtime-native-signing-fixture', runner: { - os: 'macOS', - architecture: 'ARM64', + os: value.sourceIdentity.os === 'win32' ? 'Windows' : 'macOS', + architecture: value.sourceIdentity.architecture.toUpperCase(), environment: 'fixture', - requestedLabel: 'macos-15', - image: { os: 'macos', version: 'fixture' } + requestedLabel: value.sourceIdentity.os === 'win32' ? 'windows-fixture' : 'macos-15', + image: { os: value.sourceIdentity.os, version: 'fixture' } + }, + toolchain: toolchain(value.tupleId), + nativeVerificationTool: { + name: value.sourceIdentity.os === 'win32' ? 'Get-AuthenticodeSignature' : 'codesign', + version: 'fixture-v1' }, - toolchain: toolchain(), - nativeVerificationTool: { name: 'codesign', version: 'fixture-v1' }, verifiedAt: '2026-07-15T12:00:00.000Z', verifyNativeImpl: ({ sourceIdentity, finalIdentity, selection }) => nativeReport(sourceIdentity, finalIdentity, selection), @@ -158,25 +200,29 @@ afterEach(async () => { describe('SSH relay runtime native signing finalization', () => { it('emits only final signed-byte assets after native verification and smoke', async () => { - const value = await fixture() - const outputDirectory = join(value.root, 'final') - const result = await finalizeSshRelayRuntimeNativeSigning( - finalizationInput(value, outputDirectory) - ) + const tuples = + process.platform === 'win32' ? [`win32-${process.arch}`] : ['darwin-arm64', 'win32-x64'] + for (const tupleId of tuples) { + const value = await fixture(tupleId) + const outputDirectory = join(value.root, 'final') + const result = await finalizeSshRelayRuntimeNativeSigning( + finalizationInput(value, outputDirectory) + ) - expect(result.finalContentId).not.toBe(value.sourceIdentity.contentId) - expect(result.returnedFiles).toHaveLength(3) - expect(await readdir(result.assetsRoot)).toHaveLength(4) - expect( - (await readdir(result.assetsRoot)).some((name) => name.endsWith('.manifest-tuple.json')) - ).toBe(true) - expect(await readdir(result.evidenceRoot)).toEqual( - expect.arrayContaining([ - 'darwin-arm64.final-identity.json', - 'darwin-arm64.finalization.json', - 'darwin-arm64.native-verification.json' - ]) - ) + expect(result.finalContentId).not.toBe(value.sourceIdentity.contentId) + expect(result.returnedFiles).toHaveLength(3) + expect(await readdir(result.assetsRoot)).toHaveLength(4) + expect( + (await readdir(result.assetsRoot)).some((name) => name.endsWith('.manifest-tuple.json')) + ).toBe(true) + expect(await readdir(result.evidenceRoot)).toEqual( + expect.arrayContaining([ + `${value.tupleId}.final-identity.json`, + `${value.tupleId}.finalization.json`, + `${value.tupleId}.native-verification.json` + ]) + ) + } }) it('removes the complete output when native trust fails', async () => { From 70b3892aefc31423ad30467ad5251c090842526a Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 08:32:51 -0400 Subject: [PATCH 124/267] docs(ssh): record Windows signing fixture red Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 9 ++- ...github-release-implementation-checklist.md | 61 +++++++++++++++++-- 2 files changed, 63 insertions(+), 7 deletions(-) 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 7f523978611..70a111dce34 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 @@ -209,7 +209,14 @@ complete a box. reconstruction, bounded signing boundary, transactional finalization, and credential-free regression gates are locally green (`E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-RED-001`, `E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-001`). Exact-head native contract proof and real protected - Apple/SignPath/native-trust evidence remain required before this item can close. + Apple/SignPath/native-trust evidence remain required before this item can close. First + exact-head run + [29415080004](https://github.com/stablyai/orca/actions/runs/29415080004) is the required Windows + CI RED: both Windows contract jobs reject two hardcoded Darwin tar fixtures because NTFS cannot + materialize their declared executable modes, while all four POSIX native build jobs pass and + strict production mode validation remains unchanged (`E-M4-NATIVE-SIGNING-WORKFLOW-CI-RED-001`). + The test-only native ZIP fixture correction at `85c70c5e9` and full 241-test release suite pass + locally; a replacement all-six exact-head run is next. - [ ] Add a fail-closed aggregate and immutable manifest-signing job. - [ ] 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. 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 1374046b717..aa98fab8401 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 platform-native signing-job workflow contract — **In progress — 2026-07-15, Codex implementation owner**. The reusable credential-free target-native build-prerequisite package is closed locally and on all six exact-head Node 24 native build jobs under E-M4-BUILD-PREREQUISITE-LOCAL-RED-001, E-M4-BUILD-PREREQUISITE-LOCAL-001, and E-M4-BUILD-PREREQUISITE-CI-001. The active disconnected signing slice now reconstructs the authenticated unsigned archive, binds the exact staged native payload to the source identity, returns changed signed bytes through platform-native Apple/SignPath boundaries, applies them into an exclusive complete runtime, verifies the complete final tree and native policy before PTY/watcher smoke, and only then emits the final archive, SBOM, provenance, and tuple descriptor. Credential-free local proof is complete under E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-001, but it cannot satisfy real Apple/SignPath signing, returned production signatures, or native-trust evidence. Exact-head native contract execution is the next internal gate. Release-cut and every desktop build remain disconnected until exact floors, real signing/trust, aggregate, publication/read-back, and embedding gates are complete. The slice may not publish, connect a desktop consumer, 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** — the purpose-named signing-workflow RED is now green locally through authenticated archive reconstruction, exact stage-report validation, bounded macOS codesign invocation, SignPath workflow wiring, transactional full-tree finalization, target-native verification ordering, PTY/watcher smoke handoff, and regenerated final metadata/tuple assets under E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-001. The callable workflow has only read permissions, all actions are SHA-pinned, and release-cut, release-mac-build, the reusable native builder, and desktop consumers remain disconnected. No production signing credential or real returned signature was used. Exact-head credential-free execution on all six native build jobs remains next. The preceding exact-head artifact run 29412010582, PR Checks 29412010603, and Golden E2E 29412010624 retain their recorded status; the artifact run is red only for the declared Windows arm64 oldest-floor mismatch (hosted build 26200 versus required build 26100). Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, exact macOS 13.5/Linux kernel 4.18/Windows arm64 build-26100 runners, protected manifest signing, publication wiring, desktop embedding, and native trust remain separately gated. Nothing is published or enabled, legacy remains the production default, and merge to `main` remains prohibited.
      +Current phase: Milestone 4 / Work Package 3 platform-native signing-job workflow contract — **In progress — 2026-07-15, Codex implementation owner**. The reusable credential-free target-native build-prerequisite package is closed locally and on all six exact-head Node 24 native build jobs under E-M4-BUILD-PREREQUISITE-LOCAL-RED-001, E-M4-BUILD-PREREQUISITE-LOCAL-001, and E-M4-BUILD-PREREQUISITE-CI-001. The active disconnected signing slice reconstructs the authenticated unsigned archive, binds the exact staged native payload to the source identity, returns changed signed bytes through platform-native Apple/SignPath boundaries, applies them into an exclusive complete runtime, verifies the complete final tree and native policy before PTY/watcher smoke, and only then emits the final archive, SBOM, provenance, and tuple descriptor. Credential-free local proof is complete under E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-001. First exact-head run 29415080004 produced the required Windows fixture RED under E-M4-NATIVE-SIGNING-WORKFLOW-CI-RED-001; the test-only native archive-family correction is locally green and replacement exact-head execution is next. No credential-free path can satisfy real Apple/SignPath signing, returned production signatures, or native-trust evidence. Release-cut and every desktop build remain disconnected until exact floors, real signing/trust, aggregate, publication/read-back, and embedding gates are complete. The slice may not publish, connect a desktop consumer, 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 run 29415080004 at `0c43b83e8260a252def164fe62d135177e79f56b` proves GitHub accepts the disconnected callable signing workflow and all four POSIX native build jobs pass the new credential-free contracts plus complete runtime builds. Both Windows contract jobs fail before Node input download, native build, or artifact upload because two new tests hardcode a Darwin tar fixture whose declared executable modes cannot be materialized by NTFS; strict archive mode validation correctly rejects `bin/node` (`E-M4-NATIVE-SIGNING-WORKFLOW-CI-RED-001`). The correction selects the native Windows ZIP tuple on Windows while POSIX runners retain both tar and ZIP coverage; the focused suite and 46-file / 241-test release suite pass locally without changing production validation. A replacement exact-head run is next. Golden E2E 29415079993 passes; PR Checks 29415080046 remain independent. The callable signing workflow still has only read permissions and no consumer, and no production signing credential or real returned signature was used. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, exact macOS 13.5/Linux kernel 4.18/Windows arm64 build-26100 runners, protected manifest signing, publication wiring, desktop embedding, and native trust remain separately gated. Nothing is published or enabled, 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) @@ -10244,6 +10244,55 @@ diff --check`. exact-head native build job, and record Actions schema/job evidence. Keep real protected Apple/SignPath/native-trust rehearsals and every release/default consumer separately gated. +### E-M4-NATIVE-SIGNING-WORKFLOW-CI-RED-001 — Windows rejects non-native POSIX test fixtures + +- Date: 2026-07-15 +- Commit SHA / PR: `0c43b83e8260a252def164fe62d135177e79f56b`; draft PR #8741 +- Workflow/run: SSH Relay Runtime Artifacts run + [29415080004](https://github.com/stablyai/orca/actions/runs/29415080004) +- Runners and failed jobs: + - Windows x64 job + [87351075144](https://github.com/stablyai/orca/actions/runs/29415080004/job/87351075144): + GitHub-hosted `windows-2022`, runner `GitHub Actions 1000055729`; contract-test step fails from + 12:26:12Z through 12:26:39Z. + - Windows arm64 job + [87351075167](https://github.com/stablyai/orca/actions/runs/29415080004/job/87351075167): + GitHub-hosted `windows-11-arm`, runner `GitHub Actions 1000055732`; contract-test step fails from + 12:28:14Z through 12:28:37Z. +- Remote/transport/network: GitHub-hosted native test runners only. Both failures occur before Node + release-input download, native runtime construction, artifact upload, signing credentials, + SignPath/Apple calls, SSH, publication, desktop consumption, or an enabled tuple. +- Result: expected CI RED in both Windows jobs. GitHub annotations identify the same two failures: + `ssh-relay-runtime-archive-extraction.test.mjs` and + `ssh-relay-runtime-native-signing-finalization.test.mjs` each construct a hardcoded Darwin tar + fixture, and strict archive inspection rejects `bin/node` with `Runtime archive type or mode +mismatch`. NTFS cannot materialize the fixture's declared POSIX executable mode. Darwin arm64, + Darwin x64, Linux arm64, and Linux x64 native build jobs all pass the new credential-free contracts + and complete build/inspection/smoke/upload on the same exact head. Golden E2E run 29415079993 + passes. +- Oracle proved: GitHub accepts the new callable workflow schema and action pins; the failure is a + test-fixture portability defect rather than a production archive relaxation. The production + inspector retains exact mode enforcement and blocks both Windows jobs before any candidate output + can be built or uploaded. +- Correction commit: `85c70c5e902e6ff5725fa11de054d83f2b821850`. +- Correction and local proof: the archive reconstruction test now selects native + `win32-${process.arch}` ZIP fixtures on Windows and retains both tar and ZIP coverage on POSIX. The + finalization test does the same while preserving the real Windows policy shape: official Node, + three returned SignPath candidates, and two exact preserved upstream ConPTY files. Focused 3 files + / 10 tests pass locally. The exact broad command + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay*.test.mjs` + passes 46 files / 241 tests in 20.07 seconds test duration / 22.43 seconds wall with + 190,513,152-byte maximum RSS, 96,048,528-byte peak memory footprint, and zero swaps. Focused + `oxfmt`, `oxlint`, syntax, and `git diff --check` gates pass. +- Does not prove: corrected execution on either native Windows runner, a production signing job, + Apple/SignPath credentials or returned bytes, native trust, oldest baselines, protected aggregate, + publication/read-back, desktop embedding, SSH behavior, or an enabled tuple. +- Checklist items satisfied: required CI RED and locally green test-fixture-only correction for the + active signing workflow package. The package remains open pending replacement all-six exact-head + execution. +- Follow-up: commit and push the fixture-only correction, then require both Windows architectures + and all four POSIX jobs to pass on the replacement exact head. Do not weaken archive mode checks. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -10301,10 +10350,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the locally green disconnected signing/finalization package recorded under -E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-001, then require its credential-free purpose-named contracts to -pass on every exact-head native build job. Record GitHub Actions schema acceptance, runner identity, -test counts, timings, and residual failures. Do not connect release-cut or any desktop build; +Commit and push the native test-fixture correction recorded under +E-M4-NATIVE-SIGNING-WORKFLOW-CI-RED-001, then require its credential-free purpose-named contracts to +pass on every replacement exact-head native build job. Record runner identity, test counts, timings, +and residual failures. Do not connect release-cut or any desktop build; Windows arm64 build-26100, macOS 13.5, Linux kernel 4.18, real Apple/SignPath returned bytes and native trust, protected aggregate/manifest signing, publication/read-back, and embedding remain prerequisites. Do not merge to `main`, publish, use production signing credentials during ordinary From 5b1a994a31a3a04b6e6602208d5d797809903c4b Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 08:50:38 -0400 Subject: [PATCH 125/267] docs: record native signing workflow CI proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 19 ++-- ...github-release-implementation-checklist.md | 97 ++++++++++++++++--- 2 files changed, 99 insertions(+), 17 deletions(-) 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 70a111dce34..68c886c182b 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 @@ -112,9 +112,10 @@ complete a box. exclusive full tree, verifies native policy before PTY/watcher smoke, and regenerates the final archive, SBOM, provenance, and tuple descriptor. Its purpose-named RED and locally green implementation are recorded under `E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-RED-001` and - `E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-001`. Exact-head native schema/contract execution and real - Apple/SignPath returned-byte/native-trust rehearsals remain open. Release-cut, publication, - desktop consumers, and every tuple remain disconnected. Merging to `main` remains prohibited. + `E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-001`. Exact-head native schema/contract execution is closed + under `E-M4-NATIVE-SIGNING-WORKFLOW-CI-001`; real Apple/SignPath returned-byte/native-trust + rehearsals remain open. Release-cut, publication, desktop consumers, and every tuple remain + disconnected. Merging to `main` remains prohibited. ## Work packages, in required order @@ -208,15 +209,21 @@ complete a box. no test double counts as real signing/native-trust evidence. The workflow, authenticated reconstruction, bounded signing boundary, transactional finalization, and credential-free regression gates are locally green (`E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-RED-001`, - `E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-001`). Exact-head native contract proof and real protected - Apple/SignPath/native-trust evidence remain required before this item can close. First + `E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-001`). Exact-head native contract proof is complete; real + protected Apple/SignPath/native-trust evidence remains required before this item can close. First exact-head run [29415080004](https://github.com/stablyai/orca/actions/runs/29415080004) is the required Windows CI RED: both Windows contract jobs reject two hardcoded Darwin tar fixtures because NTFS cannot materialize their declared executable modes, while all four POSIX native build jobs pass and strict production mode validation remains unchanged (`E-M4-NATIVE-SIGNING-WORKFLOW-CI-RED-001`). The test-only native ZIP fixture correction at `85c70c5e9` and full 241-test release suite pass - locally; a replacement all-six exact-head run is next. + locally. Replacement exact-head run + [29415642475](https://github.com/stablyai/orca/actions/runs/29415642475) at `70b3892ae` is + complete: all six target-native jobs pass the corrected contracts and full runtime build, + smoke, equality, and upload under `E-M4-NATIVE-SIGNING-WORKFLOW-CI-001`. Both Linux userland + supplements and Windows x64 baseline pass. The run remains red only for the separately + declared Windows arm64 floor mismatch (hosted build 26200 versus required 26100). Golden E2E + and PR Checks are green at the exact head. Real signing/native trust is still open. - [ ] Add a fail-closed aggregate and immutable manifest-signing job. - [ ] 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. 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 aa98fab8401..a62b0ce95a9 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 platform-native signing-job workflow contract — **In progress — 2026-07-15, Codex implementation owner**. The reusable credential-free target-native build-prerequisite package is closed locally and on all six exact-head Node 24 native build jobs under E-M4-BUILD-PREREQUISITE-LOCAL-RED-001, E-M4-BUILD-PREREQUISITE-LOCAL-001, and E-M4-BUILD-PREREQUISITE-CI-001. The active disconnected signing slice reconstructs the authenticated unsigned archive, binds the exact staged native payload to the source identity, returns changed signed bytes through platform-native Apple/SignPath boundaries, applies them into an exclusive complete runtime, verifies the complete final tree and native policy before PTY/watcher smoke, and only then emits the final archive, SBOM, provenance, and tuple descriptor. Credential-free local proof is complete under E-M4-NATIVE-SIGNING-WORKFLOW-LOCAL-001. First exact-head run 29415080004 produced the required Windows fixture RED under E-M4-NATIVE-SIGNING-WORKFLOW-CI-RED-001; the test-only native archive-family correction is locally green and replacement exact-head execution is next. No credential-free path can satisfy real Apple/SignPath signing, returned production signatures, or native-trust evidence. Release-cut and every desktop build remain disconnected until exact floors, real signing/trust, aggregate, publication/read-back, and embedding gates are complete. The slice may not publish, connect a desktop consumer, 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 run 29415080004 at `0c43b83e8260a252def164fe62d135177e79f56b` proves GitHub accepts the disconnected callable signing workflow and all four POSIX native build jobs pass the new credential-free contracts plus complete runtime builds. Both Windows contract jobs fail before Node input download, native build, or artifact upload because two new tests hardcode a Darwin tar fixture whose declared executable modes cannot be materialized by NTFS; strict archive mode validation correctly rejects `bin/node` (`E-M4-NATIVE-SIGNING-WORKFLOW-CI-RED-001`). The correction selects the native Windows ZIP tuple on Windows while POSIX runners retain both tar and ZIP coverage; the focused suite and 46-file / 241-test release suite pass locally without changing production validation. A replacement exact-head run is next. Golden E2E 29415079993 passes; PR Checks 29415080046 remain independent. The callable signing workflow still has only read permissions and no consumer, and no production signing credential or real returned signature was used. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, exact macOS 13.5/Linux kernel 4.18/Windows arm64 build-26100 runners, protected manifest signing, publication wiring, desktop embedding, and native trust remain separately gated. Nothing is published or enabled, legacy remains the production default, and merge to `main` remains prohibited.
      +Current phase: Milestone 4 / Work Package 3 protected platform-native signing rehearsal — **In progress — 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 next narrow package is a manual-only rehearsal caller that can produce the existing unpublished native artifacts and invoke the callable signing workflow with repository credentials without connecting release-cut, desktop builds, publication, or any tuple. It requires a purpose-named RED and must preserve the expected Windows-arm64 floor failure as a separate gate rather than bypassing it in production. Release-cut and every desktop build remain disconnected until exact floors, real signing/trust, aggregate, publication/read-back, and embedding gates are complete. 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** — replacement exact-head artifact run 29415642475 at `70b3892aefc31423ad30467ad5251c090842526a` is complete under `E-M4-NATIVE-SIGNING-WORKFLOW-CI-001`. All six target-native build jobs pass the corrected credential-free contracts, reproducible full runtime construction, inspection, bundled Node/PTY/watcher smoke, and unpublished artifact upload. Both Linux oldest-userland supplements and Windows x64 oldest-baseline verification pass. The overall run fails only because the separately retained Windows arm64 floor gate requires build 26100 while GitHub supplied 26200; that job still verifies the 85,213,511-byte tree and passes bundled Node, PTY, watcher, and resource-settlement smoke before refusing qualification. Golden E2E 29415642509 and PR Checks 29415642512 both pass at the exact head. No production signing credential or real returned signature was used by this credential-free run. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, exact macOS 13.5/Linux kernel 4.18/Windows arm64 build-26100 runners, protected manifest signing, publication wiring, desktop embedding, and native trust remain separately gated. Nothing is published or enabled, 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) @@ -10293,6 +10293,82 @@ mismatch`. NTFS cannot materialize the fixture's declared POSIX executable mode. - Follow-up: commit and push the fixture-only correction, then require both Windows architectures and all four POSIX jobs to pass on the replacement exact head. Do not weaken archive mode checks. +### E-M4-NATIVE-SIGNING-WORKFLOW-CI-001 — Corrected signing contracts pass all six native builds + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Commit SHA / PR: exact head `70b3892aefc31423ad30467ad5251c090842526a`; correction commit + `85c70c5e902e6ff5725fa11de054d83f2b821850`; draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). +- Workflow/run: SSH Relay Runtime Artifacts + [29415642475](https://github.com/stablyai/orca/actions/runs/29415642475), 12:33:09Z–12:45:41Z. + This is a credential-free exact-head replacement for the required Windows RED. It does not call + the signing workflow or use repository secrets. +- Target-native build jobs and resolved images: + - Linux x64 job + [87352975773](https://github.com/stablyai/orca/actions/runs/29415642475/job/87352975773): + `ubuntu-24.04`, `ubuntu24` image `20260705.232.1`, X64, 12:33:13Z–12:38:05Z. + - Linux arm64 job + [87352975853](https://github.com/stablyai/orca/actions/runs/29415642475/job/87352975853): + `ubuntu-24.04-arm`, `ubuntu24-arm64` image `20260706.52.2`, ARM64, + 12:33:16Z–12:37:57Z. + - macOS x64 job + [87352975950](https://github.com/stablyai/orca/actions/runs/29415642475/job/87352975950): + `macos-15-intel`, `macos15` image `20260629.0276.1`, X64, + 12:33:14Z–12:39:14Z. + - macOS arm64 job + [87352975799](https://github.com/stablyai/orca/actions/runs/29415642475/job/87352975799): + `macos-15`, `macos15` image `20260706.0213.1`, ARM64, 12:33:14Z–12:37:31Z. + - Windows x64 job + [87352975877](https://github.com/stablyai/orca/actions/runs/29415642475/job/87352975877): + `windows-2022`, `win22` image `20260706.237.1`, X64, 12:33:13Z–12:38:30Z. + - Windows arm64 job + [87352975797](https://github.com/stablyai/orca/actions/runs/29415642475/job/87352975797): + `windows-11-arm`, `win11-arm64` image `20260706.102.1`, ARM64, + 12:33:13Z–12:41:51Z. +- Contract results: every job checks out the exact 40-hex source and passes the new archive + reconstruction, signing-stage, returned-tree finalization, and callable-workflow contracts before + downloading Node inputs or constructing an artifact. Each POSIX job passes 45 files / 237 tests; + durations are 5.64 seconds on Linux x64, 5.45 seconds on Linux arm64, 10.95 seconds on macOS + arm64, and 24.06 seconds on macOS x64. Each Windows job passes 46 files / 232 tests with nine + declared non-Windows-applicable skips out of 241; durations are 11.63 seconds on x64 and 11.30 + seconds on arm64. Both corrected Windows fixtures exercise the native ZIP/runtime policy while + POSIX retains exact tar-mode coverage; production archive mode validation is unchanged. +- Artifact oracle: all six jobs then complete two clean target-native builds, exact equality, + archive/tree inspection, bundled Node v24.18.0, PTY, watcher, source-signature/native-plan checks, + and unpublished artifact upload. Both supplemental Linux userland jobs pass: + [87354301778](https://github.com/stablyai/orca/actions/runs/29415642475/job/87354301778) + for x64 and + [87354301762](https://github.com/stablyai/orca/actions/runs/29415642475/job/87354301762) + for arm64. Windows x64 oldest-baseline job + [87354877657](https://github.com/stablyai/orca/actions/runs/29415642475/job/87354877657) + also passes. +- Expected retained failure: Windows arm64 baseline job + [87354877689](https://github.com/stablyai/orca/actions/runs/29415642475/job/87354877689) + fails closed only because the contract requires OS build 26100 while the hosted runner reports + `10.0.26200`. Before refusing qualification it authenticates the 60-entry / 42-file / + 85,213,511-expanded-byte tree with content ID + `sha256:02edf462be83c2864a89546d5344d348f9e07ce10964660342608a8c614e47db`, runs bundled Node, + PTY resize/exit, and watcher create/update/delete/rename smoke, settles to two pipe handles after a + 2,000 ms observation, and reports 5,877.1343 ms smoke time, 48,570,368-byte RSS, and 7,671.6441 ms + total verification. Platform and architecture checks pass; only the exact floor check is false. +- Adjacent exact-head regressions: Golden E2E + [29415642509](https://github.com/stablyai/orca/actions/runs/29415642509) passes both macOS and Linux + jobs. PR Checks [29415642512](https://github.com/stablyai/orca/actions/runs/29415642512) pass the + `verify` job from 12:33:13Z through 12:47:00Z. Both use the same exact head. +- Does not prove: real Apple/SignPath credentials, approval, returned signed bytes, native trust, + Gatekeeper/notarization, Defender/WDAC, macOS 13.5, Linux kernel 4.18, Windows arm64 build 26100, + protected manifest signing, release publication/read-back, desktop embedding, SSH + transfer/install, or an enabled tuple. No production/default consumer is connected and no artifact + is published. +- Checklist items satisfied: credential-free exact-head native execution prerequisite for the active + signing-job package. The Work Package 3 signing item remains open pending real protected signing + and native-trust evidence. +- Follow-up: add a purpose-named RED and manual-only rehearsal caller for the existing build and + signing workflows. Keep baseline qualification, release-cut, desktop consumers, publication, and + every tuple disconnected; a rehearsal must not convert the hosted Windows arm64 floor gap into a + pass. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -10350,15 +10426,14 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the native test-fixture correction recorded under -E-M4-NATIVE-SIGNING-WORKFLOW-CI-RED-001, then require its credential-free purpose-named contracts to -pass on every replacement exact-head native build job. Record runner identity, test counts, timings, -and residual failures. Do not connect release-cut or any desktop build; -Windows arm64 build-26100, macOS 13.5, Linux kernel 4.18, real Apple/SignPath returned bytes and -native trust, protected aggregate/manifest signing, publication/read-back, and embedding remain -prerequisites. Do not merge to `main`, publish, use production signing credentials during ordinary -PR validation, or enable a tuple. Retain every production/default gate. In parallel, provision the -qualifying exact-floor and real macOS/Windows signing/trust environments. +Commit and push the exact-head CI evidence recorded under +E-M4-NATIVE-SIGNING-WORKFLOW-CI-001. Then add a purpose-named RED and a manual-only rehearsal caller +that produces the existing unpublished native artifacts and invokes the disconnected signing +workflow with repository credentials. Do not connect release-cut or any desktop build. The +rehearsal must retain the Windows arm64 build-26100, macOS 13.5, and Linux kernel 4.18 qualification +gates as separately open, and must not publish or enable a tuple. Record real Apple/SignPath request, +approval/return, exact changed-byte identity, native trust, runner, duration, and residual evidence. +Do not merge to `main`; retain every production/default gate. 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 From c2db2b62e544139176d0993b1340f9d0c706e017 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 09:00:34 -0400 Subject: [PATCH 126/267] ci: add manual relay runtime signing rehearsal Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 17 ++- ...relay-runtime-native-signing-rehearsal.yml | 61 ++++++++ ...-relay-runtime-build-prerequisite.test.mjs | 19 ++- ...native-signing-rehearsal-workflow.test.mjs | 111 +++++++++++++++ ...elease-implementation-checklist-summary.md | 7 +- ...github-release-implementation-checklist.md | 130 +++++++++++++++--- 6 files changed, 320 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/ssh-relay-runtime-native-signing-rehearsal.yml create mode 100644 config/scripts/ssh-relay-runtime-native-signing-rehearsal-workflow.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 48c52d78826..5b4b8d2682b 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -16,7 +16,13 @@ on: - 'src/relay/**' workflow_dispatch: # Why: callers may gate on the native graph only after separate release wiring is reviewed. - workflow_call: {} + workflow_call: + inputs: + include-baseline-gates: + description: Run separately gated oldest-baseline qualification jobs + required: false + default: true + type: boolean concurrency: group: ssh-relay-runtime-artifacts-${{ github.event.pull_request.number || github.ref }} @@ -174,6 +180,7 @@ jobs: 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-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 pnpm exec vitest run --config config/vitest.config.ts \ config/scripts/ssh-relay-runtime-compatibility.test.mjs \ @@ -210,6 +217,7 @@ jobs: config/scripts/ssh-relay-runtime-macos-signing.test.mjs \ config/scripts/ssh-relay-runtime-native-signing-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 \ config/scripts/ssh-relay-runtime-closure.test.mjs \ config/scripts/ssh-relay-runtime-pty-smoke.test.mjs \ @@ -584,6 +592,8 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime signing 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 + if ($LASTEXITCODE -ne 0) { throw 'runtime native signing rehearsal workflow test syntax check failed' } node --check config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime build prerequisite test syntax check failed' } pnpm exec vitest run --config config/vitest.config.ts ` @@ -621,6 +631,7 @@ jobs: config/scripts/ssh-relay-runtime-macos-signing.test.mjs ` config/scripts/ssh-relay-runtime-native-signing-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 ` config/scripts/ssh-relay-runtime-closure.test.mjs ` config/scripts/ssh-relay-runtime-pty-smoke.test.mjs ` @@ -813,6 +824,8 @@ jobs: verify-linux-runtime-baseline-userland: name: Supplement ${{ matrix.tuple }} oldest userland (kernel gap remains) needs: build-posix-runtime + # Why: signing rehearsals need candidate bytes, while qualification stays a separate default-on gate. + if: github.event_name != 'workflow_call' || inputs.include-baseline-gates strategy: fail-fast: false matrix: @@ -922,6 +935,8 @@ jobs: verify-windows-runtime-baseline: name: Verify ${{ matrix.tuple }} oldest Windows baseline needs: build-windows-runtime + # Why: a newer hosted image must not block signing evidence or be mistaken for floor qualification. + if: github.event_name != 'workflow_call' || inputs.include-baseline-gates strategy: fail-fast: false matrix: diff --git a/.github/workflows/ssh-relay-runtime-native-signing-rehearsal.yml b/.github/workflows/ssh-relay-runtime-native-signing-rehearsal.yml new file mode 100644 index 00000000000..419ea74721f --- /dev/null +++ b/.github/workflows/ssh-relay-runtime-native-signing-rehearsal.yml @@ -0,0 +1,61 @@ +name: SSH Relay Runtime Native Signing Rehearsal + +on: + workflow_dispatch: + inputs: + expected-source-sha: + description: Exact 40-character source commit selected for this credentialed rehearsal + required: true + type: string + confirmation: + description: Type SIGN SSH RELAY RUNTIME ARTIFACTS to authorize the rehearsal + required: true + type: string + +concurrency: + group: ssh-relay-runtime-native-signing-rehearsal + # Why: cancelling after a signing request could abandon an approved set of immutable bytes. + cancel-in-progress: false + +permissions: + contents: read + +jobs: + authorize-rehearsal: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Bind confirmation to the selected exact source + shell: bash + env: + ORCA_RUNTIME_EXPECTED_SOURCE_SHA: ${{ inputs.expected-source-sha }} + ORCA_RUNTIME_CONFIRMATION: ${{ inputs.confirmation }} + ORCA_RUNTIME_SELECTED_SOURCE_SHA: ${{ github.sha }} + run: | + set -euo pipefail + if [[ ! "$ORCA_RUNTIME_EXPECTED_SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo 'Expected source must be an exact lowercase 40-character commit SHA.' >&2 + exit 1 + fi + if [[ "$ORCA_RUNTIME_EXPECTED_SOURCE_SHA" != "$ORCA_RUNTIME_SELECTED_SOURCE_SHA" ]]; then + echo 'Expected source does not match the workflow ref selected for this dispatch.' >&2 + exit 1 + fi + if [[ "$ORCA_RUNTIME_CONFIRMATION" != 'SIGN SSH RELAY RUNTIME ARTIFACTS' ]]; then + echo 'Credentialed signing rehearsal was not explicitly confirmed.' >&2 + exit 1 + fi + + build-native-runtimes: + needs: authorize-rehearsal + uses: ./.github/workflows/ssh-relay-runtime-artifacts.yml + with: + # Why: candidate signing evidence does not qualify a newer hosted OS as the declared floor. + include-baseline-gates: false + + sign-native-runtimes: + needs: build-native-runtimes + uses: ./.github/workflows/ssh-relay-runtime-native-signing.yml + with: + source-sha: ${{ github.sha }} + secrets: inherit diff --git a/config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs b/config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs index 415a585e5c6..a4b3803017b 100644 --- a/config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs +++ b/config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs @@ -19,7 +19,16 @@ describe('SSH relay runtime reusable build prerequisite', () => { ]) const workflow = parse(source) - expect(workflow.on.workflow_call).toEqual({}) + expect(workflow.on.workflow_call).toEqual({ + inputs: { + 'include-baseline-gates': { + description: 'Run separately gated oldest-baseline qualification jobs', + required: false, + default: true, + type: 'boolean' + } + } + }) expect(workflow.permissions).toEqual({ contents: 'read' }) expect(Object.keys(workflow.jobs)).toEqual([ 'build-posix-runtime', @@ -31,6 +40,14 @@ describe('SSH relay runtime reusable build prerequisite', () => { 'build-posix-runtime' ) expect(workflow.jobs['verify-windows-runtime-baseline'].needs).toBe('build-windows-runtime') + for (const jobName of [ + 'verify-linux-runtime-baseline-userland', + 'verify-windows-runtime-baseline' + ]) { + expect(workflow.jobs[jobName].if).toBe( + "github.event_name != 'workflow_call' || inputs.include-baseline-gates" + ) + } for (const job of Object.values(workflow.jobs)) { expect(job.steps[0].with.ref).toBe('${{ github.event.pull_request.head.sha || github.sha }}') } diff --git a/config/scripts/ssh-relay-runtime-native-signing-rehearsal-workflow.test.mjs b/config/scripts/ssh-relay-runtime-native-signing-rehearsal-workflow.test.mjs new file mode 100644 index 00000000000..66b45de4c14 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-native-signing-rehearsal-workflow.test.mjs @@ -0,0 +1,111 @@ +import { readFile } from 'node:fs/promises' + +import { parse } from 'yaml' +import { describe, expect, it } from 'vitest' + +const rehearsalWorkflowUrl = new URL( + '../../.github/workflows/ssh-relay-runtime-native-signing-rehearsal.yml', + import.meta.url +) +const buildWorkflowUrl = new URL( + '../../.github/workflows/ssh-relay-runtime-artifacts.yml', + import.meta.url +) +const signingWorkflowUrl = new URL( + '../../.github/workflows/ssh-relay-runtime-native-signing.yml', + import.meta.url +) +const releaseCutUrl = new URL('../../.github/workflows/release-cut.yml', import.meta.url) +const releaseMacUrl = new URL('../../.github/workflows/release-mac-build.yml', import.meta.url) + +describe('SSH relay runtime native-signing rehearsal workflow', () => { + it('requires an exact manual confirmation and keeps every production consumer disconnected', async () => { + const [rehearsalSource, buildSource, signingSource, releaseCut, releaseMac] = await Promise.all( + [ + readFile(rehearsalWorkflowUrl, 'utf8'), + readFile(buildWorkflowUrl, 'utf8'), + readFile(signingWorkflowUrl, 'utf8'), + readFile(releaseCutUrl, 'utf8'), + readFile(releaseMacUrl, 'utf8') + ] + ) + const rehearsal = parse(rehearsalSource) + const build = parse(buildSource) + const signing = parse(signingSource) + + expect(Object.keys(rehearsal.on)).toEqual(['workflow_dispatch']) + expect(rehearsal.on.workflow_dispatch.inputs).toEqual({ + 'expected-source-sha': { + description: 'Exact 40-character source commit selected for this credentialed rehearsal', + required: true, + type: 'string' + }, + confirmation: { + description: 'Type SIGN SSH RELAY RUNTIME ARTIFACTS to authorize the rehearsal', + required: true, + type: 'string' + } + }) + expect(rehearsal.permissions).toEqual({ contents: 'read' }) + expect(rehearsal.concurrency).toEqual({ + group: 'ssh-relay-runtime-native-signing-rehearsal', + 'cancel-in-progress': false + }) + expect(Object.keys(rehearsal.jobs)).toEqual([ + 'authorize-rehearsal', + 'build-native-runtimes', + 'sign-native-runtimes' + ]) + + const authorization = rehearsal.jobs['authorize-rehearsal'] + expect(authorization['runs-on']).toBe('ubuntu-24.04') + expect(authorization['timeout-minutes']).toBe(5) + expect(authorization.steps.map((step) => step.name)).toEqual([ + 'Bind confirmation to the selected exact source' + ]) + expect(authorization.steps[0].env).toEqual({ + ORCA_RUNTIME_EXPECTED_SOURCE_SHA: '${{ inputs.expected-source-sha }}', + ORCA_RUNTIME_CONFIRMATION: '${{ inputs.confirmation }}', + ORCA_RUNTIME_SELECTED_SOURCE_SHA: '${{ github.sha }}' + }) + expect(authorization.steps[0].run).toContain('SIGN SSH RELAY RUNTIME ARTIFACTS') + expect(authorization.steps[0].run).toContain('^[0-9a-f]{40}$') + expect(authorization.steps[0].run).toContain('ORCA_RUNTIME_SELECTED_SOURCE_SHA') + + expect(build.on.workflow_call.inputs).toEqual({ + 'include-baseline-gates': { + description: 'Run separately gated oldest-baseline qualification jobs', + required: false, + default: true, + type: 'boolean' + } + }) + for (const jobName of [ + 'verify-linux-runtime-baseline-userland', + 'verify-windows-runtime-baseline' + ]) { + expect(build.jobs[jobName].if).toBe( + "github.event_name != 'workflow_call' || inputs.include-baseline-gates" + ) + } + + expect(rehearsal.jobs['build-native-runtimes']).toEqual({ + needs: 'authorize-rehearsal', + uses: './.github/workflows/ssh-relay-runtime-artifacts.yml', + with: { 'include-baseline-gates': false } + }) + expect(rehearsal.jobs['sign-native-runtimes']).toEqual({ + needs: 'build-native-runtimes', + uses: './.github/workflows/ssh-relay-runtime-native-signing.yml', + with: { 'source-sha': '${{ github.sha }}' }, + secrets: 'inherit' + }) + + expect(Object.keys(signing.on)).toEqual(['workflow_call']) + expect(rehearsalSource).not.toMatch(/release|publish|upload-release-asset|contents:\s*write/u) + for (const consumer of [releaseCut, releaseMac]) { + expect(consumer).not.toContain('ssh-relay-runtime-native-signing-rehearsal.yml') + expect(consumer).not.toContain('ssh-relay-runtime-native-signing.yml') + } + }) +}) 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 68c886c182b..c1351f2ac61 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 @@ -223,7 +223,10 @@ complete a box. smoke, equality, and upload under `E-M4-NATIVE-SIGNING-WORKFLOW-CI-001`. Both Linux userland supplements and Windows x64 baseline pass. The run remains red only for the separately declared Windows arm64 floor mismatch (hosted build 26200 versus required 26100). Golden E2E - and PR Checks are green at the exact head. Real signing/native trust is still open. + and PR Checks are green at the exact head. The manual credentialed rehearsal starts with the + missing-caller RED `E-M4-NATIVE-SIGNING-REHEARSAL-LOCAL-RED-001` and is locally green under + `E-M4-NATIVE-SIGNING-REHEARSAL-LOCAL-001`. GitHub cannot dispatch a new workflow before it + exists on the default branch, so real signing/native trust remains blocked without merging. - [ ] Add a fail-closed aggregate and immutable manifest-signing job. - [ ] 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. @@ -279,6 +282,8 @@ complete a box. egress rules, teardown SLA, and cost/capacity ownership. - [ ] Release administrator provisions protected manifest/native-signing environments, reviewers, test keys/certificates, and access auditing. +- [ ] After a separately authorized merge, dispatch the reviewed exact-source native-signing + rehearsal; GitHub cannot dispatch its new workflow file while it exists only in this PR. While blocked, artifact-only and test work may continue. No tuple may be enabled or published. 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 a62b0ce95a9..3f89bcd582c 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 protected platform-native signing rehearsal — **In progress — 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 next narrow package is a manual-only rehearsal caller that can produce the existing unpublished native artifacts and invoke the callable signing workflow with repository credentials without connecting release-cut, desktop builds, publication, or any tuple. It requires a purpose-named RED and must preserve the expected Windows-arm64 floor failure as a separate gate rather than bypassing it in production. Release-cut and every desktop build remain disconnected until exact floors, real signing/trust, aggregate, publication/read-back, and embedding gates are complete. 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** — replacement exact-head artifact run 29415642475 at `70b3892aefc31423ad30467ad5251c090842526a` is complete under `E-M4-NATIVE-SIGNING-WORKFLOW-CI-001`. All six target-native build jobs pass the corrected credential-free contracts, reproducible full runtime construction, inspection, bundled Node/PTY/watcher smoke, and unpublished artifact upload. Both Linux oldest-userland supplements and Windows x64 oldest-baseline verification pass. The overall run fails only because the separately retained Windows arm64 floor gate requires build 26100 while GitHub supplied 26200; that job still verifies the 85,213,511-byte tree and passes bundled Node, PTY, watcher, and resource-settlement smoke before refusing qualification. Golden E2E 29415642509 and PR Checks 29415642512 both pass at the exact head. No production signing credential or real returned signature was used by this credential-free run. Real Apple/SignPath signing, returned production signatures, Gatekeeper/notarization, Defender/WDAC, exact macOS 13.5/Linux kernel 4.18/Windows arm64 build-26100 runners, protected manifest signing, publication wiring, desktop embedding, and native trust remain separately gated. Nothing is published or enabled, legacy remains the production default, and merge to `main` remains prohibited.
      +Current phase: Milestone 4 / Work Package 3 manual platform-native signing rehearsal caller — **In progress — 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 rehearsal caller is locally green under E-M4-NATIVE-SIGNING-REHEARSAL-LOCAL-RED-001 and E-M4-NATIVE-SIGNING-REHEARSAL-LOCAL-001. It binds typed authorization to the exact selected source, serializes rehearsals without cancelling an approval in flight, reuses unpublished native artifacts, and invokes the callable signing workflow without connecting release-cut, desktop builds, publication, or any tuple. Real credentialed dispatch is blocked until this new workflow exists on GitHub's default branch; merging to `main` remains outside the implementation owner's authority. Release-cut and every desktop build remain disconnected until exact floors, real signing/trust, aggregate, publication/read-back, and embedding gates are complete. 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** — the rehearsal caller, default-preserving qualification input, and both native-family contract integrations pass locally under `E-M4-NATIVE-SIGNING-REHEARSAL-LOCAL-001`: focused 3 files / 3 tests, broad 47 files / 242 tests, desktop parity 3 files / 48 tests, typecheck, full lint/reliability/localization/max-lines, focused syntax/lint/formatting, and `git diff --check`. The qualification supplements remain default-on for pull requests, direct manual artifact runs, and ordinary reusable calls; only the explicit rehearsal passes `false`, so the known hosted Windows-arm64 floor mismatch remains open rather than being reclassified. GitHub requires a `workflow_dispatch` file to exist on the default branch before dispatch, so real Apple/SignPath requests cannot be triggered from this unmerged PR. The safe default is to keep credentials unused, publish nothing, retain the caller for post-merge rehearsal, and continue independent disconnected work. Release-cut, desktop builds, 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.
      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) @@ -10369,23 +10369,108 @@ mismatch`. NTFS cannot materialize the fixture's declared POSIX executable mode. every tuple disconnected; a rehearsal must not convert the hosted Windows arm64 floor gap into a pass. +### E-M4-NATIVE-SIGNING-REHEARSAL-LOCAL-RED-001 — Manual signing rehearsal caller is absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted purpose-named contract test atop exact pushed head + `5b1a994a3`; draft PR #8741. +- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. Local workflow files + only; no GitHub Actions dispatch, repository secret, Apple timestamp service, SignPath request, + manual approval, 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-native-signing-rehearsal-workflow.test.mjs`. +- Result: expected FAIL, 1 file / 1 test in 152 ms test duration / 1.00 second wall; + 131,629,056-byte maximum RSS, 95,769,952-byte peak memory footprint, and zero swaps. The only + failure is `ENOENT` for + `.github/workflows/ssh-relay-runtime-native-signing-rehearsal.yml`. +- Oracle proved: no manual-only caller currently binds an explicit confirmation and exact selected + source to the existing credential-free native build outputs and callable signing workflow. The + RED also defines that oldest-baseline qualification remains default-on for ordinary artifact runs + but may be separately skipped by this signing rehearsal so the known hosted Windows-arm64 floor + mismatch cannot prevent collection of signed-byte evidence or be misreported as qualified. +- Does not prove: implementation, Actions schema acceptance, secret availability, real Apple or + SignPath signing, returned bytes, native trust, approval/timeout behavior, exact floors, + publication, desktop embedding, SSH behavior, or an enabled tuple. +- Follow-up: implement the exact manual caller and default-preserving qualification input, integrate + the contract into both native test families, and run focused plus broad credential-free local + proof before any dispatch. + +### E-M4-NATIVE-SIGNING-REHEARSAL-LOCAL-001 — Manual rehearsal caller passes locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: local implementation atop exact pushed head `5b1a994a3`; draft PR #8741. +- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. Local workflow files + and tests only; no Actions dispatch, repository secret, Apple timestamp service, SignPath request, + approval, SSH host, publication, desktop consumer, or enabled tuple. +- Intermediate contract RED: the first three-file implementation run passes the new rehearsal and + existing signing-workflow contracts but fails the older build-prerequisite contract because it + still requires an empty `workflow_call` interface. Result: 2 passed / 1 failed in 404 ms test / + 1.16 seconds wall, 131,563,520-byte maximum RSS, 95,638,760-byte peak footprint, and zero swaps. + The correction makes that older contract require the exact Boolean input, safe `true` default, + and both default-preserving qualification conditions. +- Commands and final results: + - Focused caller/signing/build-prerequisite suite: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-native-signing-rehearsal-workflow.test.mjs config/scripts/ssh-relay-runtime-native-signing-workflow.test.mjs config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs` + — PASS, 3 files / 3 tests in 397 ms test / 1.12 seconds wall, 131,956,736-byte maximum RSS, + 96,114,016-byte peak footprint, zero swaps. + - Broad runtime-release suite: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay*.test.mjs` + — PASS, 47 files / 242 tests in 8.98 seconds test / 9.95 seconds wall, + 188,628,992-byte maximum RSS, 95,900,928-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 570 ms test / 1.27 seconds wall, 131,792,896-byte maximum RSS, + 95,950,176-byte peak footprint, zero swaps. + - `/usr/bin/time -l pnpm run typecheck` — PASS in 2.15 seconds wall, + 1,279,410,176-byte maximum RSS, 96,523,760-byte peak footprint, zero swaps. + - `/usr/bin/time -l pnpm run lint` — PASS in 9.90 seconds wall, 2,006,204,416-byte maximum RSS, + 95,606,040-byte peak footprint, zero swaps. All 41 reliability gates, the 355-entry max-lines + ratchet, bundled-skill guides, localization catalog/parity, and localization coverage pass; all + 26 warnings are in untouched existing files. + - Focused `oxfmt --check`, `oxlint`, both `node --check` commands, `git diff --check`, and zero diff + in both user-owned Node/npm resolver files — PASS. +- Oracle proved: only `workflow_dispatch` can enter the caller. The dispatch must supply the exact + lowercase 40-hex commit selected by GitHub plus the exact typed confirmation. Rehearsals serialize + and cannot auto-cancel after approval starts. Repository permissions remain read-only. The caller + invokes the existing credential-free native build, explicitly separates qualification supplements, + then invokes only the existing callable native-signing workflow with inherited secrets. Ordinary + pull-request, direct-dispatch, and reusable artifact calls retain baseline gates by default. + Release-cut and macOS desktop build remain disconnected, and the caller contains no release, + publication, desktop, or tuple path. +- Operational blocker and safe default: GitHub emits `workflow_dispatch` only for workflow files + present on the default branch. This new caller is unmerged by design, so it cannot be credential- + dispatched from the PR. Do not temporarily route repository secrets through the existing routine + artifact workflow. Keep credentials unused and record real signing/native trust only after a + separately authorized merge makes this reviewed caller dispatchable. +- Does not prove: GitHub schema acceptance at an exact pushed head, a live dispatch, secret + availability, Apple/SignPath request or approval, returned signed bytes, native trust, + Gatekeeper/notarization, Defender/WDAC, exact oldest floors, protected manifest signing, + publication/read-back, desktop embedding, SSH behavior, or an enabled tuple. +- Follow-up: commit/push and require both native contract families plus ordinary PR checks to pass at + the exact head. Attempting the caller by file after push may record GitHub's default-branch refusal + but must not use an alternate secret-bearing path. Continue the next independent disconnected + artifact-only package while the real signing gate remains open. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit owner and promotion condition. -| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | -| ------------------------------------------ | ----------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------ | -| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | -| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | -| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | -| Native clean-rebuild identity unproved | All six native cells pass exact clean-build equality | Historical blocker is closed | Codex implementation owner | Met by E-M3-REPRODUCIBILITY-CI-001 | CLOSED | -| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | -| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | -| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | -| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | -| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | -| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | +| Gap | Current behavior | Risk | Owner | Promotion/removal condition | Status | +| ------------------------------------------ | ----------------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------ | +| Bundled runtime only partially implemented | Six unpublished native artifact proofs; no production consumer | #8450/#1693 environment failures remain | Codex implementation owner | Complete Work Packages 2–7 plus Milestones 3–14 | Open | +| No bundled tuple enabled | Every target's default and effective mode remains legacy | No bundled support claim can be made | Codex implementation owner | Complete target-native build/trust and both required live-evidence layers | Open | +| Windows runtime smoke incomplete | Native x64/arm64 smoke settles and uploads exact evidence | Historical blocker is closed | Codex implementation owner | Met by E-M3-WINDOWS-CI-001 | CLOSED | +| Native clean-rebuild identity unproved | All six native cells pass exact clean-build equality | Historical blocker is closed | Codex implementation owner | Met by E-M3-REPRODUCIBILITY-CI-001 | CLOSED | +| Cross-family Layer B remotes unavailable | GitHub native runner labels exist; no approved reachable target pool | Client/remote integration gaps may escape | Repository release administrator + implementation owner | Approve provider/snapshots/credentials/egress/teardown/cost owner | BLOCKED | +| Musl has no accepted official Node binary | Musl is deliberately legacy-only | Unofficial binary would break provenance trust | Codex implementation owner | Orca-owned target-native source build, signing, provenance, and live gates | ACCEPTED GAP | +| Native arm64 live matrices incomplete | Hosted Linux/Windows arm64 labels exist; full SSH/runtime cells do not | Cross-build or unit tests may hide native bugs | Codex implementation owner | Full native archive, trust, SFTP/system-SSH, RPC, and baseline evidence | Open | +| Legacy performance baseline unmeasured | Numeric budgets exist; paired cold/warm measurements do not | Regression thresholds lack a measured baseline | Codex implementation owner | Purpose-built paired harness with ten samples on pinned runner classes | Open | +| New rehearsal caller is not dispatchable | GitHub requires the workflow file on the default branch | Real Apple/SignPath/native-trust proof is gated | Repository release administrator + implementation owner | Reviewed PR is merged separately, then exact-source rehearsal passes | BLOCKED | +| Manifest signing environment unprovisioned | Ed25519/key-rotation policy exists; no protected runtime signing secret | Runtime assets cannot be safely published | Repository release administrator | Protected environment, reviewers, two test keys, rehearsals, and access audit | BLOCKED | +| Bootstrap primitives lack full live proof | POSIX/Windows contracts exist; bounded SSH implementations do not | Hidden dependency or transfer corruption | Codex implementation owner | Purpose-named full-size SFTP/POSIX/Windows system-SSH live suites | Open | ## Final Definition of Done @@ -10426,14 +10511,15 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the exact-head CI evidence recorded under -E-M4-NATIVE-SIGNING-WORKFLOW-CI-001. Then add a purpose-named RED and a manual-only rehearsal caller -that produces the existing unpublished native artifacts and invokes the disconnected signing -workflow with repository credentials. Do not connect release-cut or any desktop build. The -rehearsal must retain the Windows arm64 build-26100, macOS 13.5, and Linux kernel 4.18 qualification -gates as separately open, and must not publish or enable a tuple. Record real Apple/SignPath request, -approval/return, exact changed-byte identity, native trust, runner, duration, and residual evidence. -Do not merge to `main`; retain every production/default gate. +Commit and push the locally proven manual rehearsal caller under +E-M4-NATIVE-SIGNING-REHEARSAL-LOCAL-001, then require its purpose-named contracts to pass on both +native test families at the exact head. Record GitHub's expected default-branch dispatch refusal; +do not route secrets through an existing routine workflow as a workaround. Real Apple/SignPath +request, approval/return, changed-byte identity, and native-trust evidence remain gated until a +separately authorized merge makes the reviewed caller dispatchable. While blocked, begin the next +purpose-named disconnected aggregate/immutable-manifest job package. Keep Windows arm64 build 26100, +macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately +gated. Do not merge to `main`; retain every production/default gate. 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 From 0be32c5cc9b1c46f6bb2a7653f0ebc7b2d839209 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 09:19:04 -0400 Subject: [PATCH 127/267] docs: record signing rehearsal CI gate Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 8 +- ...github-release-implementation-checklist.md | 86 ++++++++++++++++++- 2 files changed, 91 insertions(+), 3 deletions(-) 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 c1351f2ac61..b7e342239ad 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 @@ -226,7 +226,13 @@ complete a box. and PR Checks are green at the exact head. The manual credentialed rehearsal starts with the missing-caller RED `E-M4-NATIVE-SIGNING-REHEARSAL-LOCAL-RED-001` and is locally green under `E-M4-NATIVE-SIGNING-REHEARSAL-LOCAL-001`. GitHub cannot dispatch a new workflow before it - exists on the default branch, so real signing/native trust remains blocked without merging. + exists on the default branch; the exact confirmation-bound refusal is recorded under + `E-M4-NATIVE-SIGNING-REHEARSAL-DISPATCH-BLOCKED-001`, so real signing/native trust remains + blocked without merging. Exact-head run + [29417449971](https://github.com/stablyai/orca/actions/runs/29417449971) passes the caller and + default-preservation contracts plus full builds on all six native jobs under + `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. - [ ] 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. 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 3f89bcd582c..e643b171d43 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 — **In progress — 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 rehearsal caller is locally green under E-M4-NATIVE-SIGNING-REHEARSAL-LOCAL-RED-001 and E-M4-NATIVE-SIGNING-REHEARSAL-LOCAL-001. It binds typed authorization to the exact selected source, serializes rehearsals without cancelling an approval in flight, reuses unpublished native artifacts, and invokes the callable signing workflow without connecting release-cut, desktop builds, publication, or any tuple. Real credentialed dispatch is blocked until this new workflow exists on GitHub's default branch; merging to `main` remains outside the implementation owner's authority. Release-cut and every desktop build remain disconnected until exact floors, real signing/trust, aggregate, publication/read-back, and embedding gates are complete. 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** — the rehearsal caller, default-preserving qualification input, and both native-family contract integrations pass locally under `E-M4-NATIVE-SIGNING-REHEARSAL-LOCAL-001`: focused 3 files / 3 tests, broad 47 files / 242 tests, desktop parity 3 files / 48 tests, typecheck, full lint/reliability/localization/max-lines, focused syntax/lint/formatting, and `git diff --check`. The qualification supplements remain default-on for pull requests, direct manual artifact runs, and ordinary reusable calls; only the explicit rehearsal passes `false`, so the known hosted Windows-arm64 floor mismatch remains open rather than being reclassified. GitHub requires a `workflow_dispatch` file to exist on the default branch before dispatch, so real Apple/SignPath requests cannot be triggered from this unmerged PR. The safe default is to keep credentials unused, publish nothing, retain the caller for post-merge rehearsal, and continue independent disconnected work. Release-cut, desktop builds, 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 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.
      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) @@ -10453,6 +10453,88 @@ mismatch`. NTFS cannot materialize the fixture's declared POSIX executable mode. but must not use an alternate secret-bearing path. Continue the next independent disconnected artifact-only package while the real signing gate remains open. +### E-M4-NATIVE-SIGNING-REHEARSAL-DISPATCH-BLOCKED-001 — Default branch blocks dispatch + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `c2db2b62e544139176d0993b1340f9d0c706e017`; draft PR #8741. +- Command: + `gh workflow run ssh-relay-runtime-native-signing-rehearsal.yml --repo stablyai/orca --ref Jinwoo-H/bug-8450-ssh-relay-runtime-builds -f expected-source-sha=c2db2b62e544139176d0993b1340f9d0c706e017 -f 'confirmation=SIGN SSH RELAY RUNTIME ARTIFACTS'`. +- Result: expected refusal before run creation: `HTTP 404: workflow +ssh-relay-runtime-native-signing-rehearsal.yml not found on the default branch`. No Actions run, + job, artifact, secret read, Apple/SignPath request, approver notification, publication, or enabled + tuple was created. +- Oracle proved: the reviewed caller cannot be live-rehearsed while it exists only in this PR. The + user's no-merge boundary and GitHub's default-branch dispatch rule jointly gate real signing. +- Safe default: do not add a temporary secret-bearing path to an existing routine workflow. Keep the + caller reviewable in the PR, leave real signing/native trust open, and continue only independent + disconnected work until a separately authorized merge makes the workflow dispatchable. +- Does not prove: Actions schema execution, credential availability, signing, approval/timeout, + returned-byte identity, native trust, exact floors, publication, desktop embedding, SSH behavior, + or an enabled tuple. + +### E-M4-NATIVE-SIGNING-REHEARSAL-CI-001 — Rehearsal contracts pass all six native builds + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Commit SHA / PR: exact head `c2db2b62e544139176d0993b1340f9d0c706e017`; capability commit + `c2db2b62e`; draft PR #8741. +- Workflow/run: SSH Relay Runtime Artifacts + [29417449971](https://github.com/stablyai/orca/actions/runs/29417449971), + 13:00:47Z–13:15:32Z. Credential-free PR execution only; it does not invoke the manual rehearsal + caller or any signing credential. +- Target-native jobs and resolved images: + - Windows arm64 + [87359106832](https://github.com/stablyai/orca/actions/runs/29417449971/job/87359106832): + `windows-11-arm`, `win11-arm64` image `20260714.109.1`, ARM64, + 13:01:01Z–13:11:17Z. + - Windows x64 + [87359106887](https://github.com/stablyai/orca/actions/runs/29417449971/job/87359106887): + `windows-2022`, `win22` image `20260714.244.1`, X64, 13:01:01Z–13:07:22Z. + - Linux x64 + [87359106853](https://github.com/stablyai/orca/actions/runs/29417449971/job/87359106853): + `ubuntu-24.04`, `ubuntu24` image `20260705.232.1`, X64, 13:01:01Z–13:05:10Z. + - Linux arm64 + [87359106916](https://github.com/stablyai/orca/actions/runs/29417449971/job/87359106916): + `ubuntu-24.04-arm`, `ubuntu24-arm64` image `20260714.61.1`, ARM64, + 13:01:04Z–13:06:06Z. + - macOS x64 + [87359106864](https://github.com/stablyai/orca/actions/runs/29417449971/job/87359106864): + `macos-15-intel`, `macos15` image `20260629.0276.1`, X64, + 13:01:02Z–13:07:29Z. + - macOS arm64 + [87359106894](https://github.com/stablyai/orca/actions/runs/29417449971/job/87359106894): + `macos-15`, `macos15` image `20260706.0213.1`, ARM64, 13:01:02Z–13:05:38Z. +- Contract results: each POSIX job passes 46 files / 238 tests, including the new caller contract; + durations are 6.42 seconds Linux x64, 6.18 seconds Linux arm64, 28.42 seconds macOS x64, and 11.49 + seconds macOS arm64. Each Windows job passes 47 files / 233 tests with nine declared skips out of + 242; durations are 15.23 seconds x64 and 14.23 seconds arm64. Every job then completes two exact + native builds, equality, inspection, Node/PTY/watcher smoke, and unpublished upload. +- Default-preservation oracle: both Linux supplements pass in jobs + [87360607215](https://github.com/stablyai/orca/actions/runs/29417449971/job/87360607215) + and [87360607155](https://github.com/stablyai/orca/actions/runs/29417449971/job/87360607155), + and Windows x64 baseline job + [87361449973](https://github.com/stablyai/orca/actions/runs/29417449971/job/87361449973) + passes. The ordinary PR call therefore retains qualification jobs by default. +- Expected retained failure: Windows arm64 baseline job + [87361450070](https://github.com/stablyai/orca/actions/runs/29417449971/job/87361450070) + authenticates the unchanged 85,213,511-byte tree/content ID, passes Node/PTY/watcher/resource + smoke in 6,177.5851 ms with 48,345,088-byte RSS and 7,942.833 ms total verification, then fails + only because `10.0.26200` is not required build 26100. Platform and architecture pass; exact OS + build fails closed. +- Adjacent exact-head regressions: PR Checks + [29417449960](https://github.com/stablyai/orca/actions/runs/29417449960) pass from + 13:00:51Z–13:13:15Z. Golden E2E + [29417449936](https://github.com/stablyai/orca/actions/runs/29417449936) passes macOS and Linux. +- Does not prove: a dispatch of the new caller, any secret access, Apple/SignPath signing or approval, + returned signed bytes, native trust, exact oldest floors, protected manifest signing, + publication/read-back, desktop embedding, SSH behavior, or an enabled tuple. +- Checklist items satisfied: exact-head schema/contract and default-preservation CI prerequisite for + the manual rehearsal capability. The broader native-signing item remains open because real + signing/native trust is blocked. +- 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. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 91f59d19e5251fb7b6c9f6d397cd9cd82b43cbdf Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 09:34:18 -0400 Subject: [PATCH 128/267] ci: finalize Linux relay artifacts for aggregation Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 19 ++ .../ssh-relay-runtime-linux-finalization.mjs | 226 ++++++++++++++++ ...-relay-runtime-linux-finalization.test.mjs | 251 ++++++++++++++++++ ...elease-implementation-checklist-summary.md | 5 + ...github-release-implementation-checklist.md | 76 +++++- 5 files changed, 575 insertions(+), 2 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-linux-finalization.mjs create mode 100644 config/scripts/ssh-relay-runtime-linux-finalization.test.mjs 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 From e5dfffaaa2df55b221c5ee30699ff8fe814b91aa Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 10:03:18 -0400 Subject: [PATCH 129/267] docs: record Linux relay finalization CI proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 11 +- ...github-release-implementation-checklist.md | 127 +++++++++++++++--- 2 files changed, 118 insertions(+), 20 deletions(-) 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 8ad68dab3f9..68edae4cb89 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,11 +234,12 @@ 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. + The missing Linux aggregate-ready prerequisite is closed by + `E-M4-LINUX-FINALIZATION-LOCAL-RED-001`, `E-M4-LINUX-FINALIZATION-LOCAL-001`, and + `E-M4-LINUX-FINALIZATION-CI-001`: both native Linux architectures emitted inspected, + hash-bound descriptors/receipts. The active slice is the callable, disconnected aggregate and + protected Ed25519-signing workflow contract. The environment/seed are not provisioned, so live + signing remains blocked and no release, desktop, publication, or tuple consumer is connected. - [ ] 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 1ad2c5ecbcc..351efdd6526 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 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.
      +Current phase: Milestone 4 / Work Package 3 disconnected aggregate and protected manifest-signing workflow contract — **In progress — 2026-07-15, Codex implementation owner**. Linux aggregate-ready finalization is closed locally and on exact-head native x64/arm64 runners under E-M4-LINUX-FINALIZATION-LOCAL-RED-001, E-M4-LINUX-FINALIZATION-LOCAL-001, and E-M4-LINUX-FINALIZATION-CI-001. The active package must consume only the six exact aggregate-ready outputs, prepare canonical bytes credential-free, expose only those bytes to the tag-restricted `relay-runtime-manifest-signing` environment, accept only key ID plus Ed25519 signature, reconstruct and verify before final output, and fail closed on missing/extra/mutated inputs, approval/secret/signing failure, timeout, or partial output. It remains callable and disconnected. Release-cut, desktop builds, publication, and every tuple stay disconnected; production/default behavior is unchanged.
      +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact-head run 29419759256 at `91f59d19e5251fb7b6c9f6d397cd9cd82b43cbdf` passes all six native builds, both Linux userland supplements, and Windows x64 baseline. Windows arm64 fails only the retained build-floor gate after successful runtime verification (hosted 26200 versus required 26100). Downloaded artifacts 8344803641 and 8344824674 contain the expected Linux x64/arm64 descriptors and finalization receipts, whose tuple/content/archive/metadata bindings and smoke/resource metrics were inspected under E-M4-LINUX-FINALIZATION-CI-001. Golden E2E passes. PR Checks attempt 1 exposed one unrelated terminal-routing assertion after 29,944 tests passed; the focused test passes locally and exact-head attempt 2 passes all 29,945 tests, unpacked build, and packaged CLI smoke under E-M4-LINUX-FINALIZATION-PR-CHECKS-RED-001 and E-M4-LINUX-FINALIZATION-PR-CHECKS-001. No signing credential, aggregate workflow, 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) @@ -100,10 +100,12 @@ same change as the work it records. native CI pass under E-M4-BUILD-PREREQUISITE-LOCAL-RED-001, E-M4-BUILD-PREREQUISITE-LOCAL-001, and E-M4-BUILD-PREREQUISITE-CI-001. Release workflows remain disconnected. -- Active package: Work Package 3 platform-native signing-job workflow contract. Begin with a - purpose-named RED for exact native build-input consumption, immutable returned signed bytes, - post-return hashing, bounded failure behavior, and release-consumer disconnection. Credential-free - tests do not count as real Apple/SignPath signing or native-trust evidence; keep those gates open. +- Active package: Work Package 3 callable, disconnected aggregate/manifest-signing workflow + contract. Begin with a purpose-named RED for all-six exact aggregate input consumption, + credential-free canonical preparation, protected environment isolation, seed decoding and + signature-only return, reconstruction/verification, bounded failure cleanup, and complete + release/desktop/publication disconnection. The environment and seed are unprovisioned; static and + test-key proof do not count as live protected signing evidence. - 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 @@ -10607,6 +10609,101 @@ ssh-relay-runtime-native-signing-rehearsal.yml not found on the default branch`. - 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. +### E-M4-LINUX-FINALIZATION-CI-001 — Native Linux finalization emits inspected aggregate inputs + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `91f59d19e5251fb7b6c9f6d397cd9cd82b43cbdf`; draft PR #8741. +- Workflow: [SSH Relay Runtime Artifacts run 29419759256](https://github.com/stablyai/orca/actions/runs/29419759256), + 13:34:30Z–13:50:21Z. The workflow conclusion is the expected failure solely because the separately + declared Windows arm64 oldest-build gate rejects hosted 26200 versus required 26100 after runtime + verification. It is not reported as an all-green aggregate. +- Native build jobs: all six succeed — Linux arm64 87366900226 on `ubuntu-24.04-arm` in 4m50s; + Windows x64 87366900246 in 6m14s; macOS x64 87366900262 in 6m55s; macOS arm64 87366900276 in + 4m59s; Windows arm64 87366900296 in 11m59s; and Linux x64 87366900347 on `ubuntu-24.04` in + 4m10s. Each exact native family runs the purpose suite including Linux-finalization syntax and + tests; both Linux jobs report 47 files / 242 tests passed under Node 24.18.0. +- Supplemental gates: Linux x64 job 87368626081 and Linux arm64 job 87368626106 pass the + digest-pinned Rocky Linux glibc 2.28/libstdc++ 6.0.25 userland proof; the explicit kernel 4.18 gap + remains because hosted kernels are newer. Windows x64 baseline job 87369854311 passes. Windows + arm64 job 87369854133 authenticates and executes the runtime, records 6,242.0644 ms smoke, + 48,771,072-byte RSS, and 10,149.9713 ms total verification, then fails only the exact OS-build + check (`10.0.26200` versus required 26100). +- Downloaded proof: `gh run download 29419759256 --repo stablyai/orca -n +ssh-relay-runtime-linux-x64-glibc -n ssh-relay-runtime-linux-arm64-glibc`. Artifact + [8344803641](https://github.com/stablyai/orca/actions/runs/29419759256/artifacts/8344803641) + is 29,292,240 bytes and artifact + [8344824674](https://github.com/stablyai/orca/actions/runs/29419759256/artifacts/8344824674) + is 28,222,689 bytes. Each contains exactly the runtime archive, identity, SBOM, provenance, + signing-stage evidence, `.manifest-tuple.json`, and `.linux-finalization.json` receipt expected by + the unpublished evidence contract. +- x64 receipt: tuple `linux-x64-glibc`, content + `sha256:fc63ca342a5990f460ec6d72262a8542173dab20ce03c9b9cfb755b1c6057e6d`, archive + `sha256:8b9e77fe5f33acbee4a2d532f0be564d8eceb36acaf22deec08cccdd14834d78`, descriptor + `sha256:855d83a9f0b0bb5878de335703c4398dc72420b220b31efb5786af59da0587ec`, 49 tree + entries / 34 files / 124,846,430 expanded bytes, 159.336194 ms smoke, 56,758,272-byte RSS, and + 1,749.137209 ms final verification. +- arm64 receipt: tuple `linux-arm64-glibc`, content + `sha256:96f07f62af9b35304bb8ca0870ca4d8095e059bfa61dd1bc57e81b20f3fbca67`, archive + `sha256:f2bfe798097a24a113abd37d32008d542c1ed0772ed8ccce53fb51c807d84c3f`, descriptor + `sha256:253dac72cb97e32c6fa68f886ac7c73cf00b7a03b1e87d189c490559bf1c071e`, 49 tree + entries / 34 files / 122,865,324 expanded bytes, 188.430108 ms smoke, 52,727,808-byte RSS, and + 1,700.548606 ms final verification. +- Adjacent regressions: [Golden E2E 29419759379](https://github.com/stablyai/orca/actions/runs/29419759379) + passes its macOS and Linux jobs at the exact head. PR Checks attempt 1 has the separately recorded + unrelated suite-order RED `E-M4-LINUX-FINALIZATION-PR-CHECKS-RED-001`; attempt 2 passes under + `E-M4-LINUX-FINALIZATION-PR-CHECKS-001`. +- Oracle proved: both real native Linux architectures execute the new verify-before-descriptor path, + preserve exact archive/tree/native hashes, run bundled Node/PTY/watcher smoke, emit the complete + hash-only native report, and upload receipts that bind exact aggregate inputs. The inspected + receipts agree with their descriptors, assets, tuple IDs, and content IDs. +- Does not prove: Linux kernel 4.18, native Apple/SignPath signing or trust, protected manifest + signing, aggregate workflow execution, publication/read-back, desktop embedding, SSH behavior, + performance parity, or any enabled tuple. +- Checklist items satisfied: Linux aggregate-ready prerequisite for the Work Package 3 aggregate + workflow. The broader aggregate/manifest-signing item remains open. +- Follow-up: implement the purpose-named callable/disconnected aggregate and protected signing + workflow contract. Do not connect release, desktop, publication, or tuple consumers. + +### E-M4-LINUX-FINALIZATION-PR-CHECKS-RED-001 — Adjacent full suite exposes unrelated routing flake + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `91f59d19e5251fb7b6c9f6d397cd9cd82b43cbdf`; draft PR #8741. +- Workflow: [PR Checks run 29419759287 attempt 1](https://github.com/stablyai/orca/actions/runs/29419759287), + job 87366900240. Lint, typecheck, Git compatibility, and every pre-test gate pass. The test step + reports 2,850 files / 29,944 tests passed, nine files / 55 tests skipped, and one failure after + 421.95 seconds. +- Failure: untouched + `src/renderer/src/components/terminal-pane/pty-connection.test.ts` case `keeps Droid routing visible +through command-finished foreground confirmation` receives `csi-u` instead of `alt-enter` at line 5284. Neither the Linux finalizer nor its workflow edits touch that production/test domain. +- Focused command: + `pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/terminal-pane/pty-connection.test.ts -t 'keeps Droid routing visible through command-finished foreground confirmation'` + — PASS locally, 1 file / 1 test with 423 skipped in 1.22 seconds. +- Oracle proved: the exact failure is identified and is not hidden by a blind retry; it is not a + Linux finalizer assertion. Exact-head attempt 2 was explicitly requested only after log inspection. +- Does not prove: that the unrelated test has no latent ordering defect. +- Follow-up: replacement attempt 2 passes under E-M4-LINUX-FINALIZATION-PR-CHECKS-001. Retain this + RED rather than erasing the observed suite-order failure. + +### E-M4-LINUX-FINALIZATION-PR-CHECKS-001 — Exact-head full-suite replacement passes + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `91f59d19e5251fb7b6c9f6d397cd9cd82b43cbdf`; draft PR #8741. +- Workflow: [PR Checks run 29419759287 attempt 2](https://github.com/stablyai/orca/actions/runs/29419759287), + job 87369995158, 13:47:10Z–14:01:35Z — PASS. +- Result: lint, reliability, max-lines, localization, typecheck, Git 2.25 compatibility, and all + other pre-test gates pass. The full suite passes 2,851 files / 29,945 tests with nine files / 55 + tests skipped in 537.22 seconds. The unpacked Linux app builds, packaged daemon entry loads under + plain Node, and the packaged CLI `--help` smoke succeeds outside the repository. +- Oracle proved: the exact-head replacement is green, including the assertion that failed in + attempt 1, without any code or test change and without weakening the Linux finalizer contract. +- Does not prove: real Linux kernel 4.18, signing, publication, desktop embedding, SSH behavior, or + an enabled tuple. +- Follow-up: checkpoint the Linux finalization evidence and start the disconnected aggregate and + protected manifest-signing workflow RED. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -10665,15 +10762,15 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the locally proven manual rehearsal caller under -E-M4-NATIVE-SIGNING-REHEARSAL-LOCAL-001, then require its purpose-named contracts to pass on both -native test families at the exact head. Record GitHub's expected default-branch dispatch refusal; -do not route secrets through an existing routine workflow as a workaround. Real Apple/SignPath -request, approval/return, changed-byte identity, and native-trust evidence remain gated until a -separately authorized merge makes the reviewed caller dispatchable. While blocked, begin the next -purpose-named disconnected aggregate/immutable-manifest job package. Keep Windows arm64 build 26100, -macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately -gated. Do not merge to `main`; retain every production/default gate. +Checkpoint E-M4-LINUX-FINALIZATION-CI-001 and the green PR Checks replacement. Then begin a +purpose-named RED for the callable, disconnected aggregate/immutable-manifest workflow: consume all +six exact aggregate-ready artifacts, prepare canonical bytes without credentials, isolate the +base64 32-byte seed in the tag-restricted `relay-runtime-manifest-signing` environment, return only +key ID plus signature, reconstruct and verify before emitting final output, and remove partial +output on every failure. The environment and secret remain unprovisioned, so live signing evidence +must stay BLOCKED. Keep Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, +desktop builds, publication, and every tuple separately gated. Do not merge to `main`; retain every +production/default gate. 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 From 4180e04ac4e732f082dc576af69364325a678d2b Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 10:13:01 -0400 Subject: [PATCH 130/267] ci: add protected relay manifest seed signer Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 8 + ...sh-relay-runtime-manifest-seed-signing.mjs | 236 ++++++++++++++++++ ...lay-runtime-manifest-seed-signing.test.mjs | 152 +++++++++++ ...elease-implementation-checklist-summary.md | 4 + ...github-release-implementation-checklist.md | 91 ++++++- 5 files changed, 482 insertions(+), 9 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-manifest-seed-signing.mjs create mode 100644 config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 05f9979d6c8..f58cadb26c0 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -166,6 +166,8 @@ jobs: node --check config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs node --check config/scripts/ssh-relay-runtime-manifest-signing-handoff.mjs node --check config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs + node --check config/scripts/ssh-relay-runtime-manifest-seed-signing.mjs + node --check config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs node --check config/scripts/ssh-relay-runtime-manifest-aggregate.mjs node --check config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs node --check config/scripts/ssh-relay-runtime-manifest-tuple.mjs @@ -212,6 +214,7 @@ jobs: config/scripts/ssh-relay-runtime-draft-readback.test.mjs \ config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs \ config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs \ + config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs \ config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs \ config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs \ config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs \ @@ -578,6 +581,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime manifest signing handoff syntax check failed' } node --check config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime manifest signing handoff test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-manifest-seed-signing.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime manifest seed signing syntax check failed' } + node --check config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime manifest seed signing test syntax check failed' } node --check config/scripts/ssh-relay-runtime-manifest-aggregate.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime manifest aggregate syntax check failed' } node --check config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs @@ -642,6 +649,7 @@ jobs: config/scripts/ssh-relay-runtime-draft-readback.test.mjs ` config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs ` config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs ` + config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs ` config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs ` config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs ` config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs ` diff --git a/config/scripts/ssh-relay-runtime-manifest-seed-signing.mjs b/config/scripts/ssh-relay-runtime-manifest-seed-signing.mjs new file mode 100644 index 00000000000..90ee70660e6 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-manifest-seed-signing.mjs @@ -0,0 +1,236 @@ +import { lstat, mkdir, readFile, realpath, rm, writeFile } from 'node:fs/promises' +import { basename, dirname, resolve } from 'node:path' +import { pathToFileURL } from 'node:url' + +import nacl from 'tweetnacl' + +import { + createSshRelayRuntimeManifestSigningRequest, + sshRelayRuntimeManifestKeyId +} from './ssh-relay-runtime-manifest-signing-handoff.mjs' + +const REQUEST_FIELDS = [ + 'algorithm', + 'payloadBase64', + 'payloadSha256', + 'payloadSize', + 'schemaVersion' +] +const MAX_REQUEST_ARTIFACT_BYTES = 48 * 1024 * 1024 +const BASE64_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/u + +function assertObject(value, label) { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`SSH relay runtime manifest seed signing ${label} must be an object`) + } +} + +function assertExactFields(value, fields, label) { + assertObject(value, label) + const actual = Object.keys(value).sort() + const expected = [...fields].sort() + if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) { + throw new Error( + `SSH relay runtime manifest seed signing ${label} has unexpected or missing fields` + ) + } +} + +function decodeCanonicalBase64(value, label) { + if (typeof value !== 'string' || !BASE64_PATTERN.test(value)) { + throw new Error(`SSH relay runtime manifest seed signing ${label} is not canonical base64`) + } + const bytes = Buffer.from(value, 'base64') + if (bytes.length === 0 || bytes.toString('base64') !== value) { + throw new Error(`SSH relay runtime manifest seed signing ${label} is not canonical base64`) + } + return bytes +} + +function validateInMemoryRequest(request) { + assertExactFields( + request, + ['algorithm', 'canonicalBytes', 'payloadSha256', 'payloadSize'], + 'request' + ) + if (!Buffer.isBuffer(request.canonicalBytes) && !(request.canonicalBytes instanceof Uint8Array)) { + throw new Error('SSH relay runtime manifest seed signing request payload must be bytes') + } + const expected = createSshRelayRuntimeManifestSigningRequest(request.canonicalBytes) + if ( + request.algorithm !== expected.algorithm || + request.payloadSha256 !== expected.payloadSha256 || + request.payloadSize !== expected.payloadSize + ) { + throw new Error('SSH relay runtime manifest seed signing request binding is inconsistent') + } + return expected +} + +export function encodeSshRelayRuntimeManifestSigningRequestArtifact(request) { + const parsed = validateInMemoryRequest(request) + return { + schemaVersion: 1, + algorithm: parsed.algorithm, + payloadBase64: parsed.canonicalBytes.toString('base64'), + payloadSha256: parsed.payloadSha256, + payloadSize: parsed.payloadSize + } +} + +export function decodeSshRelayRuntimeManifestSigningRequestArtifact(artifact) { + assertExactFields(artifact, REQUEST_FIELDS, 'request artifact') + if (artifact.schemaVersion !== 1 || artifact.algorithm !== 'ed25519-v1') { + throw new Error( + 'SSH relay runtime manifest seed signing request artifact schema is unsupported' + ) + } + const canonicalBytes = decodeCanonicalBase64(artifact.payloadBase64, 'request payload') + const request = createSshRelayRuntimeManifestSigningRequest(canonicalBytes) + if ( + artifact.payloadSize !== request.payloadSize || + artifact.payloadSha256 !== request.payloadSha256 + ) { + throw new Error( + 'SSH relay runtime manifest seed signing request artifact binding is inconsistent' + ) + } + return request +} + +function decodeSeed(seedBase64) { + const seed = decodeCanonicalBase64(seedBase64, 'seed') + if (seed.length !== nacl.sign.seedLength) { + throw new Error('SSH relay runtime manifest signing seed must contain exactly 32 bytes') + } + return seed +} + +export function signSshRelayRuntimeManifestRequest({ requestArtifact, seedBase64 }) { + const request = decodeSshRelayRuntimeManifestSigningRequestArtifact(requestArtifact) + const pair = nacl.sign.keyPair.fromSeed(decodeSeed(seedBase64)) + return { + keyId: sshRelayRuntimeManifestKeyId(pair.publicKey), + signature: Buffer.from(nacl.sign.detached(request.canonicalBytes, pair.secretKey)).toString( + 'base64' + ) + } +} + +export async function readSshRelayRuntimeManifestSigningRequestArtifact(path) { + const metadata = await lstat(path) + if ( + !metadata.isFile() || + metadata.isSymbolicLink() || + metadata.size <= 0 || + metadata.size > MAX_REQUEST_ARTIFACT_BYTES + ) { + throw new Error('SSH relay runtime manifest signing request must be a bounded regular file') + } + let artifact + try { + artifact = JSON.parse(await readFile(path, 'utf8')) + } catch (error) { + throw new Error(`SSH relay runtime manifest signing request is invalid JSON: ${error.message}`) + } + decodeSshRelayRuntimeManifestSigningRequestArtifact(artifact) + return artifact +} + +async function exclusiveOutputDirectory(outputDirectory) { + const absolute = resolve(outputDirectory) + const parent = resolve(await realpath(dirname(absolute))) + const parentMetadata = await lstat(parent) + if (!parentMetadata.isDirectory() || parentMetadata.isSymbolicLink()) { + throw new Error('SSH relay runtime manifest signing output parent must be a real directory') + } + const output = resolve(parent, basename(absolute)) + try { + await lstat(output) + } catch (error) { + if (error.code === 'ENOENT') { + return output + } + throw error + } + throw new Error( + 'SSH relay runtime manifest signing requires an exclusive absent output directory' + ) +} + +export async function writeSshRelayRuntimeManifestSigningResult({ + requestArtifact, + seedBase64, + outputDirectory +}) { + const result = signSshRelayRuntimeManifestRequest({ requestArtifact, seedBase64 }) + const output = await exclusiveOutputDirectory(outputDirectory) + let created = false + try { + await mkdir(output, { mode: 0o700 }) + created = true + // Why: the protected job may return only the authenticated key identity and detached signature. + await writeFile(joinResultPath(output), `${JSON.stringify(result)}\n`, { + flag: 'wx', + mode: 0o600 + }) + return result + } catch (error) { + if (created) { + await rm(output, { recursive: true, force: true }) + } + throw error + } +} + +function joinResultPath(output) { + return resolve(output, 'signing-result.json') +} + +export function parseSshRelayRuntimeManifestSeedSigningArguments(argv) { + const options = {} + const fields = new Map([ + ['--request', 'requestPath'], + ['--output-directory', 'outputDirectory'] + ]) + for (let index = 0; index < argv.length; index += 2) { + const field = fields.get(argv[index]) + const value = argv[index + 1] + if (!field || !value || value.startsWith('--') || options[field]) { + throw new Error(`Invalid SSH relay runtime manifest seed signing argument: ${argv[index]}`) + } + options[field] = resolve(value) + } + for (const field of fields.values()) { + if (!options[field]) { + throw new Error(`Missing SSH relay runtime manifest seed signing argument: ${field}`) + } + } + return options +} + +async function main() { + const options = parseSshRelayRuntimeManifestSeedSigningArguments(process.argv.slice(2)) + const requestArtifact = await readSshRelayRuntimeManifestSigningRequestArtifact( + options.requestPath + ) + const result = await writeSshRelayRuntimeManifestSigningResult({ + requestArtifact, + seedBase64: process.env.SSH_RELAY_RUNTIME_MANIFEST_SEED, + outputDirectory: options.outputDirectory + }) + process.stdout.write(`${JSON.stringify({ keyId: result.keyId })}\n`) +} + +if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) { + main().catch((error) => { + process.stderr.write( + `SSH relay runtime manifest seed signing failed: ${error.stack ?? error}\n` + ) + process.exitCode = 1 + }) +} + +export const SSH_RELAY_RUNTIME_MANIFEST_SEED_SIGNING_LIMITS = Object.freeze({ + maximumRequestArtifactBytes: MAX_REQUEST_ARTIFACT_BYTES +}) diff --git a/config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs b/config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs new file mode 100644 index 00000000000..b683554de18 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs @@ -0,0 +1,152 @@ +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import nacl from 'tweetnacl' +import { afterEach, describe, expect, it } from 'vitest' + +import { createSshRelayArtifactTestManifest } from '../../src/main/ssh/ssh-relay-artifact-test-manifest.ts' +import { assembleCanonicalSshRelayRuntimeManifest } from './ssh-relay-runtime-manifest-assembly.mjs' +import { + createSshRelayRuntimeManifestSigningRequest, + finalizeSshRelayRuntimeManifestSigningHandoff, + sshRelayRuntimeManifestKeyId +} from './ssh-relay-runtime-manifest-signing-handoff.mjs' +import { + decodeSshRelayRuntimeManifestSigningRequestArtifact, + encodeSshRelayRuntimeManifestSigningRequestArtifact, + signSshRelayRuntimeManifestRequest, + writeSshRelayRuntimeManifestSigningResult +} from './ssh-relay-runtime-manifest-seed-signing.mjs' + +const temporaryDirectories = [] +const seed = Uint8Array.from({ length: 32 }, (_, index) => index) +const seedBase64 = Buffer.from(seed).toString('base64') +const keyPair = nacl.sign.keyPair.fromSeed(seed) + +function request() { + const { signatures: _signatures, ...unsigned } = createSshRelayArtifactTestManifest() + const canonical = assembleCanonicalSshRelayRuntimeManifest(unsigned).canonicalBytes + return createSshRelayRuntimeManifestSigningRequest(canonical) +} + +afterEach(async () => { + await Promise.all( + temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true })) + ) +}) + +describe('SSH relay runtime protected manifest seed signing', () => { + it('round-trips a bounded canonical request and returns only key ID plus signature', () => { + const source = request() + const artifact = encodeSshRelayRuntimeManifestSigningRequestArtifact(source) + const decoded = decodeSshRelayRuntimeManifestSigningRequestArtifact(artifact) + const result = signSshRelayRuntimeManifestRequest({ requestArtifact: artifact, seedBase64 }) + + expect(decoded).toEqual(source) + expect(Object.keys(artifact).sort()).toEqual([ + 'algorithm', + 'payloadBase64', + 'payloadSha256', + 'payloadSize', + 'schemaVersion' + ]) + expect(result).toEqual({ + keyId: sshRelayRuntimeManifestKeyId(keyPair.publicKey), + signature: Buffer.from(nacl.sign.detached(source.canonicalBytes, keyPair.secretKey)).toString( + 'base64' + ) + }) + expect( + finalizeSshRelayRuntimeManifestSigningHandoff({ + request: decoded, + signingResults: [result], + acceptedKeys: [{ keyId: result.keyId, publicKey: keyPair.publicKey }] + }).manifest.signatures + ).toEqual([{ algorithm: 'ed25519-v1', ...result }]) + }) + + it.each([ + ['', /seed/i], + ['not-base64', /seed|base64/i], + [`${seedBase64}\n`, /seed|base64/i], + [Buffer.alloc(31).toString('base64'), /32 bytes|seed/i], + [Buffer.alloc(33).toString('base64'), /32 bytes|seed/i] + ])('rejects a malformed or non-canonical seed without a result', (candidate, message) => { + expect(() => + signSshRelayRuntimeManifestRequest({ + requestArtifact: encodeSshRelayRuntimeManifestSigningRequestArtifact(request()), + seedBase64: candidate + }) + ).toThrow(message) + }) + + it('rejects request field, size, hash, canonical-byte, and base64 drift', () => { + const valid = encodeSshRelayRuntimeManifestSigningRequestArtifact(request()) + const cases = [ + { ...valid, extra: true }, + { ...valid, algorithm: 'rsa-v1' }, + { ...valid, payloadSize: valid.payloadSize + 1 }, + { ...valid, payloadSha256: `sha256:${'f'.repeat(64)}` }, + { ...valid, payloadBase64: `${valid.payloadBase64}\n` }, + { ...valid, payloadBase64: Buffer.from('{}').toString('base64'), payloadSize: 2 } + ] + for (const artifact of cases) { + expect(() => + signSshRelayRuntimeManifestRequest({ requestArtifact: artifact, seedBase64 }) + ).toThrow() + } + }) + + it('writes only an exclusive bounded result and removes partial output on failure', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-runtime-manifest-seed-signing-')) + temporaryDirectories.push(root) + const outputDirectory = join(root, 'result') + const result = await writeSshRelayRuntimeManifestSigningResult({ + requestArtifact: encodeSshRelayRuntimeManifestSigningRequestArtifact(request()), + seedBase64, + outputDirectory + }) + expect( + JSON.parse(await readFile(join(outputDirectory, 'signing-result.json'), 'utf8')) + ).toEqual(result) + await expect( + writeSshRelayRuntimeManifestSigningResult({ + requestArtifact: encodeSshRelayRuntimeManifestSigningRequestArtifact(request()), + seedBase64, + outputDirectory + }) + ).rejects.toThrow(/exclusive|absent/i) + + const failed = join(root, 'failed') + await expect( + writeSshRelayRuntimeManifestSigningResult({ + requestArtifact: { invalid: true }, + seedBase64, + outputDirectory: failed + }) + ).rejects.toThrow() + await expect(readFile(join(failed, 'signing-result.json'))).rejects.toMatchObject({ + code: 'ENOENT' + }) + }) + + it('rejects a symlinked request path before reading CLI input', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-runtime-manifest-seed-signing-')) + temporaryDirectories.push(root) + const target = join(root, 'request.json') + const linked = join(root, 'linked.json') + await writeFile( + target, + JSON.stringify(encodeSshRelayRuntimeManifestSigningRequestArtifact(request())) + ) + await expect( + import('node:fs/promises').then(({ symlink }) => symlink(target, linked)) + ).resolves.toBeUndefined() + const { readSshRelayRuntimeManifestSigningRequestArtifact } = + await import('./ssh-relay-runtime-manifest-seed-signing.mjs') + await expect(readSshRelayRuntimeManifestSigningRequestArtifact(linked)).rejects.toThrow( + /regular file|symlink/i + ) + }) +}) 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 68edae4cb89..8d895aaeafc 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 @@ -240,6 +240,10 @@ complete a box. hash-bound descriptors/receipts. The active slice is the callable, disconnected aggregate and protected Ed25519-signing workflow contract. The environment/seed are not provisioned, so live signing remains blocked and no release, desktop, publication, or tuple consumer is connected. + The missing-workflow/seed-signer RED is + `E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-RED-001`; the seed signer is locally green under + `E-M4-PROTECTED-MANIFEST-SEED-LOCAL-001`, while the filesystem aggregate command and workflow + remain 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 351efdd6526..50e1eb39676 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
      Last updated: 2026-07-15
      Current phase: Milestone 4 / Work Package 3 disconnected aggregate and protected manifest-signing workflow contract — **In progress — 2026-07-15, Codex implementation owner**. Linux aggregate-ready finalization is closed locally and on exact-head native x64/arm64 runners under E-M4-LINUX-FINALIZATION-LOCAL-RED-001, E-M4-LINUX-FINALIZATION-LOCAL-001, and E-M4-LINUX-FINALIZATION-CI-001. The active package must consume only the six exact aggregate-ready outputs, prepare canonical bytes credential-free, expose only those bytes to the tag-restricted `relay-runtime-manifest-signing` environment, accept only key ID plus Ed25519 signature, reconstruct and verify before final output, and fail closed on missing/extra/mutated inputs, approval/secret/signing failure, timeout, or partial output. It remains callable and disconnected. Release-cut, desktop builds, publication, and every tuple stay disconnected; production/default behavior is unchanged.
      -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact-head run 29419759256 at `91f59d19e5251fb7b6c9f6d397cd9cd82b43cbdf` passes all six native builds, both Linux userland supplements, and Windows x64 baseline. Windows arm64 fails only the retained build-floor gate after successful runtime verification (hosted 26200 versus required 26100). Downloaded artifacts 8344803641 and 8344824674 contain the expected Linux x64/arm64 descriptors and finalization receipts, whose tuple/content/archive/metadata bindings and smoke/resource metrics were inspected under E-M4-LINUX-FINALIZATION-CI-001. Golden E2E passes. PR Checks attempt 1 exposed one unrelated terminal-routing assertion after 29,944 tests passed; the focused test passes locally and exact-head attempt 2 passes all 29,945 tests, unpacked build, and packaged CLI smoke under E-M4-LINUX-FINALIZATION-PR-CHECKS-RED-001 and E-M4-LINUX-FINALIZATION-PR-CHECKS-001. No signing credential, aggregate workflow, 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.
      +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact-head run 29419759256 at `91f59d19e5251fb7b6c9f6d397cd9cd82b43cbdf` passes all six native builds, both Linux userland supplements, and Windows x64 baseline. Windows arm64 fails only the retained build-floor gate after successful runtime verification (hosted 26200 versus required 26100). Downloaded artifacts 8344803641 and 8344824674 contain the expected Linux x64/arm64 descriptors and finalization receipts, whose tuple/content/archive/metadata bindings and smoke/resource metrics were inspected under E-M4-LINUX-FINALIZATION-CI-001. Golden E2E and replacement PR Checks pass. The protected seed signer is now locally green under E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-RED-001 and E-M4-PROTECTED-MANIFEST-SEED-LOCAL-001: canonical request serialization, exact base64 32-byte seed validation, signature-only output, drift/symlink rejection, and exclusive failure cleanup pass, and both native contract families execute the tests. The callable workflow remains intentionally absent/red while the filesystem prepare/finalize command is implemented next. No signing credential, aggregate workflow, 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) @@ -10704,6 +10704,78 @@ through command-finished foreground confirmation` receives `csi-u` instead of `a - Follow-up: checkpoint the Linux finalization evidence and start the disconnected aggregate and protected manifest-signing workflow RED. +### E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-RED-001 — Callable protected signing workflow is absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted purpose-named test atop pushed head + `e5dfffaaa2df55b221c5ee30699ff8fe814b91aa`; draft PR #8741. +- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. Static repository + workflow-source read only; no Actions runner, environment approval, secret, signer, artifact + download, release, desktop consumer, SSH host, or enabled tuple. +- Command: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-manifest-signing-workflow.test.mjs`. +- Result: expected FAIL, 1 file / 1 test in 182 ms Vitest / 0.89 seconds wall, + 131,743,744-byte maximum RSS, 95,966,656-byte peak footprint, zero swaps. The only failure is + `ENOENT` for `.github/workflows/ssh-relay-runtime-manifest-signing.yml`. +- Seed-signer prerequisite RED: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs` + — expected FAIL before collection because + `config/scripts/ssh-relay-runtime-manifest-seed-signing.mjs` is absent; 196 ms Vitest / 0.90 + seconds wall, 132,022,272-byte maximum RSS, 96,228,776-byte peak footprint, zero swaps. The test + defines canonical request serialization, exact 32-byte base64 seed handling, signature-only + output, request/seed drift rejection, exclusive output cleanup, and symlink rejection. +- Oracle proved: no callable workflow currently enforces the required three-stage boundary: exact + six-artifact credential-free preparation; one protected `relay-runtime-manifest-signing` job with + the sole seed reference and signature-only output; then credential-free reconstruction and final + verification. The RED also requires SHA-pinned actions, strict timeouts, no wildcard artifact + merge, read-only permissions, and complete release/desktop/rehearsal disconnection. +- Does not prove: implementation, signer seed validation, file-based prepare/finalize behavior, + Actions execution, environment protection, real signatures, publication, desktop embedding, SSH + behavior, or an enabled tuple. +- Follow-up: implement the seed signer and exclusive filesystem prepare/finalize commands with + hostile-input tests, then add the callable workflow and make this contract green without + connecting any consumer. + +### E-M4-PROTECTED-MANIFEST-SEED-LOCAL-001 — Protected seed signer passes locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: local implementation atop pushed head `e5dfffaaa2df55b221c5ee30699ff8fe814b91aa`; + draft PR #8741. +- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. Test-only in-memory + Ed25519 seeds and local temporary files; no GitHub Environment, Actions secret, release, desktop + consumer, publication, SSH host, or enabled tuple. +- Focused command: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs` + — PASS, 3 files / 20 tests in 638 ms Vitest / 1.30 seconds wall, 188,612,608-byte maximum RSS, + 96,163,336-byte peak footprint, zero swaps. +- Broad release command excluding the deliberately still-red workflow-source test: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 $(rg --files config/scripts | rg '/ssh-relay.*\\.test\\.mjs$' | rg -v 'ssh-relay-runtime-manifest-signing-workflow\\.test\\.mjs$')` + — PASS, 49 files / 255 tests in 8.54 seconds Vitest / 9.46 seconds wall, + 188,858,368-byte maximum RSS, 96,523,568-byte peak footprint, zero swaps. The unfiltered 50-file + run passes the same 49 files / 255 tests and fails only the recorded missing-workflow RED. +- Desktop 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 493 ms Vitest / 1.12 seconds wall, 131,858,432-byte maximum RSS, + 96,032,120-byte peak footprint, zero swaps. +- Static results: `node --check`, focused `oxlint`, and focused `oxfmt --check` pass. `pnpm run +typecheck` passes in 1.88 seconds wall. Full `pnpm run lint` passes in 9.26 seconds wall with all 41 + reliability gates, the 355-entry max-lines ratchet, bundled-skill, localization catalog/parity, + and localization coverage gates; all 26 warnings are in untouched existing files. +- Oracle proved: request artifacts contain only the exact canonical payload plus algorithm/hash/size + bindings; malformed fields, noncanonical base64, payload drift, and noncanonical manifest bytes + fail. Only a canonical base64 32-byte seed is accepted. Output contains exactly key ID plus the + detached 64-byte signature, is created in an exclusive directory, and is removed on failure. + Symlinked request files are rejected. Both POSIX and Windows native artifact contract families now + syntax-check and run the purpose suite without exposing a secret. +- Does not prove: a real GitHub Environment or seed, reviewer approval, the filesystem aggregate + prepare/finalize boundary, the callable workflow, Actions execution, publication, desktop + embedding, SSH behavior, or an enabled tuple. +- Follow-up: implement the exclusive six-artifact filesystem prepare/finalize command, including + receipt/path/mutation/accepted-key/signature/partial-output failures, before adding the callable + workflow. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -10762,14 +10834,15 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Checkpoint E-M4-LINUX-FINALIZATION-CI-001 and the green PR Checks replacement. Then begin a -purpose-named RED for the callable, disconnected aggregate/immutable-manifest workflow: consume all -six exact aggregate-ready artifacts, prepare canonical bytes without credentials, isolate the -base64 32-byte seed in the tag-restricted `relay-runtime-manifest-signing` environment, return only -key ID plus signature, reconstruct and verify before emitting final output, and remove partial -output on every failure. The environment and secret remain unprovisioned, so live signing evidence -must stay BLOCKED. Keep Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, -desktop builds, publication, and every tuple separately gated. Do not merge to `main`; retain every +Checkpoint the locally proven protected seed signer and require its contract on all six native test +families. Then implement the exclusive filesystem prepare/finalize command that collects only the +six exact aggregate-ready artifacts, binds receipts and accepted public keys, serializes canonical +bytes without credentials, reconstructs after signature return, and removes partial output on every +failure. Only after that command is green may the still-red callable workflow isolate the base64 +32-byte seed in the tag-restricted `relay-runtime-manifest-signing` environment. The environment, +accepted production keys, and secret remain unprovisioned, so live signing evidence must stay +BLOCKED. Keep Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, +publication, and every tuple separately gated. Do not merge to `main`; retain every production/default gate. Cross-family Layer B targets, the protected manifest-signing environment, oldest-baseline/native- From 8cf80a0228ffa095beceec018a0de2e237b89e2d Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 10:33:16 -0400 Subject: [PATCH 131/267] ci: add fail-closed relay manifest aggregation Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 11 + ...lay-runtime-manifest-aggregate-command.mjs | 401 +++++++++++++++++ ...untime-manifest-aggregate-command.test.mjs | 420 ++++++++++++++++++ ...y-runtime-manifest-artifact-collection.mjs | 236 ++++++++++ ...elease-implementation-checklist-summary.md | 7 +- ...github-release-implementation-checklist.md | 110 ++++- 6 files changed, 1172 insertions(+), 13 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-manifest-aggregate-command.mjs create mode 100644 config/scripts/ssh-relay-runtime-manifest-aggregate-command.test.mjs create mode 100644 config/scripts/ssh-relay-runtime-manifest-artifact-collection.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index f58cadb26c0..52e7101ccc0 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -168,6 +168,9 @@ jobs: node --check config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs node --check config/scripts/ssh-relay-runtime-manifest-seed-signing.mjs node --check config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs + node --check config/scripts/ssh-relay-runtime-manifest-artifact-collection.mjs + node --check config/scripts/ssh-relay-runtime-manifest-aggregate-command.mjs + node --check config/scripts/ssh-relay-runtime-manifest-aggregate-command.test.mjs node --check config/scripts/ssh-relay-runtime-manifest-aggregate.mjs node --check config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs node --check config/scripts/ssh-relay-runtime-manifest-tuple.mjs @@ -215,6 +218,7 @@ jobs: config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs \ config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs \ config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs \ + config/scripts/ssh-relay-runtime-manifest-aggregate-command.test.mjs \ config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs \ config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs \ config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs \ @@ -585,6 +589,12 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime manifest seed signing syntax check failed' } node --check config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime manifest seed signing test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-manifest-artifact-collection.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime manifest artifact collection syntax check failed' } + node --check config/scripts/ssh-relay-runtime-manifest-aggregate-command.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime manifest aggregate command syntax check failed' } + node --check config/scripts/ssh-relay-runtime-manifest-aggregate-command.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime manifest aggregate command test syntax check failed' } node --check config/scripts/ssh-relay-runtime-manifest-aggregate.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime manifest aggregate syntax check failed' } node --check config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs @@ -650,6 +660,7 @@ jobs: config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs ` config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs ` config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs ` + config/scripts/ssh-relay-runtime-manifest-aggregate-command.test.mjs ` config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs ` config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs ` config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs ` diff --git a/config/scripts/ssh-relay-runtime-manifest-aggregate-command.mjs b/config/scripts/ssh-relay-runtime-manifest-aggregate-command.mjs new file mode 100644 index 00000000000..4bc3f018908 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-manifest-aggregate-command.mjs @@ -0,0 +1,401 @@ +import { createHash } from 'node:crypto' +import { lstat, mkdir, readFile, realpath, rm, writeFile } from 'node:fs/promises' +import { basename, dirname, join, resolve } from 'node:path' +import { pathToFileURL } from 'node:url' + +import { collectSshRelayRuntimeManifestArtifacts } from './ssh-relay-runtime-manifest-artifact-collection.mjs' +import { + finalizeSshRelayRuntimeManifestAggregate, + prepareSshRelayRuntimeManifestAggregate +} from './ssh-relay-runtime-manifest-aggregate.mjs' +import { + decodeSshRelayRuntimeManifestSigningRequestArtifact, + encodeSshRelayRuntimeManifestSigningRequestArtifact +} from './ssh-relay-runtime-manifest-seed-signing.mjs' +import { sshRelayRuntimeManifestKeyId } from './ssh-relay-runtime-manifest-signing-handoff.mjs' + +const MAX_JSON_BYTES = 48 * 1024 * 1024 +const SOURCE_SHA = /^[0-9a-f]{40}$/u +const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u +const BASE64 = /^[A-Za-z0-9+/]+={0,2}$/u +const DIGEST = /^sha256:[0-9a-f]{64}$/u +const PREPARED_FIELDS = [ + 'acceptedKeysSha256', + 'createdAt', + 'inputTuples', + 'inputTuplesSha256', + 'relayProtocolVersion', + 'releaseTag', + 'schemaVersion', + 'signingRequest', + 'sourceSha', + 'unsignedManifest' +] + +function assertObject(value, label) { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`SSH relay runtime manifest aggregate command ${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 manifest aggregate command ${label} has invalid fields`) + } +} + +function sha256(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +function canonicalJson(value) { + return Buffer.from(`${JSON.stringify(value)}\n`, 'utf8') +} + +function parseReleaseTag(tag) { + for (const [pattern, channel] of [ + [/^v(\d+\.\d+\.\d+)$/u, 'stable'], + [/^v(\d+\.\d+\.\d+-rc\.\d+)$/u, 'rc'], + [/^v(\d+\.\d+\.\d+-rc\.\d+\.perf)$/u, 'perf'] + ]) { + const match = pattern.exec(tag) + if (match) { + return { tag, version: match[1], channel } + } + } + throw new Error('SSH relay runtime manifest aggregate command release tag is invalid') +} + +function commandIdentity(input) { + if (!SOURCE_SHA.test(input.sourceSha ?? '')) { + throw new Error('SSH relay runtime manifest aggregate command source SHA is invalid') + } + const release = parseReleaseTag(input.releaseTag) + if ( + typeof input.createdAt !== 'string' || + !TIMESTAMP.test(input.createdAt) || + new Date(input.createdAt).toISOString() !== input.createdAt + ) { + throw new Error('SSH relay runtime manifest aggregate command timestamp is invalid') + } + if (!Number.isSafeInteger(input.relayProtocolVersion) || input.relayProtocolVersion <= 0) { + throw new Error('SSH relay runtime manifest aggregate command protocol version is invalid') + } + return { + sourceSha: input.sourceSha, + releaseTag: input.releaseTag, + createdAt: input.createdAt, + relayProtocolVersion: input.relayProtocolVersion, + build: { ...release, relayProtocolVersion: input.relayProtocolVersion } + } +} + +async function readBoundedJson(path, label) { + const before = await lstat(path, { bigint: true }) + if ( + !before.isFile() || + before.isSymbolicLink() || + before.size <= 0n || + before.size > BigInt(MAX_JSON_BYTES) + ) { + throw new Error( + `SSH relay runtime manifest aggregate command ${label} must be bounded regular JSON` + ) + } + try { + const bytes = await readFile(path) + const after = await lstat(path, { 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('file changed while reading') + } + return JSON.parse(bytes.toString('utf8')) + } catch (error) { + throw new Error( + `SSH relay runtime manifest aggregate command ${label} is invalid JSON: ${error.message}` + ) + } +} + +function decodeCanonicalBase64(value, label, expectedSize) { + if (typeof value !== 'string' || !BASE64.test(value)) { + throw new Error(`SSH relay runtime manifest aggregate command ${label} is not canonical base64`) + } + const bytes = Buffer.from(value, 'base64') + if (bytes.length !== expectedSize || bytes.toString('base64') !== value) { + throw new Error( + `SSH relay runtime manifest aggregate command ${label} must contain ${expectedSize} bytes` + ) + } + return bytes +} + +async function readAcceptedKeys(path) { + const document = await readBoundedJson(resolve(path), 'accepted keys') + assertExactFields(document, ['keys', 'schemaVersion'], 'accepted keys') + if ( + document.schemaVersion !== 1 || + !Array.isArray(document.keys) || + document.keys.length === 0 || + document.keys.length > 4 + ) { + throw new Error('SSH relay runtime manifest aggregate command accepted keys are invalid') + } + const seen = new Set() + const artifactKeys = document.keys + .map((key, index) => { + assertExactFields(key, ['keyId', 'publicKeyBase64'], `accepted key ${index}`) + const publicKey = decodeCanonicalBase64(key.publicKeyBase64, 'accepted public key', 32) + const keyId = sshRelayRuntimeManifestKeyId(publicKey) + if (!DIGEST.test(key.keyId ?? '') || key.keyId !== keyId || seen.has(keyId)) { + throw new Error( + 'SSH relay runtime manifest aggregate command accepted key identity is invalid' + ) + } + seen.add(keyId) + return { keyId, publicKeyBase64: key.publicKeyBase64, publicKey } + }) + .sort((left, right) => (left.keyId < right.keyId ? -1 : left.keyId > right.keyId ? 1 : 0)) + const serialized = artifactKeys.map(({ keyId, publicKeyBase64 }) => ({ keyId, publicKeyBase64 })) + return { + acceptedKeys: artifactKeys.map(({ keyId, publicKey }) => ({ keyId, publicKey })), + sha256: sha256(canonicalJson({ schemaVersion: 1, keys: serialized })) + } +} + +async function exclusiveOutputDirectory(outputDirectory) { + const absolute = resolve(outputDirectory) + const parent = resolve(await realpath(dirname(absolute))) + const parentMetadata = await lstat(parent) + if (!parentMetadata.isDirectory() || parentMetadata.isSymbolicLink()) { + throw new Error('SSH relay runtime manifest aggregate command output parent must be real') + } + const output = resolve(parent, basename(absolute)) + try { + await lstat(output) + } catch (error) { + if (error.code === 'ENOENT') { + return output + } + throw error + } + throw new Error( + 'SSH relay runtime manifest aggregate command requires an exclusive output directory' + ) +} + +function assertReceiptBindings(bindings, prepared) { + const tuples = new Map(prepared.unsignedManifest.tuples.map((tuple) => [tuple.tupleId, tuple])) + for (const binding of bindings) { + if (tuples.get(binding.tupleId)?.contentId !== binding.contentId) { + throw new Error( + `SSH relay runtime manifest aggregate command receipt content drifted: ${binding.tupleId}` + ) + } + } +} + +function preparedArtifact(identity, acceptedKeysSha256, prepared) { + return { + schemaVersion: 1, + sourceSha: identity.sourceSha, + releaseTag: identity.releaseTag, + createdAt: identity.createdAt, + relayProtocolVersion: identity.relayProtocolVersion, + acceptedKeysSha256, + inputTuples: prepared.inputTuples, + inputTuplesSha256: prepared.inputTuplesSha256, + unsignedManifest: prepared.unsignedManifest, + signingRequest: encodeSshRelayRuntimeManifestSigningRequestArtifact(prepared.signingRequest) + } +} + +async function collectAndPrepare(input, output, identity, signal) { + const stagingDirectory = join(output, 'aggregate-input') + await mkdir(stagingDirectory) + const bindings = await collectSshRelayRuntimeManifestArtifacts({ + artifactsDirectory: input.artifactsDirectory, + stagingDirectory, + signal + }) + const prepared = await prepareSshRelayRuntimeManifestAggregate({ + inputDirectory: stagingDirectory, + build: identity.build, + createdAt: identity.createdAt, + tupleInputs: bindings.map(({ aggregateInput }) => aggregateInput), + signal + }) + assertReceiptBindings(bindings, prepared) + return prepared +} + +export async function prepareSshRelayRuntimeManifestAggregateCommand(input) { + const identity = commandIdentity(input) + // Why: public-key policy must be valid before canonical bytes can leave this boundary. + const keys = await readAcceptedKeys(input.acceptedKeysPath) + const output = await exclusiveOutputDirectory(input.outputDirectory) + let created = false + try { + await mkdir(output, { mode: 0o700 }) + created = true + const prepared = await collectAndPrepare(input, output, identity, input.signal) + const artifact = preparedArtifact(identity, keys.sha256, prepared) + await writeFile(join(output, 'prepared-aggregate.json'), canonicalJson(artifact), { + flag: 'wx', + mode: 0o600 + }) + await writeFile(join(output, 'signing-request.json'), canonicalJson(artifact.signingRequest), { + flag: 'wx', + mode: 0o600 + }) + await rm(join(output, 'aggregate-input'), { recursive: true, force: true }) + return prepared + } catch (error) { + if (created) { + await rm(output, { recursive: true, force: true }) + } + throw error + } +} + +function normalizePreparedArtifact(value) { + assertExactFields(value, PREPARED_FIELDS, 'prepared aggregate') + if (value.schemaVersion !== 1 || !DIGEST.test(value.acceptedKeysSha256 ?? '')) { + throw new Error('SSH relay runtime manifest aggregate command prepared aggregate is invalid') + } + decodeSshRelayRuntimeManifestSigningRequestArtifact(value.signingRequest) + return value +} + +function sameJson(left, right) { + return JSON.stringify(left) === JSON.stringify(right) +} + +export async function finalizeSshRelayRuntimeManifestAggregateCommand(input) { + const identity = commandIdentity(input) + const keys = await readAcceptedKeys(input.acceptedKeysPath) + const prior = normalizePreparedArtifact( + await readBoundedJson( + join(resolve(input.preparedDirectory), 'prepared-aggregate.json'), + 'prepared aggregate' + ) + ) + const signingResult = await readBoundedJson(resolve(input.signingResultPath), 'signing result') + assertExactFields(signingResult, ['keyId', 'signature'], 'signing result') + const output = await exclusiveOutputDirectory(input.outputDirectory) + let created = false + try { + await mkdir(output, { mode: 0o700 }) + created = true + const prepared = await collectAndPrepare(input, output, identity, input.signal) + const current = preparedArtifact(identity, keys.sha256, prepared) + if (!sameJson(prior, current)) { + throw new Error('SSH relay runtime manifest aggregate command prepared receipt drifted') + } + const finalized = finalizeSshRelayRuntimeManifestAggregate({ + prepared, + signingResults: [signingResult], + acceptedKeys: keys.acceptedKeys + }) + const assets = join(output, 'assets') + const evidence = join(output, 'evidence') + await Promise.all([mkdir(assets), mkdir(evidence)]) + await writeFile(join(assets, finalized.manifestAsset.name), finalized.bytes, { + flag: 'wx', + mode: 0o600 + }) + await writeFile( + join(evidence, 'manifest-aggregate.json'), + canonicalJson({ + schemaVersion: 1, + sourceSha: identity.sourceSha, + releaseTag: identity.releaseTag, + acceptedKeysSha256: keys.sha256, + inputTuplesSha256: finalized.inputTuplesSha256, + manifestAsset: finalized.manifestAsset + }), + { flag: 'wx', mode: 0o600 } + ) + await rm(join(output, 'aggregate-input'), { recursive: true, force: true }) + return finalized + } catch (error) { + if (created) { + await rm(output, { recursive: true, force: true }) + } + throw error + } +} + +export function parseSshRelayRuntimeManifestAggregateCommandArguments(argv) { + const command = argv[0] + if (command !== 'prepare' && command !== 'finalize') { + throw new Error('SSH relay runtime manifest aggregate command requires prepare or finalize') + } + const fields = new Map([ + ['--artifacts-directory', 'artifactsDirectory'], + ['--accepted-keys', 'acceptedKeysPath'], + ['--output-directory', 'outputDirectory'], + ['--source-sha', 'sourceSha'], + ['--release-tag', 'releaseTag'], + ['--created-at', 'createdAt'], + ['--relay-protocol-version', 'relayProtocolVersion'], + ['--prepared-directory', 'preparedDirectory'], + ['--signing-result', 'signingResultPath'] + ]) + const result = { command } + for (let index = 1; index < argv.length; index += 2) { + const field = fields.get(argv[index]) + const value = argv[index + 1] + if (!field || !value || value.startsWith('--') || result[field]) { + throw new Error( + `Invalid SSH relay runtime manifest aggregate command argument: ${argv[index]}` + ) + } + result[field] = field === 'relayProtocolVersion' ? Number(value) : value + } + const required = [ + 'artifactsDirectory', + 'acceptedKeysPath', + 'outputDirectory', + 'sourceSha', + 'releaseTag', + 'createdAt', + 'relayProtocolVersion' + ] + if (command === 'finalize') { + required.push('preparedDirectory', 'signingResultPath') + } + for (const field of required) { + if (!result[field]) { + throw new Error(`Missing SSH relay runtime manifest aggregate command argument: ${field}`) + } + } + return result +} + +async function main() { + const options = parseSshRelayRuntimeManifestAggregateCommandArguments(process.argv.slice(2)) + const operation = + options.command === 'prepare' + ? prepareSshRelayRuntimeManifestAggregateCommand + : finalizeSshRelayRuntimeManifestAggregateCommand + const result = await operation(options) + process.stdout.write(`${JSON.stringify({ inputTuplesSha256: result.inputTuplesSha256 })}\n`) +} + +if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) { + main().catch((error) => { + process.stderr.write( + `SSH relay runtime manifest aggregate command failed: ${error.stack ?? error}\n` + ) + process.exitCode = 1 + }) +} diff --git a/config/scripts/ssh-relay-runtime-manifest-aggregate-command.test.mjs b/config/scripts/ssh-relay-runtime-manifest-aggregate-command.test.mjs new file mode 100644 index 00000000000..16e9fb78f7c --- /dev/null +++ b/config/scripts/ssh-relay-runtime-manifest-aggregate-command.test.mjs @@ -0,0 +1,420 @@ +import { createHash } from 'node:crypto' +import { lstat, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import nacl from 'tweetnacl' +import { afterEach, describe, expect, it } from 'vitest' + +import { createSshRelayArtifactTestManifest } from '../../src/main/ssh/ssh-relay-artifact-test-manifest.ts' +import { verifySshRelayArtifactManifest } from '../../src/main/ssh/ssh-relay-manifest-signature.ts' +import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' +import { + finalizeSshRelayRuntimeManifestAggregateCommand, + parseSshRelayRuntimeManifestAggregateCommandArguments, + prepareSshRelayRuntimeManifestAggregateCommand +} from './ssh-relay-runtime-manifest-aggregate-command.mjs' +import { + encodeSshRelayRuntimeManifestSigningRequestArtifact, + signSshRelayRuntimeManifestRequest +} from './ssh-relay-runtime-manifest-seed-signing.mjs' +import { sshRelayRuntimeManifestKeyId } from './ssh-relay-runtime-manifest-signing-handoff.mjs' + +const temporaryDirectories = [] +const keyPair = nacl.sign.keyPair.fromSeed(Uint8Array.from({ length: 32 }, (_, index) => index)) +const TUPLES = [ + 'linux-x64-glibc', + 'linux-arm64-glibc', + 'darwin-x64', + 'darwin-arm64', + 'win32-x64', + 'win32-arm64' +] + +function digest(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +function fileReference(name, bytes) { + return { name, size: bytes.length, sha256: digest(bytes) } +} + +function watcherPackage(tupleId) { + return tupleId.startsWith('linux-') + ? `@parcel/watcher-linux-${tupleId.split('-')[1]}-glibc` + : `@parcel/watcher-${tupleId}` +} + +function nativeEntry(path, role, marker) { + return { + path, + type: 'file', + role, + size: marker.length, + mode: 0o755, + sha256: digest(marker) + } +} + +function tupleFor(tupleId) { + const tuple = structuredClone(createSshRelayArtifactTestManifest().tuples[0]) + const [os, architecture] = tupleId.split('-') + tuple.tupleId = tupleId + tuple.os = os + tuple.architecture = architecture + tuple.compatibility = structuredClone(sshRelayRuntimeCompatibility[tupleId]) + const packageName = watcherPackage(tupleId) + tuple.entries = tuple.entries.map((entry) => { + const next = structuredClone(entry) + next.path = next.path.replace('@parcel/watcher-linux-x64-glibc', packageName) + if (next.role === 'node') { + next.path = os === 'win32' ? 'bin/node.exe' : 'bin/node' + } + return next + }) + if (os === 'darwin') { + tuple.entries.push( + nativeEntry( + 'node_modules/node-pty/build/Release/spawn-helper', + 'native-runtime', + `${tupleId}:spawn-helper` + ) + ) + } + if (os === 'win32') { + tuple.entries = tuple.entries.filter((entry) => entry.role !== 'node-pty-native') + tuple.entries.push( + { path: 'node_modules/node-pty/build/Release/conpty', type: 'directory', mode: 0o755 }, + nativeEntry( + 'node_modules/node-pty/build/Release/conpty.node', + 'node-pty-native', + `${tupleId}:conpty-node` + ), + nativeEntry( + 'node_modules/node-pty/build/Release/conpty_console_list.node', + 'node-pty-native', + `${tupleId}:console-list` + ), + nativeEntry( + 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe', + 'native-runtime', + `${tupleId}:open-console` + ), + nativeEntry( + 'node_modules/node-pty/build/Release/conpty/conpty.dll', + 'native-runtime', + `${tupleId}:conpty-dll` + ) + ) + } + const nativeRoles = new Set([ + 'node', + 'node-pty-native', + 'parcel-watcher-native', + 'native-runtime' + ]) + tuple.nativeVerification = { + policy: + os === 'linux' + ? 'linux-hash-only-v1' + : os === 'darwin' + ? 'apple-developer-id-v1' + : 'signpath-authenticode-v1', + tool: { name: 'fixture-verifier', version: '1.0.0' }, + verifiedAt: '2026-07-15T00:00:00.000Z', + files: tuple.entries + .filter((entry) => entry.type === 'file' && nativeRoles.has(entry.role)) + .map(({ path, sha256 }) => ({ path, sha256 })) + } + const files = tuple.entries.filter((entry) => entry.type === 'file') + tuple.archive.fileCount = files.length + tuple.archive.expandedSize = files.reduce((total, entry) => total + entry.size, 0) + tuple.metadataAssets.sbom.name = `orca-ssh-relay-runtime-${tupleId}.spdx.json` + tuple.metadataAssets.provenance.name = `orca-ssh-relay-runtime-${tupleId}.provenance.json` + tuple.contentId = computeSshRelayRuntimeContentId(tuple) + const extension = os === 'win32' ? 'zip' : 'tar.xz' + tuple.archive.name = `orca-ssh-relay-runtime-v1-${tupleId}-${tuple.contentId.slice(7)}.${extension}` + return tuple +} + +async function writeTupleArtifact(root, tupleId) { + const tuple = tupleFor(tupleId) + const archiveBytes = Buffer.from(`${tupleId}:archive`) + const sbomBytes = Buffer.from(`${JSON.stringify({ spdxVersion: 'SPDX-2.3', tupleId })}\n`) + const provenanceBytes = Buffer.from(`${JSON.stringify({ _type: 'in-toto', tupleId })}\n`) + tuple.archive.size = archiveBytes.length + tuple.archive.sha256 = digest(archiveBytes) + tuple.metadataAssets.sbom = fileReference(tuple.metadataAssets.sbom.name, sbomBytes) + tuple.metadataAssets.provenance = fileReference( + tuple.metadataAssets.provenance.name, + provenanceBytes + ) + const descriptorBytes = Buffer.from( + `${JSON.stringify({ schemaVersion: 1, tuple }, null, 2)}\n`, + 'utf8' + ) + const input = { + tupleId, + descriptor: fileReference( + `orca-ssh-relay-runtime-${tupleId}.manifest-tuple.json`, + descriptorBytes + ), + archive: fileReference(tuple.archive.name, archiveBytes), + sbom: { ...tuple.metadataAssets.sbom }, + provenance: { ...tuple.metadataAssets.provenance } + } + const artifactName = tupleId.startsWith('linux-') + ? `ssh-relay-runtime-${tupleId}` + : `ssh-relay-runtime-signed-${tupleId}` + const artifactRoot = join(root, artifactName) + const assetsRoot = tupleId.startsWith('linux-') ? artifactRoot : join(artifactRoot, 'assets') + const evidenceRoot = tupleId.startsWith('linux-') ? artifactRoot : join(artifactRoot, 'evidence') + await mkdir(assetsRoot, { recursive: true }) + await mkdir(evidenceRoot, { recursive: true }) + await Promise.all([ + writeFile(join(assetsRoot, input.descriptor.name), descriptorBytes), + writeFile(join(assetsRoot, input.archive.name), archiveBytes), + writeFile(join(assetsRoot, input.sbom.name), sbomBytes), + writeFile(join(assetsRoot, input.provenance.name), provenanceBytes) + ]) + const receipt = tupleId.startsWith('linux-') + ? { tupleId, contentId: tuple.contentId, verification: {}, aggregateInput: input } + : { + tupleId, + sourceContentId: tuple.contentId, + finalContentId: tuple.contentId, + returnedFiles: [], + nativeVerification: tuple.nativeVerification, + smoke: {}, + metadata: {}, + aggregateInput: input + } + const suffix = tupleId.startsWith('linux-') ? 'linux-finalization' : 'finalization' + await writeFile(join(evidenceRoot, `${tupleId}.${suffix}.json`), `${JSON.stringify(receipt)}\n`) + return { artifactRoot, assetsRoot, evidenceRoot, input, tuple } +} + +async function fixture() { + const root = await mkdtemp(join(tmpdir(), 'orca-manifest-command-')) + temporaryDirectories.push(root) + const artifactsDirectory = join(root, 'artifacts') + await mkdir(artifactsDirectory) + const tuples = [] + for (const tupleId of TUPLES) { + tuples.push(await writeTupleArtifact(artifactsDirectory, tupleId)) + } + const acceptedKeysPath = join(root, 'accepted-keys.json') + const acceptedKeys = { + schemaVersion: 1, + keys: [ + { + keyId: sshRelayRuntimeManifestKeyId(keyPair.publicKey), + publicKeyBase64: Buffer.from(keyPair.publicKey).toString('base64') + } + ] + } + await writeFile(acceptedKeysPath, `${JSON.stringify(acceptedKeys)}\n`) + return { + root, + artifactsDirectory, + acceptedKeysPath, + acceptedKeys, + tuples, + prepareOutput: join(root, 'prepared'), + finalOutput: join(root, 'final'), + sourceSha: 'a'.repeat(40), + releaseTag: 'v1.4.140-rc.1', + createdAt: '2026-07-15T01:02:03.000Z', + relayProtocolVersion: 1 + } +} + +function commandInput(value, outputDirectory = value.prepareOutput) { + return { + artifactsDirectory: value.artifactsDirectory, + acceptedKeysPath: value.acceptedKeysPath, + outputDirectory, + sourceSha: value.sourceSha, + releaseTag: value.releaseTag, + createdAt: value.createdAt, + relayProtocolVersion: value.relayProtocolVersion + } +} + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })) + ) +}) + +describe('SSH relay runtime manifest aggregate filesystem command', () => { + it('independently recollects six receipt-bound artifacts around signature-only handoff', async () => { + const value = await fixture() + const prepared = await prepareSshRelayRuntimeManifestAggregateCommand(commandInput(value)) + const requestArtifact = JSON.parse( + await readFile(join(value.prepareOutput, 'signing-request.json'), 'utf8') + ) + expect(requestArtifact).toEqual( + encodeSshRelayRuntimeManifestSigningRequestArtifact(prepared.signingRequest) + ) + const signingResult = signSshRelayRuntimeManifestRequest({ + requestArtifact, + seedBase64: Buffer.from(keyPair.secretKey.subarray(0, 32)).toString('base64') + }) + const signingResultPath = join(value.root, 'signing-result.json') + await writeFile(signingResultPath, `${JSON.stringify(signingResult)}\n`) + const finalized = await finalizeSshRelayRuntimeManifestAggregateCommand({ + ...commandInput(value, value.finalOutput), + preparedDirectory: value.prepareOutput, + signingResultPath + }) + + expect(finalized.manifest.tuples.map((tuple) => tuple.tupleId)).toEqual(TUPLES.toSorted()) + expect( + verifySshRelayArtifactManifest(finalized.manifest, [ + { keyId: value.acceptedKeys.keys[0].keyId, publicKey: keyPair.publicKey } + ]) + ).toEqual(finalized.manifest) + const finalEntries = (await lstat(value.finalOutput)).isDirectory() + expect(finalEntries).toBe(true) + await expect(lstat(join(value.finalOutput, 'aggregate-input'))).rejects.toMatchObject({ + code: 'ENOENT' + }) + }) + + it('rejects missing, extra, mutated, duplicate, and receipt-drifted artifact inputs', async () => { + const missing = await fixture() + await rm(missing.tuples[0].artifactRoot, { recursive: true }) + await expect( + prepareSshRelayRuntimeManifestAggregateCommand(commandInput(missing)) + ).rejects.toThrow(/missing|unexpected/i) + + const extra = await fixture() + await mkdir(join(extra.artifactsDirectory, 'ssh-relay-runtime-extra')) + await expect( + prepareSshRelayRuntimeManifestAggregateCommand(commandInput(extra)) + ).rejects.toThrow(/missing|unexpected/i) + + const mutated = await fixture() + await writeFile( + join(mutated.tuples[1].assetsRoot, mutated.tuples[1].input.archive.name), + 'drift' + ) + await expect( + prepareSshRelayRuntimeManifestAggregateCommand(commandInput(mutated)) + ).rejects.toThrow(/size|sha-?256|changed/i) + + const receiptDrift = await fixture() + const receiptPath = join(receiptDrift.tuples[2].evidenceRoot, `${TUPLES[2]}.finalization.json`) + const document = JSON.parse(await readFile(receiptPath, 'utf8')) + document.tupleId = TUPLES[3] + await writeFile(receiptPath, `${JSON.stringify(document)}\n`) + await expect( + prepareSshRelayRuntimeManifestAggregateCommand(commandInput(receiptDrift)) + ).rejects.toThrow(/receipt|tuple/i) + + const duplicate = await fixture() + const duplicateReceiptPath = join( + duplicate.tuples[1].evidenceRoot, + `${TUPLES[1]}.linux-finalization.json` + ) + const duplicateReceipt = JSON.parse(await readFile(duplicateReceiptPath, 'utf8')) + duplicateReceipt.aggregateInput.archive.name = duplicate.tuples[0].input.archive.name + await writeFile(duplicateReceiptPath, `${JSON.stringify(duplicateReceipt)}\n`) + await expect( + prepareSshRelayRuntimeManifestAggregateCommand(commandInput(duplicate)) + ).rejects.toThrow(/duplicate/i) + }) + + it.skipIf(process.platform === 'win32')('rejects linked finalization receipts', async () => { + const linked = await fixture() + const receipt = join(linked.tuples[0].evidenceRoot, `${TUPLES[0]}.linux-finalization.json`) + const realReceipt = `${receipt}.real` + await writeFile(realReceipt, await readFile(receipt)) + await rm(receipt) + await symlink(realReceipt, receipt) + await expect( + prepareSshRelayRuntimeManifestAggregateCommand(commandInput(linked)) + ).rejects.toThrow(/regular|symbolic|link/i) + }) + + it('validates accepted keys before exposing a request and removes partial final output', async () => { + const invalidKeys = await fixture() + invalidKeys.acceptedKeys.keys[0].publicKeyBase64 = Buffer.alloc(31).toString('base64') + await writeFile(invalidKeys.acceptedKeysPath, JSON.stringify(invalidKeys.acceptedKeys)) + await expect( + prepareSshRelayRuntimeManifestAggregateCommand(commandInput(invalidKeys)) + ).rejects.toThrow(/public key|32 bytes|accepted key/i) + await expect(lstat(invalidKeys.prepareOutput)).rejects.toMatchObject({ code: 'ENOENT' }) + + const invalidSignature = await fixture() + await prepareSshRelayRuntimeManifestAggregateCommand(commandInput(invalidSignature)) + const signingResultPath = join(invalidSignature.root, 'signing-result.json') + await writeFile( + signingResultPath, + JSON.stringify({ + keyId: invalidSignature.acceptedKeys.keys[0].keyId, + signature: Buffer.alloc(64).toString('base64') + }) + ) + await expect( + finalizeSshRelayRuntimeManifestAggregateCommand({ + ...commandInput(invalidSignature, invalidSignature.finalOutput), + preparedDirectory: invalidSignature.prepareOutput, + signingResultPath + }) + ).rejects.toThrow(/signature/i) + await expect(lstat(invalidSignature.finalOutput)).rejects.toMatchObject({ code: 'ENOENT' }) + }) + + it('rejects prepared handoff drift, cancellation, and incomplete CLI arguments', async () => { + const drifted = await fixture() + const prepared = await prepareSshRelayRuntimeManifestAggregateCommand(commandInput(drifted)) + const requestArtifact = encodeSshRelayRuntimeManifestSigningRequestArtifact( + prepared.signingRequest + ) + const signingResultPath = join(drifted.root, 'signing-result.json') + await writeFile( + signingResultPath, + JSON.stringify({ + ...signSshRelayRuntimeManifestRequest({ + requestArtifact, + seedBase64: Buffer.from(keyPair.secretKey.subarray(0, 32)).toString('base64') + }) + }) + ) + const preparedPath = join(drifted.prepareOutput, 'prepared-aggregate.json') + const preparedDocument = JSON.parse(await readFile(preparedPath, 'utf8')) + preparedDocument.sourceSha = 'b'.repeat(40) + await writeFile(preparedPath, `${JSON.stringify(preparedDocument)}\n`) + await expect( + finalizeSshRelayRuntimeManifestAggregateCommand({ + ...commandInput(drifted, drifted.finalOutput), + preparedDirectory: drifted.prepareOutput, + signingResultPath + }) + ).rejects.toThrow(/prepared|drift/i) + await expect(lstat(drifted.finalOutput)).rejects.toMatchObject({ code: 'ENOENT' }) + + const cancelled = await fixture() + const controller = new AbortController() + controller.abort(new Error('cancel aggregate collection')) + await expect( + prepareSshRelayRuntimeManifestAggregateCommand({ + ...commandInput(cancelled), + signal: controller.signal + }) + ).rejects.toThrow(/cancel aggregate collection/i) + await expect(lstat(cancelled.prepareOutput)).rejects.toMatchObject({ code: 'ENOENT' }) + + expect(() => parseSshRelayRuntimeManifestAggregateCommandArguments(['prepare'])).toThrow( + /missing/i + ) + expect(() => + parseSshRelayRuntimeManifestAggregateCommandArguments(['finalize', '--unknown', 'value']) + ).toThrow(/invalid/i) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-manifest-artifact-collection.mjs b/config/scripts/ssh-relay-runtime-manifest-artifact-collection.mjs new file mode 100644 index 00000000000..23e120f1da3 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-manifest-artifact-collection.mjs @@ -0,0 +1,236 @@ +import { constants } from 'node:fs' +import { copyFile, lstat, readFile, readdir } from 'node:fs/promises' +import { basename, join, resolve } from 'node:path' + +const MAX_RECEIPT_BYTES = 32 * 1024 * 1024 +const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,239}$/u +const DIGEST = /^sha256:[0-9a-f]{64}$/u +const INPUT_FIELDS = ['archive', 'descriptor', 'provenance', 'sbom', 'tupleId'] +const FILE_FIELDS = ['name', 'sha256', 'size'] + +export const SSH_RELAY_RUNTIME_MANIFEST_ARTIFACTS = Object.freeze([ + { + tupleId: 'linux-x64-glibc', + artifactName: 'ssh-relay-runtime-linux-x64-glibc', + layout: 'flat' + }, + { + tupleId: 'linux-arm64-glibc', + artifactName: 'ssh-relay-runtime-linux-arm64-glibc', + layout: 'flat' + }, + { + tupleId: 'darwin-x64', + artifactName: 'ssh-relay-runtime-signed-darwin-x64', + layout: 'signed' + }, + { + tupleId: 'darwin-arm64', + artifactName: 'ssh-relay-runtime-signed-darwin-arm64', + layout: 'signed' + }, + { + tupleId: 'win32-x64', + artifactName: 'ssh-relay-runtime-signed-win32-x64', + layout: 'signed' + }, + { + tupleId: 'win32-arm64', + artifactName: 'ssh-relay-runtime-signed-win32-arm64', + layout: 'signed' + } +]) + +function assertObject(value, label) { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`SSH relay runtime manifest artifact ${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 manifest artifact ${label} has invalid fields`) + } +} + +function stableState(before, after) { + return ( + before.dev === after.dev && + before.ino === after.ino && + before.size === after.size && + before.mtimeNs === after.mtimeNs && + before.ctimeNs === after.ctimeNs + ) +} + +async function realDirectory(path, label) { + const metadata = await lstat(path) + if (!metadata.isDirectory() || metadata.isSymbolicLink()) { + throw new Error(`SSH relay runtime manifest artifact ${label} must be a real directory`) + } + return resolve(path) +} + +async function readStableJson(path, label, signal) { + signal?.throwIfAborted() + const before = await lstat(path, { bigint: true }) + if ( + !before.isFile() || + before.isSymbolicLink() || + before.size <= 0n || + before.size > BigInt(MAX_RECEIPT_BYTES) + ) { + throw new Error(`SSH relay runtime manifest artifact ${label} must be bounded regular JSON`) + } + const bytes = await readFile(path, { signal }) + const after = await lstat(path, { bigint: true }) + if (!stableState(before, after)) { + throw new Error(`SSH relay runtime manifest artifact ${label} changed while reading`) + } + try { + return JSON.parse(bytes.toString('utf8')) + } catch (error) { + throw new Error( + `SSH relay runtime manifest artifact ${label} is invalid JSON: ${error.message}` + ) + } +} + +function normalizeFile(value, label) { + assertExactFields(value, FILE_FIELDS, label) + if ( + typeof value.name !== 'string' || + basename(value.name) !== value.name || + !SAFE_NAME.test(value.name) + ) { + throw new Error(`SSH relay runtime manifest artifact ${label} has an unsafe name`) + } + if (!DIGEST.test(value.sha256)) { + throw new Error(`SSH relay runtime manifest artifact ${label} has an invalid SHA-256`) + } + if (!Number.isSafeInteger(value.size) || value.size <= 0) { + throw new Error(`SSH relay runtime manifest artifact ${label} has an invalid size`) + } + return { name: value.name, sha256: value.sha256, size: value.size } +} + +function normalizeAggregateInput(value, expectedTupleId) { + assertExactFields(value, INPUT_FIELDS, `${expectedTupleId} aggregate input`) + if (value.tupleId !== expectedTupleId) { + throw new Error(`SSH relay runtime manifest artifact receipt tuple drifted: ${expectedTupleId}`) + } + return { + tupleId: expectedTupleId, + descriptor: normalizeFile(value.descriptor, `${expectedTupleId} descriptor`), + archive: normalizeFile(value.archive, `${expectedTupleId} archive`), + sbom: normalizeFile(value.sbom, `${expectedTupleId} SBOM`), + provenance: normalizeFile(value.provenance, `${expectedTupleId} provenance`) + } +} + +function receiptBinding(receipt, artifact) { + assertObject(receipt, `${artifact.tupleId} finalization receipt`) + if (receipt.tupleId !== artifact.tupleId) { + throw new Error( + `SSH relay runtime manifest artifact receipt tuple drifted: ${artifact.tupleId}` + ) + } + const contentId = artifact.layout === 'flat' ? receipt.contentId : receipt.finalContentId + if (!DIGEST.test(contentId ?? '')) { + throw new Error( + `SSH relay runtime manifest artifact receipt content drifted: ${artifact.tupleId}` + ) + } + return { + tupleId: artifact.tupleId, + contentId, + aggregateInput: normalizeAggregateInput(receipt.aggregateInput, artifact.tupleId) + } +} + +async function copyStableFile(source, destination, file, signal) { + signal?.throwIfAborted() + const before = await lstat(source, { bigint: true }) + if (!before.isFile() || before.isSymbolicLink()) { + throw new Error( + `SSH relay runtime manifest artifact input is not the declared regular file: ${file.name}` + ) + } + if (before.size !== BigInt(file.size)) { + throw new Error(`SSH relay runtime manifest artifact input size mismatch: ${file.name}`) + } + await copyFile(source, destination, constants.COPYFILE_EXCL) + const after = await lstat(source, { bigint: true }) + if (!stableState(before, after)) { + throw new Error(`SSH relay runtime manifest artifact input changed while copying: ${file.name}`) + } +} + +async function assertExactArtifactDirectories(root) { + const entries = await readdir(root, { withFileTypes: true }) + const actual = entries.map((entry) => entry.name).sort() + const expected = SSH_RELAY_RUNTIME_MANIFEST_ARTIFACTS.map( + ({ artifactName }) => artifactName + ).sort() + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + throw new Error( + 'SSH relay runtime manifest artifact root has missing or unexpected directories' + ) + } + for (const entry of entries) { + if (!entry.isDirectory() || entry.isSymbolicLink()) { + throw new Error(`SSH relay runtime manifest artifact is not a real directory: ${entry.name}`) + } + } +} + +export async function collectSshRelayRuntimeManifestArtifacts({ + artifactsDirectory, + stagingDirectory, + signal +}) { + const root = await realDirectory(resolve(artifactsDirectory), 'root') + const staging = await realDirectory(resolve(stagingDirectory), 'staging directory') + await assertExactArtifactDirectories(root) + const names = new Set() + const bindings = [] + for (const artifact of SSH_RELAY_RUNTIME_MANIFEST_ARTIFACTS) { + signal?.throwIfAborted() + const artifactRoot = await realDirectory( + join(root, artifact.artifactName), + artifact.artifactName + ) + const assetsRoot = + artifact.layout === 'flat' + ? artifactRoot + : await realDirectory(join(artifactRoot, 'assets'), `${artifact.tupleId} assets`) + const evidenceRoot = + artifact.layout === 'flat' + ? artifactRoot + : await realDirectory(join(artifactRoot, 'evidence'), `${artifact.tupleId} evidence`) + const suffix = artifact.layout === 'flat' ? 'linux-finalization' : 'finalization' + const receipt = await readStableJson( + join(evidenceRoot, `${artifact.tupleId}.${suffix}.json`), + `${artifact.tupleId} receipt`, + signal + ) + const binding = receiptBinding(receipt, artifact) + for (const file of [ + binding.aggregateInput.descriptor, + binding.aggregateInput.archive, + binding.aggregateInput.sbom, + binding.aggregateInput.provenance + ]) { + if (names.has(file.name)) { + throw new Error(`SSH relay runtime manifest artifact has a duplicate file: ${file.name}`) + } + names.add(file.name) + await copyStableFile(join(assetsRoot, file.name), join(staging, file.name), file, signal) + } + bindings.push(binding) + } + return bindings +} 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 8d895aaeafc..6fb74adf9c0 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 @@ -242,8 +242,11 @@ complete a box. signing remains blocked and no release, desktop, publication, or tuple consumer is connected. The missing-workflow/seed-signer RED is `E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-RED-001`; the seed signer is locally green under - `E-M4-PROTECTED-MANIFEST-SEED-LOCAL-001`, while the filesystem aggregate command and workflow - remain open. + `E-M4-PROTECTED-MANIFEST-SEED-LOCAL-001` and all-six exact-head CI is closed by + `E-M4-PROTECTED-MANIFEST-SEED-CI-001`. The filesystem prepare/finalize command is locally + green under `E-M4-MANIFEST-AGGREGATE-COMMAND-LOCAL-RED-001` and + `E-M4-MANIFEST-AGGREGATE-COMMAND-LOCAL-001`; exact-head CI and the callable workflow remain + 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 50e1eb39676..2986ee50794 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
      Last updated: 2026-07-15
      Current phase: Milestone 4 / Work Package 3 disconnected aggregate and protected manifest-signing workflow contract — **In progress — 2026-07-15, Codex implementation owner**. Linux aggregate-ready finalization is closed locally and on exact-head native x64/arm64 runners under E-M4-LINUX-FINALIZATION-LOCAL-RED-001, E-M4-LINUX-FINALIZATION-LOCAL-001, and E-M4-LINUX-FINALIZATION-CI-001. The active package must consume only the six exact aggregate-ready outputs, prepare canonical bytes credential-free, expose only those bytes to the tag-restricted `relay-runtime-manifest-signing` environment, accept only key ID plus Ed25519 signature, reconstruct and verify before final output, and fail closed on missing/extra/mutated inputs, approval/secret/signing failure, timeout, or partial output. It remains callable and disconnected. Release-cut, desktop builds, publication, and every tuple stay disconnected; production/default behavior is unchanged.
      -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact-head run 29419759256 at `91f59d19e5251fb7b6c9f6d397cd9cd82b43cbdf` passes all six native builds, both Linux userland supplements, and Windows x64 baseline. Windows arm64 fails only the retained build-floor gate after successful runtime verification (hosted 26200 versus required 26100). Downloaded artifacts 8344803641 and 8344824674 contain the expected Linux x64/arm64 descriptors and finalization receipts, whose tuple/content/archive/metadata bindings and smoke/resource metrics were inspected under E-M4-LINUX-FINALIZATION-CI-001. Golden E2E and replacement PR Checks pass. The protected seed signer is now locally green under E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-RED-001 and E-M4-PROTECTED-MANIFEST-SEED-LOCAL-001: canonical request serialization, exact base64 32-byte seed validation, signature-only output, drift/symlink rejection, and exclusive failure cleanup pass, and both native contract families execute the tests. The callable workflow remains intentionally absent/red while the filesystem prepare/finalize command is implemented next. No signing credential, aggregate workflow, 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.
      +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — protected seed-signer exact-head CI is closed by E-M4-PROTECTED-MANIFEST-SEED-CI-001 at pushed head `4180e04ac4e732f082dc576af69364325a678d2b`: all six native build jobs, PR Checks, and Golden E2E pass, with the artifact workflow red only for the retained Windows arm64 build-floor mismatch. The exclusive six-artifact filesystem command is locally green under E-M4-MANIFEST-AGGREGATE-COMMAND-LOCAL-RED-001 and E-M4-MANIFEST-AGGREGATE-COMMAND-LOCAL-001. Exact-head CI for that command is the next checkpoint, after which the still-red callable workflow may be implemented. No signing credential, aggregate workflow, 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) @@ -10776,6 +10776,96 @@ typecheck` passes in 1.88 seconds wall. Full `pnpm run lint` passes in 9.26 seco receipt/path/mutation/accepted-key/signature/partial-output failures, before adding the callable workflow. +### E-M4-MANIFEST-AGGREGATE-COMMAND-LOCAL-RED-001 — Six-artifact filesystem command is absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted purpose-named test atop pushed head + `4180e04ac4e732f082dc576af69364325a678d2b`; draft PR #8741. +- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. Generated local + fixtures only; no Actions runner, GitHub Environment, seed, release, desktop consumer, SSH host, + publication, or enabled tuple. +- Command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +config/scripts/ssh-relay-runtime-manifest-aggregate-command.test.mjs`. +- Result: expected FAIL, 1 suite / 0 tests collected in 225 ms. The only failure is module + resolution for absent `config/scripts/ssh-relay-runtime-manifest-aggregate-command.mjs`. +- Oracle proved: there is no file-based boundary that collects only the exact two flattened Linux + and four signed native artifacts, binds finalization receipts and accepted public keys, exposes + canonical bytes without credentials, independently recollects after signature return, verifies + the signature, and removes partial output on failure. The test defines missing/extra directory, + symlink, mutation, receipt drift, invalid-key, invalid-signature, and cleanup failures. +- Does not prove: implementation, real downloaded artifact inputs, Actions execution, protected + signing, publication, desktop embedding, SSH behavior, or an enabled tuple. +- Follow-up: implement the command and make this suite green before adding the callable workflow. + +### E-M4-PROTECTED-MANIFEST-SEED-CI-001 — Seed signer passes all six native contract families + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `4180e04ac4e732f082dc576af69364325a678d2b`; draft PR #8741. +- Workflow: [SSH Relay Runtime Artifacts run 29422587380](https://github.com/stablyai/orca/actions/runs/29422587380), + 14:13:18Z–14:27:59Z. The workflow conclusion is the expected failure solely because Windows + arm64 hosted build 26200 does not equal the separately required build 26100 after successful + runtime execution. +- Native jobs: Linux arm64 87376626186, Linux x64 87376626194, macOS x64 87376626222, Windows arm64 + 87376626264, macOS arm64 87376626297, and Windows x64 87376626317 all pass. Linux/macOS report 48 + files / 251 tests; Windows reports 49 files / 246 passed and nine skipped of 255. The delta from + the prior package is the protected seed-signing purpose suite under Node 24.18.0 on every family. +- Supplemental gates: Linux x64 87378044229, Linux arm64 87378044358, and Windows x64 87379238862 + pass. Windows arm64 87379238661 fails only the retained 26200-versus-26100 floor assertion. +- Adjacent regressions: [PR Checks 29422587487](https://github.com/stablyai/orca/actions/runs/29422587487) + job 87376597444 passes from 14:13:22Z–14:26:30Z. [Golden E2E 29422587419](https://github.com/stablyai/orca/actions/runs/29422587419) + passes macOS job 87376596910 and Linux job 87376596912. +- Oracle proved: canonical request serialization and seed-signing failure contracts execute under + repository Node 24 on all target-native runner families without regressing builds, packaged + smoke, full PR checks, or Golden E2E. +- Does not prove: a real protected environment/seed, approval, filesystem aggregate command CI, + callable aggregate workflow, native signing/trust, oldest Windows arm64/macOS/Linux kernel + floors, publication, desktop embedding, SSH behavior, or an enabled tuple. +- Follow-up: checkpoint the locally green filesystem aggregate command and require it on the same + six native families before implementing the callable workflow. + +### E-M4-MANIFEST-AGGREGATE-COMMAND-LOCAL-001 — Six-artifact prepare/finalize boundary passes locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: local implementation atop pushed head + `4180e04ac4e732f082dc576af69364325a678d2b`; draft PR #8741. +- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. Six generated + aggregate-ready fixture directories and an in-memory Ed25519 test seed only; no Actions runner, + GitHub Environment, production key, release, desktop consumer, SSH host, publication, or enabled + tuple. +- Focused command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +config/scripts/ssh-relay-runtime-manifest-aggregate-command.test.mjs` — PASS, 1 file / 4 tests in + 409 ms Vitest. A final cross-platform split rerun passes 1 file / 5 tests in 504 ms, preserving + the symlink case on POSIX while leaving all other hostile cases active on Windows. The suite covers + exact six-directory enumeration, flattened Linux versus signed + `assets/` + `evidence/` layouts, stable exclusive copies, finalization receipts, content and + descriptor bindings, duplicate names/tuples, missing/extra directories, symlinks, mutation, + invalid accepted keys, prepared drift, cancellation, invalid signatures, and failure cleanup. +- Broad release command: `rg --files config/scripts | rg +'/ssh-relay.*[.]test[.]mjs$' | rg -v +'ssh-relay-runtime-manifest-signing-workflow[.]test[.]mjs$' | xargs /usr/bin/time -l pnpm exec + vitest run --config config/vitest.config.ts --maxWorkers=1` — PASS, 50 files / 260 tests in 8.68 + seconds Vitest / 9.56 seconds wall, 189,104,128-byte maximum RSS, 96,130,448-byte peak footprint, + zero swaps. The preceding unfiltered 51-file run passes all implemented suites and fails only the + recorded absent-workflow RED; its pre-split count was 50 files / 259 tests. +- Desktop parity and static gates: manifest schema/signature/release-asset parity passes 3 files / 48 + tests; `pnpm run typecheck`, `node --check`, focused oxlint/formatting, and full `pnpm run lint` + pass. Full lint reports all 41 reliability gates, the 355-entry max-lines ratchet, bundled-skill, + localization catalog/parity, and localization coverage green; 26 warnings remain only in + untouched existing files. +- Oracle proved: accepted public keys are validated and hash-bound before a request is exposed. The + preparation phase copies and verifies only the four declared files from each exact finalization + receipt into exclusive staging. Finalization independently recollects all six inputs, compares + the complete prepared receipt/request, verifies the returned key/signature, emits the manifest + and evidence exclusively, and removes staging/partial output on every tested failure. +- Does not prove: actual downloaded native artifact directories, Node 24/native CI, a real protected + signer/environment/approval, the callable workflow, native signing/trust, publication, desktop + embedding, SSH behavior, or an enabled tuple. +- Follow-up: checkpoint and push this command, require it on all six exact-head native jobs, then + implement the still-red callable workflow without connecting release or product consumers. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -10834,16 +10924,14 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Checkpoint the locally proven protected seed signer and require its contract on all six native test -families. Then implement the exclusive filesystem prepare/finalize command that collects only the -six exact aggregate-ready artifacts, binds receipts and accepted public keys, serializes canonical -bytes without credentials, reconstructs after signature return, and removes partial output on every -failure. Only after that command is green may the still-red callable workflow isolate the base64 -32-byte seed in the tag-restricted `relay-runtime-manifest-signing` environment. The environment, -accepted production keys, and secret remain unprovisioned, so live signing evidence must stay -BLOCKED. Keep Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, -publication, and every tuple separately gated. Do not merge to `main`; retain every -production/default gate. +Checkpoint the locally proven filesystem prepare/finalize command and require its purpose suite on +all six exact-head native test families. After that CI evidence is green, implement the still-red +callable workflow so only canonical request bytes and the base64 32-byte seed enter the +tag-restricted `relay-runtime-manifest-signing` environment, while credential-free preparation and +final reconstruction independently download the six exact artifacts. The environment, accepted +production keys, and secret remain unprovisioned, so live signing evidence must stay BLOCKED. Keep +Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, +and every tuple separately gated. Do not merge to `main`; retain every production/default gate. 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 From 4979bc26c900b83b0f55bab6ebc2b540cde860fc Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 11:00:10 -0400 Subject: [PATCH 132/267] ci: add protected relay manifest workflow Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 5 + .../ssh-relay-runtime-manifest-signing.yml | 257 ++++++++++++++++++ ...runtime-manifest-signing-workflow.test.mjs | 106 ++++++++ ...elease-implementation-checklist-summary.md | 7 +- ...github-release-implementation-checklist.md | 81 +++++- 5 files changed, 447 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/ssh-relay-runtime-manifest-signing.yml create mode 100644 config/scripts/ssh-relay-runtime-manifest-signing-workflow.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 52e7101ccc0..e82c629d64b 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -171,6 +171,7 @@ jobs: node --check config/scripts/ssh-relay-runtime-manifest-artifact-collection.mjs node --check config/scripts/ssh-relay-runtime-manifest-aggregate-command.mjs node --check config/scripts/ssh-relay-runtime-manifest-aggregate-command.test.mjs + node --check config/scripts/ssh-relay-runtime-manifest-signing-workflow.test.mjs node --check config/scripts/ssh-relay-runtime-manifest-aggregate.mjs node --check config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs node --check config/scripts/ssh-relay-runtime-manifest-tuple.mjs @@ -219,6 +220,7 @@ jobs: config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs \ config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs \ config/scripts/ssh-relay-runtime-manifest-aggregate-command.test.mjs \ + config/scripts/ssh-relay-runtime-manifest-signing-workflow.test.mjs \ config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs \ config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs \ config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs \ @@ -595,6 +597,8 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime manifest aggregate command syntax check failed' } node --check config/scripts/ssh-relay-runtime-manifest-aggregate-command.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime manifest aggregate command test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-manifest-signing-workflow.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime manifest signing workflow test syntax check failed' } node --check config/scripts/ssh-relay-runtime-manifest-aggregate.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime manifest aggregate syntax check failed' } node --check config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs @@ -661,6 +665,7 @@ jobs: config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs ` config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs ` config/scripts/ssh-relay-runtime-manifest-aggregate-command.test.mjs ` + config/scripts/ssh-relay-runtime-manifest-signing-workflow.test.mjs ` config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs ` config/scripts/ssh-relay-runtime-manifest-tuple.test.mjs ` config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs ` diff --git a/.github/workflows/ssh-relay-runtime-manifest-signing.yml b/.github/workflows/ssh-relay-runtime-manifest-signing.yml new file mode 100644 index 00000000000..1e6fc5f292e --- /dev/null +++ b/.github/workflows/ssh-relay-runtime-manifest-signing.yml @@ -0,0 +1,257 @@ +name: SSH Relay Runtime Manifest Signing + +on: + workflow_call: + inputs: + source-sha: + required: true + type: string + release-tag: + required: true + type: string + created-at: + required: true + type: string + relay-protocol-version: + required: true + type: number + secrets: + SSH_RELAY_RUNTIME_MANIFEST_SEED: + required: true + +permissions: + actions: read + contents: read + +jobs: + prepare-manifest: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Checkout exact source revision + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + ref: ${{ inputs.source-sha }} + + - name: Setup pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6 + with: + run_install: false + + - name: Setup Node 24.18.0 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 24.18.0 + cache: pnpm + + - name: Install credential-free dependencies + run: pnpm install --frozen-lockfile --ignore-scripts + + - name: Download linux-x64-glibc artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ssh-relay-runtime-linux-x64-glibc + path: ${{ runner.temp }}/relay-manifest-inputs/ssh-relay-runtime-linux-x64-glibc + + - name: Download linux-arm64-glibc artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ssh-relay-runtime-linux-arm64-glibc + path: ${{ runner.temp }}/relay-manifest-inputs/ssh-relay-runtime-linux-arm64-glibc + + - name: Download signed darwin-x64 artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ssh-relay-runtime-signed-darwin-x64 + path: ${{ runner.temp }}/relay-manifest-inputs/ssh-relay-runtime-signed-darwin-x64 + + - name: Download signed darwin-arm64 artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ssh-relay-runtime-signed-darwin-arm64 + path: ${{ runner.temp }}/relay-manifest-inputs/ssh-relay-runtime-signed-darwin-arm64 + + - name: Download signed win32-x64 artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ssh-relay-runtime-signed-win32-x64 + path: ${{ runner.temp }}/relay-manifest-inputs/ssh-relay-runtime-signed-win32-x64 + + - name: Download signed win32-arm64 artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ssh-relay-runtime-signed-win32-arm64 + path: ${{ runner.temp }}/relay-manifest-inputs/ssh-relay-runtime-signed-win32-arm64 + + - name: Prepare canonical manifest request + run: | + set -euo pipefail + # Why: missing reviewed production keys must stop before signer bytes are exposed. + node config/scripts/ssh-relay-runtime-manifest-aggregate-command.mjs prepare \ + --artifacts-directory '${{ runner.temp }}/relay-manifest-inputs' \ + --accepted-keys config/ssh-relay-runtime-manifest-accepted-keys.json \ + --output-directory '${{ runner.temp }}/relay-manifest-prepared' \ + --source-sha '${{ inputs.source-sha }}' \ + --release-tag '${{ inputs.release-tag }}' \ + --created-at '${{ inputs.created-at }}' \ + --relay-protocol-version '${{ inputs.relay-protocol-version }}' + + - name: Upload canonical signing request + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: ssh-relay-runtime-manifest-signing-request + path: ${{ runner.temp }}/relay-manifest-prepared/signing-request.json + if-no-files-found: error + retention-days: 1 + + - name: Upload credential-free prepared receipt + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: ssh-relay-runtime-manifest-prepared + path: ${{ runner.temp }}/relay-manifest-prepared/prepared-aggregate.json + if-no-files-found: error + retention-days: 1 + + sign-manifest: + needs: prepare-manifest + runs-on: ubuntu-24.04 + timeout-minutes: 5 + environment: relay-runtime-manifest-signing + steps: + - name: Checkout exact source revision + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + ref: ${{ inputs.source-sha }} + + - name: Setup pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6 + with: + run_install: false + + - name: Setup Node 24.18.0 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 24.18.0 + cache: pnpm + + - name: Install signer dependencies + run: pnpm install --frozen-lockfile --ignore-scripts + + - name: Download canonical signing request only + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ssh-relay-runtime-manifest-signing-request + path: ${{ runner.temp }}/relay-manifest-signing-request + + - name: Sign exact canonical bytes + env: + SSH_RELAY_RUNTIME_MANIFEST_SEED: ${{ secrets.SSH_RELAY_RUNTIME_MANIFEST_SEED }} + run: | + set -euo pipefail + node config/scripts/ssh-relay-runtime-manifest-seed-signing.mjs \ + --request '${{ runner.temp }}/relay-manifest-signing-request/signing-request.json' \ + --output-directory '${{ runner.temp }}/relay-manifest-signing-result' + + - name: Upload signature-only result + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: ssh-relay-runtime-manifest-signing-result + path: ${{ runner.temp }}/relay-manifest-signing-result/signing-result.json + if-no-files-found: error + retention-days: 1 + + finalize-manifest: + needs: [prepare-manifest, sign-manifest] + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Checkout exact source revision + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + ref: ${{ inputs.source-sha }} + + - name: Setup pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6 + with: + run_install: false + + - name: Setup Node 24.18.0 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: 24.18.0 + cache: pnpm + + - name: Install credential-free dependencies + run: pnpm install --frozen-lockfile --ignore-scripts + + - name: Download linux-x64-glibc artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ssh-relay-runtime-linux-x64-glibc + path: ${{ runner.temp }}/relay-manifest-inputs/ssh-relay-runtime-linux-x64-glibc + + - name: Download linux-arm64-glibc artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ssh-relay-runtime-linux-arm64-glibc + path: ${{ runner.temp }}/relay-manifest-inputs/ssh-relay-runtime-linux-arm64-glibc + + - name: Download signed darwin-x64 artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ssh-relay-runtime-signed-darwin-x64 + path: ${{ runner.temp }}/relay-manifest-inputs/ssh-relay-runtime-signed-darwin-x64 + + - name: Download signed darwin-arm64 artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ssh-relay-runtime-signed-darwin-arm64 + path: ${{ runner.temp }}/relay-manifest-inputs/ssh-relay-runtime-signed-darwin-arm64 + + - name: Download signed win32-x64 artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ssh-relay-runtime-signed-win32-x64 + path: ${{ runner.temp }}/relay-manifest-inputs/ssh-relay-runtime-signed-win32-x64 + + - name: Download signed win32-arm64 artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ssh-relay-runtime-signed-win32-arm64 + path: ${{ runner.temp }}/relay-manifest-inputs/ssh-relay-runtime-signed-win32-arm64 + + - name: Download credential-free prepared receipt + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ssh-relay-runtime-manifest-prepared + path: ${{ runner.temp }}/relay-manifest-prepared + + - name: Download signature-only result + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ssh-relay-runtime-manifest-signing-result + path: ${{ runner.temp }}/relay-manifest-signing-result + + - name: Independently reconstruct and verify final manifest + run: | + set -euo pipefail + node config/scripts/ssh-relay-runtime-manifest-aggregate-command.mjs finalize \ + --artifacts-directory '${{ runner.temp }}/relay-manifest-inputs' \ + --accepted-keys config/ssh-relay-runtime-manifest-accepted-keys.json \ + --prepared-directory '${{ runner.temp }}/relay-manifest-prepared' \ + --signing-result '${{ runner.temp }}/relay-manifest-signing-result/signing-result.json' \ + --output-directory '${{ runner.temp }}/relay-manifest-final' \ + --source-sha '${{ inputs.source-sha }}' \ + --release-tag '${{ inputs.release-tag }}' \ + --created-at '${{ inputs.created-at }}' \ + --relay-protocol-version '${{ inputs.relay-protocol-version }}' + + - name: Upload verified manifest output + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: ssh-relay-runtime-signed-manifest + path: | + ${{ runner.temp }}/relay-manifest-final/assets/ + ${{ runner.temp }}/relay-manifest-final/evidence/ + if-no-files-found: error + retention-days: 7 diff --git a/config/scripts/ssh-relay-runtime-manifest-signing-workflow.test.mjs b/config/scripts/ssh-relay-runtime-manifest-signing-workflow.test.mjs new file mode 100644 index 00000000000..6d2431a1cbd --- /dev/null +++ b/config/scripts/ssh-relay-runtime-manifest-signing-workflow.test.mjs @@ -0,0 +1,106 @@ +import { access, readFile } from 'node:fs/promises' + +import { parse } from 'yaml' +import { describe, expect, it } from 'vitest' + +const workflowUrl = new URL( + '../../.github/workflows/ssh-relay-runtime-manifest-signing.yml', + import.meta.url +) +const releaseCutUrl = new URL('../../.github/workflows/release-cut.yml', import.meta.url) +const releaseMacUrl = new URL('../../.github/workflows/release-mac-build.yml', import.meta.url) +const rehearsalUrl = new URL( + '../../.github/workflows/ssh-relay-runtime-native-signing-rehearsal.yml', + import.meta.url +) +const acceptedKeysUrl = new URL('../ssh-relay-runtime-manifest-accepted-keys.json', import.meta.url) + +const EXPECTED_INPUTS = { + 'source-sha': { required: true, type: 'string' }, + 'release-tag': { required: true, type: 'string' }, + 'created-at': { required: true, type: 'string' }, + 'relay-protocol-version': { required: true, type: 'number' } +} +const EXPECTED_ARTIFACTS = [ + 'ssh-relay-runtime-linux-x64-glibc', + 'ssh-relay-runtime-linux-arm64-glibc', + 'ssh-relay-runtime-signed-darwin-x64', + 'ssh-relay-runtime-signed-darwin-arm64', + 'ssh-relay-runtime-signed-win32-x64', + 'ssh-relay-runtime-signed-win32-arm64' +] + +function actionRefs(source) { + return [...source.matchAll(/^\s*uses:\s+[^\s#]+@([^\s#]+)/gmu)].map((match) => match[1]) +} + +describe('SSH relay runtime protected manifest-signing workflow', () => { + it('isolates the seed between credential-free reconstruction stages and stays disconnected', async () => { + const [source, releaseCut, releaseMac, rehearsal] = await Promise.all([ + readFile(workflowUrl, 'utf8'), + readFile(releaseCutUrl, 'utf8'), + readFile(releaseMacUrl, 'utf8'), + readFile(rehearsalUrl, 'utf8') + ]) + const workflow = parse(source) + + expect(Object.keys(workflow.on)).toEqual(['workflow_call']) + expect(workflow.on.workflow_call.inputs).toEqual(EXPECTED_INPUTS) + expect(workflow.on.workflow_call.secrets).toEqual({ + SSH_RELAY_RUNTIME_MANIFEST_SEED: { required: true } + }) + expect(workflow.permissions).toEqual({ actions: 'read', contents: 'read' }) + expect(Object.keys(workflow.jobs)).toEqual([ + 'prepare-manifest', + 'sign-manifest', + 'finalize-manifest' + ]) + + const prepare = workflow.jobs['prepare-manifest'] + const sign = workflow.jobs['sign-manifest'] + const finalize = workflow.jobs['finalize-manifest'] + expect(prepare).toMatchObject({ 'runs-on': 'ubuntu-24.04', 'timeout-minutes': 15 }) + expect(sign).toMatchObject({ + needs: 'prepare-manifest', + 'runs-on': 'ubuntu-24.04', + 'timeout-minutes': 5, + environment: 'relay-runtime-manifest-signing' + }) + expect(finalize).toMatchObject({ + needs: ['prepare-manifest', 'sign-manifest'], + 'runs-on': 'ubuntu-24.04', + 'timeout-minutes': 15 + }) + + for (const artifact of EXPECTED_ARTIFACTS) { + expect(source.match(new RegExp(`name: ${artifact}$`, 'gmu'))).toHaveLength(2) + expect(JSON.stringify(sign)).not.toContain(artifact) + } + expect(source).not.toContain('pattern: ssh-relay-runtime-') + expect(source).not.toContain('merge-multiple: true') + expect(source.match(/ssh-relay-runtime-manifest-aggregate-command\.mjs prepare/g)).toHaveLength( + 1 + ) + expect(source.match(/ssh-relay-runtime-manifest-seed-signing\.mjs/g)).toHaveLength(1) + expect( + source.match(/ssh-relay-runtime-manifest-aggregate-command\.mjs finalize/g) + ).toHaveLength(1) + + const seedReferences = source.match(/secrets\.SSH_RELAY_RUNTIME_MANIFEST_SEED/g) ?? [] + expect(seedReferences).toHaveLength(1) + expect(JSON.stringify(sign)).toContain('secrets.SSH_RELAY_RUNTIME_MANIFEST_SEED') + expect(JSON.stringify(prepare)).not.toContain('secrets.') + expect(JSON.stringify(finalize)).not.toContain('secrets.') + expect(actionRefs(source).every((ref) => /^[0-9a-f]{40}$/u.test(ref))).toBe(true) + expect(source).not.toContain('continue-on-error') + expect(source).not.toMatch(/contents:\s*write|gh\s+release|upload-release-asset/u) + expect(source.match(/config\/ssh-relay-runtime-manifest-accepted-keys\.json/g)).toHaveLength(2) + // Why: a placeholder key would make an unreviewed trust root look provisioned. + await expect(access(acceptedKeysUrl)).rejects.toMatchObject({ code: 'ENOENT' }) + + // Why: this capability must remain inert until release/publication and desktop gates are reviewed. + for (const consumer of [releaseCut, releaseMac, rehearsal]) { + expect(consumer).not.toContain('ssh-relay-runtime-manifest-signing.yml') + } + }) +}) 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 6fb74adf9c0..7f49e6744df 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 @@ -245,8 +245,11 @@ complete a box. `E-M4-PROTECTED-MANIFEST-SEED-LOCAL-001` and all-six exact-head CI is closed by `E-M4-PROTECTED-MANIFEST-SEED-CI-001`. The filesystem prepare/finalize command is locally green under `E-M4-MANIFEST-AGGREGATE-COMMAND-LOCAL-RED-001` and - `E-M4-MANIFEST-AGGREGATE-COMMAND-LOCAL-001`; exact-head CI and the callable workflow remain - open. + `E-M4-MANIFEST-AGGREGATE-COMMAND-LOCAL-001`, with all-six exact-head CI closed by + `E-M4-MANIFEST-AGGREGATE-COMMAND-CI-001`. The callable workflow remains open. + Its disconnected three-job source contract is locally green under + `E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-001`, but exact-head CI and live protected signing + remain open; no accepted production key, environment, or seed is provisioned. - [ ] 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 2986ee50794..338749d382b 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
      Last updated: 2026-07-15
      Current phase: Milestone 4 / Work Package 3 disconnected aggregate and protected manifest-signing workflow contract — **In progress — 2026-07-15, Codex implementation owner**. Linux aggregate-ready finalization is closed locally and on exact-head native x64/arm64 runners under E-M4-LINUX-FINALIZATION-LOCAL-RED-001, E-M4-LINUX-FINALIZATION-LOCAL-001, and E-M4-LINUX-FINALIZATION-CI-001. The active package must consume only the six exact aggregate-ready outputs, prepare canonical bytes credential-free, expose only those bytes to the tag-restricted `relay-runtime-manifest-signing` environment, accept only key ID plus Ed25519 signature, reconstruct and verify before final output, and fail closed on missing/extra/mutated inputs, approval/secret/signing failure, timeout, or partial output. It remains callable and disconnected. 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** — protected seed-signer exact-head CI is closed by E-M4-PROTECTED-MANIFEST-SEED-CI-001 at pushed head `4180e04ac4e732f082dc576af69364325a678d2b`: all six native build jobs, PR Checks, and Golden E2E pass, with the artifact workflow red only for the retained Windows arm64 build-floor mismatch. The exclusive six-artifact filesystem command is locally green under E-M4-MANIFEST-AGGREGATE-COMMAND-LOCAL-RED-001 and E-M4-MANIFEST-AGGREGATE-COMMAND-LOCAL-001. Exact-head CI for that command is the next checkpoint, after which the still-red callable workflow may be implemented. No signing credential, aggregate workflow, 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.
      +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — the exclusive six-artifact filesystem command is closed locally and on all six exact-head native families under E-M4-MANIFEST-AGGREGATE-COMMAND-LOCAL-RED-001, E-M4-MANIFEST-AGGREGATE-COMMAND-LOCAL-001, and E-M4-MANIFEST-AGGREGATE-COMMAND-CI-001 at pushed head `8cf80a0228ffa095beceec018a0de2e237b89e2d`. The callable three-job workflow is locally green under E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-001 and is the active exact-head CI checkpoint. It stays disconnected and deliberately fails closed before signer exposure because no accepted production key exists. No signing credential, protected environment, 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) @@ -10866,6 +10866,73 @@ config/scripts/ssh-relay-runtime-manifest-aggregate-command.test.mjs` — PASS, - Follow-up: checkpoint and push this command, require it on all six exact-head native jobs, then implement the still-red callable workflow without connecting release or product consumers. +### E-M4-MANIFEST-AGGREGATE-COMMAND-CI-001 — Filesystem aggregation passes all native families + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `8cf80a0228ffa095beceec018a0de2e237b89e2d`; draft PR #8741. +- Workflow: [SSH Relay Runtime Artifacts run 29424084956](https://github.com/stablyai/orca/actions/runs/29424084956), + 14:33:33Z–14:53:05Z. The expected workflow failure is solely Windows arm64 baseline job + 87385641582: runtime verification passes in 8,318.1202 ms with 6,107.7828 ms smoke and + 49,930,240-byte RSS, then the exact floor gate rejects observed build 26200 versus required 26100. +- Native jobs: macOS x64 87381843694, Linux arm64 87381843705, macOS arm64 87381843796, Linux x64 + 87381843825, Windows x64 87381843853, and Windows arm64 87381844294 all pass complete artifact + construction. POSIX reports 49 files / 256 tests. Windows reports 50 files / 250 passed and ten + skipped of 260; only the POSIX-only symlink case is skipped by the new command suite on Windows. +- Supplemental jobs: Linux arm64 87383766220, Linux x64 87383766265, and Windows x64 87385641378 + pass. The sole Windows arm64 failure is the retained exact-build mismatch described above. +- Adjacent regressions: [PR Checks 29424084938](https://github.com/stablyai/orca/actions/runs/29424084938) + job 87381832092 passes 14:33:36Z–14:48:09Z. [Golden E2E 29424085004](https://github.com/stablyai/orca/actions/runs/29424085004) + passes macOS job 87381832460 and Linux job 87381832629. +- Oracle proved: every target-native Node 24 family executes the exact six-artifact collection, + accepted-key, canonical request, independent reconstruction, signature verification, hostile + input, cancellation, and cleanup suite without regressing runtime construction or adjacent CI. +- Does not prove: real downloaded signed macOS/Windows outputs, a provisioned accepted production + key, protected environment/seed/reviewer approval, live workflow execution, native trust, exact + oldest floors, publication, desktop embedding, SSH behavior, or an enabled tuple. +- Follow-up: implement the callable three-job workflow contract. Keep it disconnected and fail + closed while accepted production keys, the environment, and seed remain unprovisioned. + +### E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-001 — Disconnected protected workflow contract passes locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: local implementation atop pushed head + `8cf80a0228ffa095beceec018a0de2e237b89e2d`; draft PR #8741. +- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. Static workflow + parsing only; no Actions runner, artifact download, accepted production key, GitHub Environment, + seed, approval, release, desktop consumer, SSH host, publication, or enabled tuple. +- Focused command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +config/scripts/ssh-relay-runtime-manifest-signing-workflow.test.mjs` — PASS, 1 file / 1 test in + 159 ms on the final contract/static-gate rerun (the first green implementation run passed in 169 + ms). +- Broad release command: `/usr/bin/time -l sh -c "rg --files config/scripts | rg +'/ssh-relay.*[.]test[.]mjs$' | xargs pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1"` — PASS, 51 files / 261 tests in 10.23 seconds Vitest / 11.41 seconds wall, + 188,825,600-byte maximum RSS, zero swaps. +- Desktop parity/static results: manifest schema/signature/release-asset parity passes 3 files / 48 + tests; typecheck, `node --check`, and workflow/source formatting pass. Full lint passes all 41 + reliability gates, the 355-entry max-lines ratchet, bundled-skill, and localization gates; all 26 + warnings remain in untouched existing files. +- Oracle proved: the reusable interface has only the exact source SHA, tag, timestamp, and protocol + inputs plus one required seed secret. Preparation and finalization are credential-free, download + six exact artifacts separately, and use 15-minute bounds. The sole five-minute protected job sees + no runtime artifacts, references the seed exactly once, and returns only key ID/signature. Final + reconstruction independently recollects all six inputs. Actions are SHA-pinned, permissions are + read-only, no wildcard merge/continue-on-error/write publication exists, and no release, + desktop, or rehearsal workflow calls it. +- Fail-closed provisioning gate: both credential-free stages reference the absent + `config/ssh-relay-runtime-manifest-accepted-keys.json`; the purpose test requires that absence so + no placeholder trust root can look provisioned. Consequently the workflow cannot expose a + signing request or reach the protected environment until a separately reviewed accepted key is + committed. The environment/seed/reviewer restrictions remain external and unprovisioned. +- Does not prove: GitHub accepts/executes the YAML, actual artifact downloads, accepted-key + provisioning, environment/tag policy, secret isolation at runtime, approval denial/timeout, + signature generation, final output, native trust, publication, desktop embedding, SSH behavior, + or an enabled tuple. +- Follow-up: checkpoint and push the workflow contract, require exact-head PR/native CI, and keep + live signing BLOCKED until accepted keys and the protected environment receive separate review. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -10924,12 +10991,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Checkpoint the locally proven filesystem prepare/finalize command and require its purpose suite on -all six exact-head native test families. After that CI evidence is green, implement the still-red -callable workflow so only canonical request bytes and the base64 32-byte seed enter the -tag-restricted `relay-runtime-manifest-signing` environment, while credential-free preparation and -final reconstruction independently download the six exact artifacts. The environment, accepted -production keys, and secret remain unprovisioned, so live signing evidence must stay BLOCKED. Keep +Checkpoint and push the locally green callable workflow, then require its purpose contract on all +six exact-head native test families plus PR Checks and Golden E2E. Only canonical request bytes and +the base64 32-byte seed may enter the tag-restricted `relay-runtime-manifest-signing` environment, +while credential-free preparation and final reconstruction independently download the six exact +artifacts. The environment, accepted production keys, and secret remain unprovisioned, so live +signing evidence must stay BLOCKED. Keep Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. Do not merge to `main`; retain every production/default gate. From c9ffa26a90751fb0941e44066c36722d9c6ee49d Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 11:26:11 -0400 Subject: [PATCH 133/267] ci: bound Linux relay runtime prerequisites Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 22 ++- .../ssh-relay-runtime-workflow.test.mjs | 15 ++ ...elease-implementation-checklist-summary.md | 11 ++ ...github-release-implementation-checklist.md | 140 +++++++++++++++++- ...6-07-14-ssh-relay-github-release-plan.html | 4 +- 5 files changed, 182 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index e82c629d64b..afe4e77fd87 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -93,8 +93,26 @@ jobs: shell: bash run: | set -euo pipefail - sudo apt-get update -qq - sudo apt-get install -y -qq build-essential ca-certificates curl gnupg gpgv python3 xz-utils + # Why: hosted ARM runners repeatedly lost port-80 mirror access; bounded HTTPS keeps failures explicit. + for source_file in /etc/apt/sources.list /etc/apt/sources.list.d/*.sources; do + [ -f "$source_file" ] || continue + sudo sed -i \ + -e 's|http://ports.ubuntu.com|https://ports.ubuntu.com|g' \ + -e 's|http://archive.ubuntu.com|https://archive.ubuntu.com|g' \ + -e 's|http://security.ubuntu.com|https://security.ubuntu.com|g' \ + -e 's|http://azure.archive.ubuntu.com|https://azure.archive.ubuntu.com|g' \ + "$source_file" + done + apt_options=( + -o Acquire::Retries=3 + -o Acquire::http::Timeout=15 + -o Acquire::https::Timeout=15 + -o Acquire::ForceIPv4=true + -o DPkg::Lock::Timeout=60 + ) + timeout --signal=TERM --kill-after=15s 180s sudo apt-get "${apt_options[@]}" update -qq + timeout --signal=TERM --kill-after=15s 180s sudo apt-get "${apt_options[@]}" install -y -qq \ + build-essential ca-certificates curl gnupg gpgv python3 xz-utils - name: Install macOS signature verifier if: runner.os == 'macOS' diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index b6c680e47e7..64306802340 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -51,6 +51,21 @@ describe('SSH relay runtime artifact workflow', () => { } }) + it('bounds hosted Linux prerequisite acquisition over HTTPS', async () => { + const workflow = parse(await readFile(workflowUrl, 'utf8')) + const installStep = workflow.jobs['build-posix-runtime'].steps.find( + (step) => step.name === 'Install Linux build and verification tools' + ) + + expect(installStep.run).toContain("'s|http://ports.ubuntu.com|https://ports.ubuntu.com|g'") + expect(installStep.run).toContain('Acquire::Retries=3') + expect(installStep.run).toContain('Acquire::https::Timeout=15') + expect(installStep.run).toContain('Acquire::ForceIPv4=true') + expect( + installStep.run.match(/timeout --signal=TERM --kill-after=15s 180s sudo apt-get/g) + ).toHaveLength(2) + }) + it('uploads only the first output after two clean builds verify and compare', async () => { const source = await readFile(workflowUrl, 'utf8') const workflow = parse(source) 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 7f49e6744df..cff55dfa36b 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 @@ -9,6 +9,17 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. +Active checkpoint: **Milestone 4 / Work Package 3 Linux prerequisite transport correction — in +progress, 2026-07-15, Codex implementation owner.** The disconnected protected-manifest workflow is +locally green at `4979bc26c`. Exact-head artifact run +[29426142423](https://github.com/stablyai/orca/actions/runs/29426142423) and its isolated attempt 2 +both prove Ubuntu 24.04 ARM port-80 mirror failure before dependencies or tests. The narrow active +package now passes locally: hosted Linux prerequisite acquisition moves to HTTPS with bounded +connection timeouts, retries, lock wait, and TERM-to-KILL settlement; exhaustion still fails the +native build. The correction is ready to checkpoint and push for fresh all-six native proof. Golden +E2E and PR Checks are green on the prior exact head. Production keys/environment/seed, publication, +desktop/default consumers, tuple enablement, and merge to `main` remain disconnected. + ## Safety status - [x] Existing SSH relay installation remains the default for every target. 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 338749d382b..a6a1ac4febc 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
      Last updated: 2026-07-15
      Current phase: Milestone 4 / Work Package 3 disconnected aggregate and protected manifest-signing workflow contract — **In progress — 2026-07-15, Codex implementation owner**. Linux aggregate-ready finalization is closed locally and on exact-head native x64/arm64 runners under E-M4-LINUX-FINALIZATION-LOCAL-RED-001, E-M4-LINUX-FINALIZATION-LOCAL-001, and E-M4-LINUX-FINALIZATION-CI-001. The active package must consume only the six exact aggregate-ready outputs, prepare canonical bytes credential-free, expose only those bytes to the tag-restricted `relay-runtime-manifest-signing` environment, accept only key ID plus Ed25519 signature, reconstruct and verify before final output, and fail closed on missing/extra/mutated inputs, approval/secret/signing failure, timeout, or partial output. It remains callable and disconnected. Release-cut, desktop builds, publication, and every tuple stay disconnected; production/default behavior is unchanged.
      -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — the exclusive six-artifact filesystem command is closed locally and on all six exact-head native families under E-M4-MANIFEST-AGGREGATE-COMMAND-LOCAL-RED-001, E-M4-MANIFEST-AGGREGATE-COMMAND-LOCAL-001, and E-M4-MANIFEST-AGGREGATE-COMMAND-CI-001 at pushed head `8cf80a0228ffa095beceec018a0de2e237b89e2d`. The callable three-job workflow is locally green under E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-001 and is the active exact-head CI checkpoint. It stays disconnected and deliberately fails closed before signer exposure because no accepted production key exists. No signing credential, protected environment, 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.
      +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact-head artifact run 29426142423 and isolated attempt 2 reproduce the same Ubuntu 24.04 ARM port-80 mirror failure before source dependencies or implementation tests under E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-RED-001 and E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-RED-002. The evidence-driven correction is locally green under E-M4-LINUX-PREREQUISITE-TRANSPORT-LOCAL-RED-001 and E-M4-LINUX-PREREQUISITE-TRANSPORT-LOCAL-001: hosted Linux prerequisite acquisition moves to HTTPS with bounded connection timeouts, retries, lock wait, and TERM-to-KILL command settlement; exhaustion still fails the native build. Focused and broad release contracts, desktop parity, typecheck, full lint, formatting, shell syntax, and diff checks pass. Checkpoint and push the correction, then require a fresh exact-head all-six run. Golden E2E and PR Checks are green on the prior exact head. The user authorizes commits, pushes, draft-PR updates, CI reruns, and protected rehearsals within PRs, but not merging to `main`. The callable protected workflow remains disconnected and deliberately fails closed before signer exposure because no accepted production key exists. No signing credential, protected environment, release consumer, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
      Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
      Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -10933,6 +10933,131 @@ config/scripts/ssh-relay-runtime-manifest-signing-workflow.test.mjs` — PASS, 1 - Follow-up: checkpoint and push the workflow contract, require exact-head PR/native CI, and keep live signing BLOCKED until accepted keys and the protected environment receive separate review. +### E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-RED-001 — Ubuntu ARM dependency mirror is unreachable + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `4979bc26c900b83b0f55bab6ebc2b540cde860fc`; draft PR #8741. +- Workflow/job: [SSH Relay Runtime Artifacts run 29426142423](https://github.com/stablyai/orca/actions/runs/29426142423), + first Linux arm64 job + [87388934005](https://github.com/stablyai/orca/actions/runs/29426142423/job/87388934005), + 15:00:30Z–15:02:29Z. +- Runner/remote/network: GitHub-hosted `ubuntu-24.04-arm` image `20260706.52.2`, runner 2.335.1, + image provisioner `20260624.560`. This is a native arm64 artifact-build runner with public package + egress; no SSH remote, protected environment, signing seed, release, desktop consumer, + publication, or enabled tuple. +- Evidence command: `gh api repos/stablyai/orca/actions/jobs/87388934005/logs | rg -n -C 2 +"Runner Image|Image:|Architecture|System:|ports\\.ubuntu\\.com|Failed to fetch|Unable to +fetch|exit code|Process completed"`. +- Result: expected infrastructure RED. `apt-get update` could not reach IPv6 or IPv4 endpoints for + `ports.ubuntu.com`; the later `build-essential_12.10ubuntu1_arm64.deb` fetch timed out and package + installation exited 100. Checkout, pnpm setup, Node 24.18.0 setup, and runner identity passed. + Source dependency installation and the runtime artifact contract tests were never reached, so no + implementation assertion failed and no test count or runtime metric exists for this attempt. +- Oracle proved: the job fails before producing evidence when its package mirror is unavailable; + this attempt cannot satisfy the Linux arm64 exact-head cell and must not be treated as a product + regression or erased by a replacement run. +- Does not prove: the protected-workflow contract on Linux arm64, artifact construction, baseline + smoke, signing, publication, desktop embedding, SSH behavior, or an enabled tuple. +- Follow-up: rerun only failed job 87388934005, retain this RED alongside the replacement job ID and + result, then collect exact purpose-suite counts for every native family plus PR Checks and Golden + E2E before closing E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001. + +### E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-RED-002 — Isolated Ubuntu ARM rerun reproduces port-80 failure + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: unchanged exact pushed head `4979bc26c900b83b0f55bab6ebc2b540cde860fc`; draft PR + #8741. +- Workflow/job: isolated attempt 2 of + [SSH Relay Runtime Artifacts run 29426142423](https://github.com/stablyai/orca/actions/runs/29426142423), + Linux arm64 replacement job + [87392977597](https://github.com/stablyai/orca/actions/runs/29426142423/job/87392977597), + 15:16:05Z–15:20:13Z. Command: `gh run rerun --repo stablyai/orca --job 87388934005`. +- Runner/remote/network: GitHub-hosted `ubuntu-24.04-arm` image `20260706.52.2`, runner 2.335.1, + image provisioner `20260624.560`; the same image revision as the first attempt. Public package + egress only; no SSH remote, protected environment, signing seed, release, desktop consumer, + publication, or enabled tuple. +- Log command: `gh api repos/stablyai/orca/actions/jobs/87392977597/logs | rg -n -C 2 +"Runner Image|Image:|Version:|ports\\.ubuntu\\.com|Failed to fetch|Unable to fetch|exit +code|Process completed"`. +- Result: infrastructure FAIL after 4 minutes 8 seconds. Ubuntu index acquisition spent about 150 + seconds failing `http://ports.ubuntu.com:80`; the subsequent `build-essential` fetch also timed + out and `apt-get` exited 100. As in attempt 1, checkout, pnpm, Node 24.18.0, and runner identity + passed, while source dependencies and every implementation test were never reached. +- Oracle proved: a same-source isolated rerun does not recover the hosted ARM port-80 path. The + workflow currently lacks an authenticated mirror transport plus explicit per-request retry and + connection-time bounds; relying only on the 20-minute job timeout wastes runner time and leaves + the native proof cell empty. +- Does not prove: that HTTPS is reachable from the hosted runner, the bounded correction, runtime + contracts on Linux arm64, native build output, signing, publication, desktop embedding, SSH + behavior, or an enabled tuple. +- Follow-up: add a purpose assertion that fails against the current workflow, then use HTTPS, + bounded APT retries/connect timeouts, IPv4 selection, and bounded command settlement for hosted + Linux prerequisite acquisition. Require local focused/broad proof and a fresh exact-head native + run; keep both infrastructure RED attempts in the ledger. + +### E-M4-LINUX-PREREQUISITE-TRANSPORT-LOCAL-RED-001 — Hosted Linux package acquisition is unbounded HTTP + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted purpose assertion atop exact pushed head + `4979bc26c900b83b0f55bab6ebc2b540cde860fc`; draft PR #8741. +- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. Static workflow YAML + parsing only; no APT request, Actions runner, SSH remote, protected environment, release, + 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-workflow.test.mjs`. +- Result: expected FAIL, 1 file / 1 failed and 6 passed of 7 tests in 180 ms Vitest / 0.86 seconds + wall, 131,989,504-byte maximum RSS, 96,196,008-byte peak footprint, zero swaps. The purpose + assertion receives only raw `sudo apt-get update` and `install` commands: no HTTPS mirror rewrite, + retry, per-request connection timeout, IPv4 selection, lock bound, or command settlement bound. +- Oracle proved: the workflow contract permits the exact unbounded port-80 behavior reproduced by + both native ARM attempts; the 20-minute job timeout is the only surrounding bound. +- Does not prove: a correction, runner HTTPS reachability, APT execution, runtime contracts, native + build output, signing, publication, desktop embedding, SSH behavior, or an enabled tuple. +- Follow-up: implement the narrowly planned HTTPS and bounded APT acquisition without changing any + runtime bytes or product/default behavior. + +### E-M4-LINUX-PREREQUISITE-TRANSPORT-LOCAL-001 — HTTPS and bounded APT contract passes locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: local correction atop exact pushed head + `4979bc26c900b83b0f55bab6ebc2b540cde860fc`; draft PR #8741. +- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. Static YAML and shell + syntax validation only; no APT request or Actions runner. No SSH remote, signing credential, + protected environment, release, publication, desktop consumer, or enabled tuple. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 1 file / 7 tests in 165 + ms Vitest / 0.82 seconds wall, 131,923,968-byte maximum RSS, 96,114,064-byte peak footprint, zero + swaps. The first post-implementation run failed only because the new test expected a literal + `/ubuntu-ports` suffix and prohibited the HTTP source string needed by the `sed` rewrite; the + corrected oracle asserts the exact HTTP-to-HTTPS transformation instead. +- Broad release command: `/usr/bin/time -l sh -c "rg --files config/scripts | rg +'/ssh-relay.*[.]test[.]mjs$' | xargs pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1"` — PASS, 51 files / 262 tests in 11.73 seconds Vitest / 13.70 seconds wall, + 189,661,184-byte maximum RSS, zero swaps. +- Desktop 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 1.87 seconds Vitest / 3.49 seconds wall, 132,857,856-byte maximum RSS, + 96,998,704-byte peak footprint, zero swaps. +- Static gates: `pnpm run typecheck` passes. Full `pnpm run lint` passes all 41 reliability gates, + the 355-entry max-lines ratchet, bundled-skill, localization catalog/parity, and localization + coverage gates; all 26 warnings remain in untouched existing files. Parsed install-step shell + source passes `bash -n`; focused five-file `oxfmt --check` and `git diff --check` pass. +- Oracle proved: Ubuntu ports, archive, security, and Azure archive domains are rewritten from HTTP + to HTTPS before APT. Both update and install use three retries, 15-second HTTP/HTTPS connection + timeouts, forced IPv4, a 60-second package-lock wait, a 180-second TERM bound, and a 15-second KILL + settlement. Nonzero exit or exhaustion still aborts the native job under `set -euo pipefail`. + Runtime construction, signing, release, desktop, SSH, and product/default paths are unchanged. +- Does not prove: HTTPS reachability or package installation on GitHub Linux x64/arm64, exact-head + Node 24 execution, artifact construction, native smoke, signing, publication, desktop embedding, + SSH behavior, or an enabled tuple. +- Follow-up: checkpoint and push, then require fresh exact-head Linux x64/arm64 plus all other native + contracts, PR Checks, and Golden E2E. Retain both port-80 RED attempts in the final evidence. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -10991,12 +11116,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Checkpoint and push the locally green callable workflow, then require its purpose contract on all -six exact-head native test families plus PR Checks and Golden E2E. Only canonical request bytes and -the base64 32-byte seed may enter the tag-restricted `relay-runtime-manifest-signing` environment, -while credential-free preparation and final reconstruction independently download the six exact -artifacts. The environment, accepted production keys, and secret remain unprovisioned, so live -signing evidence must stay BLOCKED. Keep +Checkpoint and push the locally green Linux prerequisite transport correction, then require fresh +exact-head Linux x64/arm64 package acquisition and all-six native purpose-suite counts plus PR +Checks and Golden E2E. Only canonical request bytes and the base64 32-byte seed may enter the +tag-restricted +`relay-runtime-manifest-signing` environment, while credential-free preparation and final +reconstruction independently download the six exact artifacts. The environment, accepted +production keys, and secret remain unprovisioned, so live signing evidence must stay BLOCKED. Keep Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. Do not merge to `main`; retain every production/default gate. diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index fef6ddd37bc..384045f81c3 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -938,7 +938,9 @@

      Build self-contained runtimes

      Assemble pinned Node, relay entries, patched node-pty, the exact @parcel/watcher package, and required JavaScript dependencies on - matching CI environments. Run executable smoke probes before archiving. + matching CI environments. Hosted Linux prerequisite acquisition uses HTTPS with + bounded connection timeouts and retries; exhaustion fails the native build before + any artifact is accepted. Run executable smoke probes before archiving.

    From bb4b527e4ea030cbd94f11ca9b6e284900ab8c8a Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 11:35:26 -0400 Subject: [PATCH 134/267] ci: probe Linux relay build prerequisites Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 77 +++++++++---- .../ssh-relay-runtime-workflow.test.mjs | 17 +++ ...elease-implementation-checklist-summary.md | 18 ++- ...github-release-implementation-checklist.md | 103 +++++++++++++++++- ...6-07-14-ssh-relay-github-release-plan.html | 8 +- 5 files changed, 186 insertions(+), 37 deletions(-) diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index afe4e77fd87..c2f3d13acf8 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -93,26 +93,65 @@ jobs: shell: bash run: | set -euo pipefail - # Why: hosted ARM runners repeatedly lost port-80 mirror access; bounded HTTPS keeps failures explicit. - for source_file in /etc/apt/sources.list /etc/apt/sources.list.d/*.sources; do - [ -f "$source_file" ] || continue - sudo sed -i \ - -e 's|http://ports.ubuntu.com|https://ports.ubuntu.com|g' \ - -e 's|http://archive.ubuntu.com|https://archive.ubuntu.com|g' \ - -e 's|http://security.ubuntu.com|https://security.ubuntu.com|g' \ - -e 's|http://azure.archive.ubuntu.com|https://azure.archive.ubuntu.com|g' \ - "$source_file" + # Why: hosted ARM mirror egress can disappear; prove preinstalled tools before bounded APT fallback. + probe_root=$(mktemp -d) + trap 'rm -rf "$probe_root"' EXIT + required_commands=(cc c++ make ar ld strip curl gpg gpgv python3 xz) + missing_requirements=() + for required_command in "${required_commands[@]}"; do + if ! command -v "$required_command" >/dev/null; then + missing_requirements+=("$required_command") + fi done - apt_options=( - -o Acquire::Retries=3 - -o Acquire::http::Timeout=15 - -o Acquire::https::Timeout=15 - -o Acquire::ForceIPv4=true - -o DPkg::Lock::Timeout=60 - ) - timeout --signal=TERM --kill-after=15s 180s sudo apt-get "${apt_options[@]}" update -qq - timeout --signal=TERM --kill-after=15s 180s sudo apt-get "${apt_options[@]}" install -y -qq \ - build-essential ca-certificates curl gnupg gpgv python3 xz-utils + if [ ! -r /etc/ssl/certs/ca-certificates.crt ]; then + missing_requirements+=(ca-certificates) + fi + + probe_c_toolchain() { + printf '%s\n' '#include ' 'int main(void) { return EXIT_SUCCESS; }' \ + | cc -x c - -o "$probe_root/orca-ci-c-probe" \ + && "$probe_root/orca-ci-c-probe" + } + probe_cxx_toolchain() { + printf '%s\n' '#include ' 'int main() { return EXIT_SUCCESS; }' \ + | c++ -x c++ - -o "$probe_root/orca-ci-cxx-probe" \ + && "$probe_root/orca-ci-cxx-probe" + } + if command -v cc >/dev/null && command -v c++ >/dev/null; then + if ! probe_c_toolchain || ! probe_cxx_toolchain; then + missing_requirements+=(build-essential) + fi + fi + + if ((${#missing_requirements[@]} > 0)); then + printf 'missing Linux prerequisites: %s\n' "${missing_requirements[*]}" + for source_file in /etc/apt/sources.list /etc/apt/sources.list.d/*.sources; do + [ -f "$source_file" ] || continue + sudo sed -i \ + -e 's|http://ports.ubuntu.com|https://ports.ubuntu.com|g' \ + -e 's|http://archive.ubuntu.com|https://archive.ubuntu.com|g' \ + -e 's|http://security.ubuntu.com|https://security.ubuntu.com|g' \ + -e 's|http://azure.archive.ubuntu.com|https://azure.archive.ubuntu.com|g' \ + "$source_file" + done + apt_options=( + -o Acquire::Retries=3 + -o Acquire::http::Timeout=15 + -o Acquire::https::Timeout=15 + -o Acquire::ForceIPv4=true + -o DPkg::Lock::Timeout=60 + ) + timeout --signal=TERM --kill-after=15s 180s sudo apt-get "${apt_options[@]}" update -qq + timeout --signal=TERM --kill-after=15s 180s sudo apt-get "${apt_options[@]}" install -y -qq \ + build-essential ca-certificates curl gnupg gpgv python3 xz-utils + fi + + for required_command in "${required_commands[@]}"; do + command -v "$required_command" >/dev/null + done + test -r /etc/ssl/certs/ca-certificates.crt + probe_c_toolchain + probe_cxx_toolchain - name: Install macOS signature verifier if: runner.os == 'macOS' diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 64306802340..f9dfe7cf3d6 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -56,7 +56,19 @@ describe('SSH relay runtime artifact workflow', () => { const installStep = workflow.jobs['build-posix-runtime'].steps.find( (step) => step.name === 'Install Linux build and verification tools' ) + const missingGate = 'if ((${#missing_requirements[@]} > 0)); then' + expect(installStep.run).toContain( + 'required_commands=(cc c++ make ar ld strip curl gpg gpgv python3 xz)' + ) + expect(installStep.run).toContain('missing_requirements+=(ca-certificates)') + expect(installStep.run).toContain(missingGate) + expect(installStep.run.indexOf('if ! probe_c_toolchain')).toBeLessThan( + installStep.run.indexOf(missingGate) + ) + expect(installStep.run.indexOf('sudo sed -i')).toBeGreaterThan( + installStep.run.indexOf(missingGate) + ) expect(installStep.run).toContain("'s|http://ports.ubuntu.com|https://ports.ubuntu.com|g'") expect(installStep.run).toContain('Acquire::Retries=3') expect(installStep.run).toContain('Acquire::https::Timeout=15') @@ -64,6 +76,11 @@ describe('SSH relay runtime artifact workflow', () => { expect( installStep.run.match(/timeout --signal=TERM --kill-after=15s 180s sudo apt-get/g) ).toHaveLength(2) + expect(installStep.run).toContain('orca-ci-c-probe') + expect(installStep.run).toContain('orca-ci-cxx-probe') + expect(installStep.run.lastIndexOf('probe_cxx_toolchain')).toBeGreaterThan( + installStep.run.indexOf(missingGate) + ) }) it('uploads only the first output after two clean builds verify and compare', async () => { 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 cff55dfa36b..05cf6a01502 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 @@ -9,16 +9,14 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 4 / Work Package 3 Linux prerequisite transport correction — in -progress, 2026-07-15, Codex implementation owner.** The disconnected protected-manifest workflow is -locally green at `4979bc26c`. Exact-head artifact run -[29426142423](https://github.com/stablyai/orca/actions/runs/29426142423) and its isolated attempt 2 -both prove Ubuntu 24.04 ARM port-80 mirror failure before dependencies or tests. The narrow active -package now passes locally: hosted Linux prerequisite acquisition moves to HTTPS with bounded -connection timeouts, retries, lock wait, and TERM-to-KILL settlement; exhaustion still fails the -native build. The correction is ready to checkpoint and push for fresh all-six native proof. Golden -E2E and PR Checks are green on the prior exact head. Production keys/environment/seed, publication, -desktop/default consumers, tuple enablement, and merge to `main` remain disconnected. +Active checkpoint: **Milestone 4 / Work Package 3 Linux prerequisite capability gate — in progress, +2026-07-15, Codex implementation owner.** Port 80 failed twice at `4979bc26c`; fresh ARM job +[87395626746](https://github.com/stablyai/orca/actions/runs/29428082375/job/87395626746) +at `c9ffa26a9` proves port 443 is also unavailable and that the new command bound settles with exit 124. The narrow capability-first refinement is locally green: it verifies preinstalled primitives +and executable C/C++ probes, and invokes bounded HTTPS APT only when a prerequisite is missing. It +is ready to checkpoint and push for fresh native proof. Other `c9ffa26a9` jobs are still settling. +Production keys/environment/seed, publication, desktop/default consumers, tuple enablement, and +merge to `main` remain disconnected. ## Safety status 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 a6a1ac4febc..e31f7fc0612 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 4 / Work Package 3 disconnected aggregate and protected manifest-signing workflow contract — **In progress — 2026-07-15, Codex implementation owner**. Linux aggregate-ready finalization is closed locally and on exact-head native x64/arm64 runners under E-M4-LINUX-FINALIZATION-LOCAL-RED-001, E-M4-LINUX-FINALIZATION-LOCAL-001, and E-M4-LINUX-FINALIZATION-CI-001. The active package must consume only the six exact aggregate-ready outputs, prepare canonical bytes credential-free, expose only those bytes to the tag-restricted `relay-runtime-manifest-signing` environment, accept only key ID plus Ed25519 signature, reconstruct and verify before final output, and fail closed on missing/extra/mutated inputs, approval/secret/signing failure, timeout, or partial output. It remains callable and disconnected. Release-cut, desktop builds, publication, and every tuple stay disconnected; production/default behavior is unchanged.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact-head artifact run 29426142423 and isolated attempt 2 reproduce the same Ubuntu 24.04 ARM port-80 mirror failure before source dependencies or implementation tests under E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-RED-001 and E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-RED-002. The evidence-driven correction is locally green under E-M4-LINUX-PREREQUISITE-TRANSPORT-LOCAL-RED-001 and E-M4-LINUX-PREREQUISITE-TRANSPORT-LOCAL-001: hosted Linux prerequisite acquisition moves to HTTPS with bounded connection timeouts, retries, lock wait, and TERM-to-KILL command settlement; exhaustion still fails the native build. Focused and broad release contracts, desktop parity, typecheck, full lint, formatting, shell syntax, and diff checks pass. Checkpoint and push the correction, then require a fresh exact-head all-six run. Golden E2E and PR Checks are green on the prior exact head. The user authorizes commits, pushes, draft-PR updates, CI reruns, and protected rehearsals within PRs, but not merging to `main`. The callable protected workflow remains disconnected and deliberately fails closed before signer exposure because no accepted production key exists. No signing credential, protected environment, release consumer, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — fresh exact-head ARM job 87395626746 at `c9ffa26a90751fb0941e44066c36722d9c6ee49d` proves HTTPS to `ports.ubuntu.com` is also unavailable; the new TERM/KILL bound settles the command with exit 124 after 3 minutes 40 seconds under E-M4-LINUX-PREREQUISITE-HTTPS-CI-RED-001. The capability-first refinement is locally green under E-M4-LINUX-PREREQUISITE-CAPABILITY-LOCAL-RED-001 and E-M4-LINUX-PREREQUISITE-CAPABILITY-LOCAL-001: it checks all declared command/CA primitives, runs native C and C++ compile/link/execute probes before skipping APT, retains bounded HTTPS fallback only for missing requirements, and revalidates after fallback. Focused/broad release tests, desktop parity, typecheck, full lint, shell syntax, formatting, and diff checks pass. Checkpoint and push, then require fresh exact-head native proof. Other jobs in run 29428082375 are still settling. The user authorizes commits, pushes, draft-PR updates, CI reruns, and protected rehearsals within PRs, but not merging to `main`. The callable protected workflow remains disconnected and deliberately fails closed before signer exposure because no accepted production key exists. No signing credential, protected environment, release consumer, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -11058,6 +11058,100 @@ src/main/ssh/ssh-relay-manifest-signature.test.ts src/main/ssh/ssh-relay-release - Follow-up: checkpoint and push, then require fresh exact-head Linux x64/arm64 plus all other native contracts, PR Checks, and Golden E2E. Retain both port-80 RED attempts in the final evidence. +### E-M4-LINUX-PREREQUISITE-HTTPS-CI-RED-001 — ARM runner cannot reach bounded HTTPS mirror + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `c9ffa26a90751fb0941e44066c36722d9c6ee49d`; draft PR #8741. +- Workflow/job: [SSH Relay Runtime Artifacts run 29428082375](https://github.com/stablyai/orca/actions/runs/29428082375), + Linux arm64 job + [87395626746](https://github.com/stablyai/orca/actions/runs/29428082375/job/87395626746), + 15:26:30Z–15:30:10Z. +- Runner/remote/network: GitHub-hosted `ubuntu-24.04-arm` image `20260706.52.2`, runner 2.335.1, + image provisioner `20260624.560`; the same image revision as both port-80 REDs. Public package + egress only; no SSH remote, protected environment, signing seed, release, desktop consumer, + publication, or enabled tuple. +- Log command: `gh api repos/stablyai/orca/actions/jobs/87395626746/logs | rg -n -C 3 +"Runner Image|Image:|Version:|https://ports\\.ubuntu\\.com|Ign:|Err:|Failed to fetch|exit +code 124|Process completed|Terminated"`. +- Result: expected CI RED. The HTTP-to-HTTPS source rewrite executes, then bounded APT update reaches + its 180-second TERM limit and 15-second KILL settlement; the job exits 124 after 3 minutes 40 + seconds. Source dependencies and every implementation test are never reached. In the same run, + Linux x64 job 87395626849 crosses the identical prerequisite step and proceeds to runtime build. +- Oracle proved: the command-level settlement correction works and materially reduces the previous + unbounded wait, but HTTPS is not a sufficient availability assumption for this hosted ARM image. + Always contacting the mirror is unnecessary when the runner already contains the required + compiler and verification primitives. +- Does not prove: which required commands are preinstalled, compiler/linker execution, the + capability-first refinement, implementation tests on Linux arm64, artifact construction, signing, + publication, desktop embedding, SSH behavior, or an enabled tuple. +- Follow-up: require exact command presence, CA roots, and executable C/C++ compile/link/run probes + before skipping APT; keep the bounded HTTPS path only for genuinely missing prerequisites. Add a + purpose RED first and require a fresh exact-head native run after local proof. + +### E-M4-LINUX-PREREQUISITE-CAPABILITY-LOCAL-RED-001 — Capability-first gate is absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted purpose assertion atop exact pushed head + `c9ffa26a90751fb0941e44066c36722d9c6ee49d`; draft PR #8741. +- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. Static workflow YAML + parsing only; no Linux compiler, APT request, Actions runner, SSH remote, protected environment, + release, 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-workflow.test.mjs`. +- Result: expected FAIL, 1 file / 1 failed and 6 passed of 7 tests in 185 ms Vitest / 0.85 seconds + wall, 131,629,056-byte maximum RSS, 95,851,968-byte peak footprint, zero swaps. The sole failure + shows no required-command list, CA-root gate, missing-requirements branch, or C/C++ probe; APT is + always invoked. +- Oracle proved: the workflow cannot use a complete preinstalled runner toolchain while package + mirrors are unavailable and therefore repeats a network dependency that is not inherently needed + for every build. +- Does not prove: implementation, actual preinstalled capabilities, native compile/link/execute, + APT fallback, artifact construction, signing, publication, desktop embedding, SSH behavior, or an + enabled tuple. +- Follow-up: implement the exact capability gate and preserve fail-closed bounded fallback for any + missing or broken prerequisite. + +### E-M4-LINUX-PREREQUISITE-CAPABILITY-LOCAL-001 — Capability probes and conditional fallback pass locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: local correction atop exact pushed head + `c9ffa26a90751fb0941e44066c36722d9c6ee49d`; draft PR #8741. +- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. Static YAML and shell + syntax only; native Linux probe execution remains CI evidence. No APT request, SSH remote, + protected environment, signing seed, release, publication, desktop consumer, or enabled tuple. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 config/scripts/ssh-relay-runtime-workflow.test.mjs` plus parsed install-step + `bash -n` — PASS, 1 file / 7 tests in 196 ms Vitest / 0.83 seconds wall, 131,923,968-byte maximum + RSS, 96,130,472-byte peak footprint, zero swaps. The subsequent stronger order assertions also + pass inside the broad suite. +- Broad release command: `/usr/bin/time -l sh -c "rg --files config/scripts | rg +'/ssh-relay.*[.]test[.]mjs$' | xargs pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1"` — PASS, 51 files / 262 tests in 11.17 seconds Vitest / 13.03 seconds wall, + 188,973,056-byte maximum RSS, zero swaps. +- Desktop 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 1.47 seconds Vitest / 2.97 seconds wall, 131,809,280-byte maximum RSS, + 95,917,312-byte peak footprint, zero swaps. +- Static gates: `pnpm run typecheck` passes. Full `pnpm run lint` passes all 41 reliability gates, + the 355-entry max-lines ratchet, bundled-skill, localization catalog/parity, and localization + coverage gates; all 26 warnings remain in untouched existing files. +- Oracle proved: command presence covers `cc`, `c++`, `make`, `ar`, `ld`, `strip`, `curl`, `gpg`, + `gpgv`, `python3`, and `xz`, plus the system CA root. Native C and C++ source is compiled, linked, + and executed before APT may be skipped. Any absent command, CA root, or failed toolchain probe + enters the bounded HTTPS branch; final command, CA, and both executable probes run again after any + fallback. Temporary probe files are removed on every exit. Nonzero probe/install status aborts the + job under `set -euo pipefail`. +- Does not prove: actual command availability or probe execution on GitHub Linux x64/arm64, + conditional APT avoidance, exact-head Node 24 tests, artifact construction, signing, publication, + desktop embedding, SSH behavior, or an enabled tuple. +- Follow-up: checkpoint and push, then require fresh exact-head Linux x64/arm64 proof plus all other + native contracts, PR Checks, and Golden E2E. Retain all three mirror REDs and the bounded-settlement + result. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -11116,10 +11210,9 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Checkpoint and push the locally green Linux prerequisite transport correction, then require fresh -exact-head Linux x64/arm64 package acquisition and all-six native purpose-suite counts plus PR -Checks and Golden E2E. Only canonical request bytes and the base64 32-byte seed may enter the -tag-restricted +Checkpoint and push the locally green capability-first Linux prerequisite gate, then require fresh +exact-head Linux x64/arm64 and all-six native purpose-suite counts plus PR Checks and Golden E2E. +Only canonical request bytes and the base64 32-byte seed may enter the tag-restricted `relay-runtime-manifest-signing` environment, while credential-free preparation and final reconstruction independently download the six exact artifacts. The environment, accepted production keys, and secret remain unprovisioned, so live signing evidence must stay BLOCKED. Keep diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 384045f81c3..fd027ae3067 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -938,9 +938,11 @@

    Build self-contained runtimes

    Assemble pinned Node, relay entries, patched node-pty, the exact @parcel/watcher package, and required JavaScript dependencies on - matching CI environments. Hosted Linux prerequisite acquisition uses HTTPS with - bounded connection timeouts and retries; exhaustion fails the native build before - any artifact is accepted. Run executable smoke probes before archiving. + matching CI environments. Hosted Linux jobs first prove the preinstalled + compiler/toolchain with executable C and C++ probes; only genuinely missing + prerequisites use HTTPS with bounded connection timeouts and retries. Probe or + install exhaustion fails the native build before any artifact is accepted. Run + executable runtime smoke probes before archiving.

    From 9b83971866cd5f7fc71de20a3e09ab2a120e01bd Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 11:56:36 -0400 Subject: [PATCH 135/267] docs: record protected relay workflow CI proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 22 ++-- ...github-release-implementation-checklist.md | 112 +++++++++++++++--- 2 files changed, 108 insertions(+), 26 deletions(-) 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 05cf6a01502..379ffe5558d 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 @@ -9,14 +9,15 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 4 / Work Package 3 Linux prerequisite capability gate — in progress, -2026-07-15, Codex implementation owner.** Port 80 failed twice at `4979bc26c`; fresh ARM job -[87395626746](https://github.com/stablyai/orca/actions/runs/29428082375/job/87395626746) -at `c9ffa26a9` proves port 443 is also unavailable and that the new command bound settles with exit 124. The narrow capability-first refinement is locally green: it verifies preinstalled primitives -and executable C/C++ probes, and invokes bounded HTTPS APT only when a prerequisite is missing. It -is ready to checkpoint and push for fresh native proof. Other `c9ffa26a9` jobs are still settling. -Production keys/environment/seed, publication, desktop/default consumers, tuple enablement, and -merge to `main` remain disconnected. +Active checkpoint: **Milestone 4 / Work Package 3 disconnected release failure/recovery contracts — +in progress, 2026-07-15, Codex implementation owner.** Exact-head run +[29428772206](https://github.com/stablyai/orca/actions/runs/29428772206) at `bb4b527e4` passes all six +target-native artifact jobs. Both Linux runners prove the complete preinstalled toolchain and skip +unreachable APT; the protected manifest-workflow source contract passes on every runner. PR Checks +and Golden E2E are green. Next, add purpose-named disconnected failure contracts for timeout, retry +exhaustion, approval failure, signing failure, partial output, aggregate failure, and exact-draft +recovery. Production keys/environment/seed, publication, desktop/default consumers, tuple +enablement, and merge to `main` remain disconnected. ## Safety status @@ -257,8 +258,9 @@ merge to `main` remain disconnected. `E-M4-MANIFEST-AGGREGATE-COMMAND-LOCAL-001`, with all-six exact-head CI closed by `E-M4-MANIFEST-AGGREGATE-COMMAND-CI-001`. The callable workflow remains open. Its disconnected three-job source contract is locally green under - `E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-001`, but exact-head CI and live protected signing - remain open; no accepted production key, environment, or seed is provisioned. + `E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-001`, and exact-head execution passes on all six native + jobs under `E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001`. Live protected signing remains open; no + accepted production key, environment, or seed is provisioned. - [ ] 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 e31f7fc0612..18a3caeae13 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md @@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    -Current phase: Milestone 4 / Work Package 3 disconnected aggregate and protected manifest-signing workflow contract — **In progress — 2026-07-15, Codex implementation owner**. Linux aggregate-ready finalization is closed locally and on exact-head native x64/arm64 runners under E-M4-LINUX-FINALIZATION-LOCAL-RED-001, E-M4-LINUX-FINALIZATION-LOCAL-001, and E-M4-LINUX-FINALIZATION-CI-001. The active package must consume only the six exact aggregate-ready outputs, prepare canonical bytes credential-free, expose only those bytes to the tag-restricted `relay-runtime-manifest-signing` environment, accept only key ID plus Ed25519 signature, reconstruct and verify before final output, and fail closed on missing/extra/mutated inputs, approval/secret/signing failure, timeout, or partial output. It remains callable and disconnected. 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** — fresh exact-head ARM job 87395626746 at `c9ffa26a90751fb0941e44066c36722d9c6ee49d` proves HTTPS to `ports.ubuntu.com` is also unavailable; the new TERM/KILL bound settles the command with exit 124 after 3 minutes 40 seconds under E-M4-LINUX-PREREQUISITE-HTTPS-CI-RED-001. The capability-first refinement is locally green under E-M4-LINUX-PREREQUISITE-CAPABILITY-LOCAL-RED-001 and E-M4-LINUX-PREREQUISITE-CAPABILITY-LOCAL-001: it checks all declared command/CA primitives, runs native C and C++ compile/link/execute probes before skipping APT, retains bounded HTTPS fallback only for missing requirements, and revalidates after fallback. Focused/broad release tests, desktop parity, typecheck, full lint, shell syntax, formatting, and diff checks pass. Checkpoint and push, then require fresh exact-head native proof. Other jobs in run 29428082375 are still settling. The user authorizes commits, pushes, draft-PR updates, CI reruns, and protected rehearsals within PRs, but not merging to `main`. The callable protected workflow remains disconnected and deliberately fails closed before signer exposure because no accepted production key exists. No signing credential, protected environment, release consumer, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
    +Current phase: Milestone 4 / Work Package 3 disconnected release failure/recovery contracts — **In progress — 2026-07-15, Codex implementation owner**. The callable aggregate and protected manifest-signing workflow contract is closed locally and on all six exact-head native jobs under E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-001 and E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001. The active package must add purpose-named disconnected failure contracts for timeout, bounded retry success/exhaustion, approval absence/denial/timeout, signing failure, aggregate failure, partial output, and exact unpublished-draft recovery without connecting credentials, publication, desktop builds, or any tuple. Release-cut, desktop builds, publication, and every tuple stay disconnected; production/default behavior is unchanged.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `bb4b527e4ea030cbd94f11ca9b6e284900ab8c8a` passes all six target-native artifact jobs in run 29428772206 under E-M4-LINUX-PREREQUISITE-CAPABILITY-CI-001 and E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001. Linux x64/arm64 capability probes complete in about four seconds, never enter APT, and proceed through full runtime construction; the all-six workflow source contract and broad runtime suites pass. Both Linux oldest-userland supplements and Windows x64 floor pass. Windows arm64 runtime smoke succeeds but the floor job retains the expected fail-closed 26200-versus-26100 mismatch. PR Checks 29428772127 and Golden E2E 29428772389 are green. The user authorizes commits, pushes, draft-PR updates, CI reruns, and protected rehearsals within PRs, but not merging to `main`. No accepted production key, protected environment/seed, native-signing trust proof, release consumer, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -100,12 +100,16 @@ same change as the work it records. native CI pass under E-M4-BUILD-PREREQUISITE-LOCAL-RED-001, E-M4-BUILD-PREREQUISITE-LOCAL-001, and E-M4-BUILD-PREREQUISITE-CI-001. Release workflows remain disconnected. -- Active package: Work Package 3 callable, disconnected aggregate/manifest-signing workflow - contract. Begin with a purpose-named RED for all-six exact aggregate input consumption, - credential-free canonical preparation, protected environment isolation, seed decoding and - signature-only return, reconstruction/verification, bounded failure cleanup, and complete - release/desktop/publication disconnection. The environment and seed are unprovisioned; static and - test-key proof do not count as live protected signing evidence. +- Completed Work Package 3 gate: callable, disconnected aggregate/manifest-signing workflow source + contract. Exact aggregate input names, credential-free preparation/finalization, protected signer + isolation, signature-only return, SHA-pinned actions, bounds, and complete consumer disconnection + pass locally and on all six exact-head native jobs under E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-001 + and E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001. The environment and seed remain unprovisioned; static + and test-key proof do not count as live protected signing evidence. +- Active package: Work Package 3 disconnected release failure/recovery contracts. Add a purpose-named + RED and credential-free harness for the declared timeout, retry exhaustion, approval failure, + signing failure, aggregate failure, partial-output cleanup, and same-unpublished-draft recovery + states. Keep every real credential, release write, desktop consumer, and tuple disconnected. - 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 @@ -11152,6 +11156,80 @@ src/main/ssh/ssh-relay-manifest-signature.test.ts src/main/ssh/ssh-relay-release native contracts, PR Checks, and Golden E2E. Retain all three mirror REDs and the bounded-settlement result. +### E-M4-LINUX-PREREQUISITE-CAPABILITY-CI-001 — Native Linux capability gates avoid unavailable mirrors + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `bb4b527e4ea030cbd94f11ca9b6e284900ab8c8a`; draft PR #8741. +- Workflow/jobs: [SSH Relay Runtime Artifacts run 29428772206](https://github.com/stablyai/orca/actions/runs/29428772206), + 15:35:38Z–15:49:34Z. Linux x64 job + [87398040874](https://github.com/stablyai/orca/actions/runs/29428772206/job/87398040874) + passes in 4m39s; Linux arm64 job + [87398040969](https://github.com/stablyai/orca/actions/runs/29428772206/job/87398040969) + passes in 4m14s. +- Runner/remote/network: GitHub-hosted Ubuntu 24.04.4, runner 2.335.1. X64 uses + `ubuntu-24.04` image `20260714.240.1`, provisioner `20260707.563`, Azure `westus`; arm64 uses + `ubuntu-24.04-arm` image `20260706.52.2`, provisioner `20260624.560`, Azure `northcentralus`. + There is no SSH remote, protected environment, signing seed, release, desktop consumer, + publication, or enabled tuple. +- Log command: `gh run view 29428772206 --repo stablyai/orca --log` followed by job-scoped `rg` for + runner identity, prerequisite-step boundaries, `missing Linux prerequisites`, test counts, and + durations. +- Result: PASS. The x64 gate runs from 15:36:34.048Z until the next step at 15:36:38.449Z; arm64 + runs from 15:36:22.134Z until the next step at 15:36:26.177Z. In each complete job log, the exact + text `missing Linux prerequisites` appears only once in the echoed shell source and never as + runtime output, so neither runner enters APT. Native C and C++ compile/link/execute probes, final revalidation, + dependency installation, both clean runtime builds, exact equality, bundled Node, PTY/watcher + smoke, metadata, aggregate-ready finalization, and artifact upload all pass. +- Contract counts: each Linux job passes 50 files / 258 tests. Vitest duration is 8.12s on x64 and + 6.42s on arm64. Both Linux oldest-userland supplements pass: arm64 job 87399667498 in 1m05s and + x64 job 87399667523 in 52s. +- Adjacent exact-head evidence: macOS x64 job 87398040917 passes in 6m21s (50 files / 258 tests, + 28.75s Vitest), macOS arm64 job 87398040906 passes in 3m30s (50 / 258, 7.71s), Windows x64 job + 87398040936 passes in 5m10s (51 files / 252 passed plus 10 skipped of 262, 11.49s), and Windows + arm64 job 87398041157 passes in 9m17s (51 / 252 plus 10 skipped, 13.70s). Windows x64 floor job + 87400400656 passes in 2m11s. Windows arm64 floor job 87400400701 executes the runtime successfully + and then fails exactly because hosted build 26200 is not the required build 26100. +- Oracle proved: both GitHub Linux architectures possess and can execute the declared compiler, + linker, archive, strip, crypto, download, Python, XZ, and CA capabilities without package egress; + unavailable mirrors no longer block a complete native artifact job. A genuinely absent or broken + capability still enters the bounded HTTPS fallback and fails closed if installation cannot settle. +- Does not prove: the missing-capability APT success branch, Linux kernel 4.18, native signing/trust, + protected manifest signing, release publication/read-back, desktop embedding, SSH transfer/install, + packaged RPC behavior, performance against legacy, or any enabled tuple. + +### E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001 — Protected workflow source contract passes all six native jobs + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source and jobs: exact pushed head `bb4b527e4ea030cbd94f11ca9b6e284900ab8c8a` in artifact run + [29428772206](https://github.com/stablyai/orca/actions/runs/29428772206); build jobs + 87398040874, 87398040969, 87398040917, 87398040906, 87398040936, and 87398041157. +- Native runners: Linux x64 `ubuntu-24.04` image `20260714.240.1`; Linux arm64 + `ubuntu-24.04-arm` `20260706.52.2`; macOS x64 `macos-15` `20260629.0276.1` on macOS 15.7.7; + macOS arm64 `macos-15-arm64` `20260706.0213.1` on macOS 15.7.7; Windows x64 `windows-2022` + `20260706.237.1` on Windows Server 2022 build 20348; Windows arm64 `windows-11-arm64` + `20260706.102.1` on Windows 11 build 26200. Every job uses runner 2.335.1 and Node 24.18.0. +- Result: PASS. `config/scripts/ssh-relay-runtime-manifest-signing-workflow.test.mjs` passes 1/1 on + Linux x64 in 44ms, Linux arm64 in 27ms, macOS x64 in 67ms, macOS arm64 in 15ms, Windows x64 in + 45ms, and Windows arm64 in 30ms. The all-six broad counts and durations are recorded in + E-M4-LINUX-PREREQUISITE-CAPABILITY-CI-001. +- Adjacent exact-head workflows: PR Checks run + [29428772127](https://github.com/stablyai/orca/actions/runs/29428772127), job 87398002184, passes + 15:35:42Z–15:49:26Z. Golden E2E run + [29428772389](https://github.com/stablyai/orca/actions/runs/29428772389) passes Linux job + 87398002948 and macOS job 87398003007. +- Oracle proved: the callable workflow YAML parses identically on every target-native runner family; + accepts only the four exact inputs and one required seed; downloads six explicit artifacts in both + credential-free stages; exposes no runtime artifacts to the signer; exposes the seed only to the + environment-gated signer; accepts only the signature result; uses SHA-pinned actions and strict + job bounds; omits write permissions and publication commands; has no accepted-key placeholder; + and remains disconnected from release-cut, macOS release, and native-signing rehearsal consumers. +- Does not prove: workflow-call execution, environment/tag/reviewer policy, seed availability or + secrecy at runtime, approval denial/timeout, a real signature, accepted production keys, native + signing/trust, aggregate artifact passage, publication/read-back, desktop embedding, SSH behavior, + or any enabled tuple. Those gates remain open and fail closed. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -11210,14 +11288,16 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Checkpoint and push the locally green capability-first Linux prerequisite gate, then require fresh -exact-head Linux x64/arm64 and all-six native purpose-suite counts plus PR Checks and Golden E2E. -Only canonical request bytes and the base64 32-byte seed may enter the tag-restricted -`relay-runtime-manifest-signing` environment, while credential-free preparation and final -reconstruction independently download the six exact artifacts. The environment, accepted -production keys, and secret remain unprovisioned, so live signing evidence must stay BLOCKED. Keep -Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, -and every tuple separately gated. Do not merge to `main`; retain every production/default gate. +Checkpoint and push E-M4-LINUX-PREREQUISITE-CAPABILITY-CI-001 and +E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001. Then begin a purpose-named disconnected RED for the +release failure/recovery harness: timeout, bounded retry success/exhaustion, approval +absence/denial/timeout, signing failure, aggregate failure, partial-output cleanup, and exact +unpublished-draft recovery. Reuse the reviewed budgets and same-draft identity constraints; do not +connect credentials, release writes, desktop consumers, or tuples. The protected environment, +accepted production keys, and secret remain unprovisioned, so live signing evidence stays BLOCKED. +Keep Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, +publication, and every tuple separately gated. Do not merge to `main`; retain every +production/default gate. 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 From 8d9013cd106734ecd979f785b515dd9640841330 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 12:06:02 -0400 Subject: [PATCH 136/267] build(ssh): add bounded relay draft upload Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 8 + .../ssh-relay-runtime-draft-upload.mjs | 348 ++++++++++++++++++ .../ssh-relay-runtime-draft-upload.test.mjs | 254 +++++++++++++ .../ssh-relay-runtime-workflow.test.mjs | 3 +- ...elease-implementation-checklist-summary.md | 15 +- ...github-release-implementation-checklist.md | 89 ++++- 6 files changed, 699 insertions(+), 18 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-draft-upload.mjs create mode 100644 config/scripts/ssh-relay-runtime-draft-upload.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index c2f3d13acf8..37d2c741edd 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -218,6 +218,8 @@ jobs: node --check config/scripts/ssh-relay-runtime-aggregate-input.test.mjs node --check config/scripts/ssh-relay-runtime-draft-readback.mjs node --check config/scripts/ssh-relay-runtime-draft-readback.test.mjs + node --check config/scripts/ssh-relay-runtime-draft-upload.mjs + node --check config/scripts/ssh-relay-runtime-draft-upload.test.mjs node --check config/scripts/ssh-relay-runtime-manifest-validation.mjs node --check config/scripts/ssh-relay-runtime-manifest-assembly.mjs node --check config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs @@ -273,6 +275,7 @@ jobs: config/scripts/ssh-relay-runtime-draft-recovery.test.mjs \ config/scripts/ssh-relay-runtime-aggregate-input.test.mjs \ config/scripts/ssh-relay-runtime-draft-readback.test.mjs \ + config/scripts/ssh-relay-runtime-draft-upload.test.mjs \ config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs \ config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs \ config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs \ @@ -634,6 +637,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime draft read-back syntax check failed' } node --check config/scripts/ssh-relay-runtime-draft-readback.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime draft read-back test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-draft-upload.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime draft upload syntax check failed' } + node --check config/scripts/ssh-relay-runtime-draft-upload.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime draft upload test syntax check failed' } node --check config/scripts/ssh-relay-runtime-manifest-validation.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime manifest validation syntax check failed' } node --check config/scripts/ssh-relay-runtime-manifest-assembly.mjs @@ -718,6 +725,7 @@ jobs: config/scripts/ssh-relay-runtime-draft-recovery.test.mjs ` config/scripts/ssh-relay-runtime-aggregate-input.test.mjs ` config/scripts/ssh-relay-runtime-draft-readback.test.mjs ` + config/scripts/ssh-relay-runtime-draft-upload.test.mjs ` config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs ` config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs ` config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs ` diff --git a/config/scripts/ssh-relay-runtime-draft-upload.mjs b/config/scripts/ssh-relay-runtime-draft-upload.mjs new file mode 100644 index 00000000000..161701bc0c7 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-draft-upload.mjs @@ -0,0 +1,348 @@ +import { createHash } from 'node:crypto' +import { createReadStream } from 'node:fs' +import { lstat } from 'node:fs/promises' +import { setTimeout as delay } from 'node:timers/promises' + +const API_VERSION = '2022-11-28' +const ASSET_HOST = 'release-assets.githubusercontent.com' +const ASSET_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,239}$/u +const COMMIT_PATTERN = /^[0-9a-f]{40}$/u +const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u +const MANAGED_ASSET_PREFIX = 'orca-ssh-relay-runtime-' +const REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u +const TAG_PATTERN = /^v\d+\.\d+\.\d+(?:-rc\.\d+(?:\.[0-9A-Za-z]+)?)?$/u +const MAX_ASSET_BYTES = 100 * 1024 * 1024 +const MAX_ASSETS = 26 +const MAX_ATTEMPTS = 3 +const MAX_TOTAL_BYTES = 1024 * 1024 * 1024 +const UPLOAD_TIMEOUT_MS = 15 * 60_000 + +function assertObject(value, label) { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`SSH relay runtime draft upload ${label} must be an object`) + } +} + +function assertExactFields(value, fields, label) { + assertObject(value, label) + if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify([...fields].sort())) { + throw new Error(`SSH relay runtime draft upload ${label} has unexpected or missing fields`) + } +} + +function authenticatedHeaders(accept = 'application/vnd.github+json', token) { + return { + Accept: accept, + Authorization: `Bearer ${token}`, + 'X-GitHub-Api-Version': API_VERSION + } +} + +async function responseError(response) { + const body = await response.text().catch(() => '') + return new Error( + `SSH relay runtime GitHub upload request failed ${response.status} ${response.statusText}: ${body.slice(0, 300)}` + ) +} + +function normalizeInputs({ repo, releaseId, tag, sourceCommit, token, assets }) { + if (typeof repo !== 'string' || !REPOSITORY_PATTERN.test(repo)) { + throw new Error('SSH relay runtime draft upload repository is invalid') + } + if (!Number.isSafeInteger(releaseId) || releaseId <= 0) { + throw new Error('SSH relay runtime draft upload release ID is invalid') + } + if (typeof tag !== 'string' || !TAG_PATTERN.test(tag)) { + throw new Error('SSH relay runtime draft upload tag is invalid') + } + if (typeof sourceCommit !== 'string' || !COMMIT_PATTERN.test(sourceCommit)) { + throw new Error('SSH relay runtime draft upload source commit is invalid') + } + if (typeof token !== 'string' || token.length === 0) { + throw new Error('SSH relay runtime draft upload token is required') + } + if (!Array.isArray(assets) || assets.length === 0 || assets.length > MAX_ASSETS) { + throw new Error('SSH relay runtime draft upload assets must be a bounded non-empty array') + } + const names = new Set() + let totalBytes = 0 + const normalizedAssets = assets.map((asset, index) => { + assertExactFields(asset, ['name', 'path', 'sha256', 'size'], `asset ${index}`) + if ( + typeof asset.name !== 'string' || + !ASSET_NAME_PATTERN.test(asset.name) || + !asset.name.startsWith(MANAGED_ASSET_PREFIX) || + names.has(asset.name) + ) { + throw new Error('SSH relay runtime draft upload asset has an invalid or duplicate name') + } + names.add(asset.name) + if (typeof asset.path !== 'string' || asset.path.length === 0) { + throw new Error(`SSH relay runtime draft upload asset path is invalid: ${asset.name}`) + } + if (typeof asset.sha256 !== 'string' || !DIGEST_PATTERN.test(asset.sha256)) { + throw new Error(`SSH relay runtime draft upload asset SHA-256 is invalid: ${asset.name}`) + } + if (!Number.isSafeInteger(asset.size) || asset.size <= 0 || asset.size > MAX_ASSET_BYTES) { + throw new Error(`SSH relay runtime draft upload asset size is invalid: ${asset.name}`) + } + totalBytes += asset.size + return { ...asset } + }) + if (!Number.isSafeInteger(totalBytes) || totalBytes > MAX_TOTAL_BYTES) { + throw new Error('SSH relay runtime draft upload assets exceed the total size limit') + } + return { repo, releaseId, tag, sourceCommit, token, assets: normalizedAssets } +} + +async function verifyLocalAsset(asset, signal) { + signal.throwIfAborted() + const metadata = await lstat(asset.path) + if (!metadata.isFile() || metadata.isSymbolicLink()) { + throw new Error( + `SSH relay runtime draft upload local asset must be a regular file: ${asset.name}` + ) + } + if (metadata.size !== asset.size) { + throw new Error(`SSH relay runtime draft upload local asset size disagrees: ${asset.name}`) + } + const hash = createHash('sha256') + for await (const chunk of createReadStream(asset.path)) { + signal.throwIfAborted() + hash.update(chunk) + } + if (`sha256:${hash.digest('hex')}` !== asset.sha256) { + throw new Error(`SSH relay runtime draft upload local asset SHA-256 disagrees: ${asset.name}`) + } +} + +function validateDraft(release, expected, { releaseId, tag, sourceCommit }) { + assertObject(release, 'release') + if (release.id !== releaseId || release.draft !== true) { + throw new Error('SSH relay runtime release must remain the requested draft during upload') + } + if (release.tag_name !== tag) { + throw new Error('SSH relay runtime draft upload tag does not match') + } + if (release.target_commitish !== sourceCommit) { + throw new Error('SSH relay runtime draft upload source commit does not match') + } + if (!Array.isArray(release.assets)) { + throw new Error('SSH relay runtime draft upload release assets must be an array') + } + const expectedNames = new Set(expected.map((asset) => asset.name)) + const existing = new Map() + for (const asset of release.assets) { + assertObject(asset, 'release asset') + if (typeof asset.name !== 'string' || existing.has(asset.name)) { + throw new Error('SSH relay runtime draft upload has a duplicate or invalid release asset') + } + existing.set(asset.name, asset) + if (asset.name.startsWith(MANAGED_ASSET_PREFIX) && !expectedNames.has(asset.name)) { + throw new Error(`SSH relay runtime draft has an unexpected managed asset: ${asset.name}`) + } + } + return existing +} + +async function fetchDraft(context, signal) { + const response = await context.fetchImpl( + `https://api.github.com/repos/${context.repo}/releases/${context.releaseId}`, + { headers: authenticatedHeaders(undefined, context.token), redirect: 'error', signal } + ) + if (!response.ok) { + throw await responseError(response) + } + return validateDraft(await response.json(), context.assets, context) +} + +async function fetchExistingAsset(context, remote, signal) { + if ( + !Number.isSafeInteger(remote.id) || + remote.id <= 0 || + remote.state !== 'uploaded' || + !Number.isSafeInteger(remote.size) + ) { + throw new Error(`SSH relay runtime draft upload has invalid asset metadata: ${remote.name}`) + } + let response = await context.fetchImpl( + `https://api.github.com/repos/${context.repo}/releases/assets/${remote.id}`, + { + headers: authenticatedHeaders('application/octet-stream', context.token), + redirect: 'manual', + signal + } + ) + if (response.status === 302) { + const location = response.headers.get('location') + const url = location ? new URL(location) : null + if (!url || url.protocol !== 'https:' || url.hostname !== ASSET_HOST) { + throw new Error('SSH relay runtime draft upload asset redirect is invalid') + } + // Why: authenticated API headers must never be forwarded to the signed asset CDN URL. + response = await context.fetchImpl(url.href, { + headers: { Accept: 'application/octet-stream' }, + redirect: 'error', + signal + }) + } + if (response.status !== 200 || !response.body) { + throw await responseError(response) + } + return response +} + +async function verifyExistingAsset(context, expected, remote, signal) { + if (remote.size !== expected.size) { + throw new Error( + `SSH relay runtime draft upload existing asset size disagrees: ${expected.name}` + ) + } + const response = await fetchExistingAsset(context, remote, signal) + const hash = createHash('sha256') + let size = 0 + for await (const chunk of response.body) { + signal.throwIfAborted() + const bytes = Buffer.from(chunk) + size += bytes.length + if (size > expected.size || size > MAX_ASSET_BYTES) { + throw new Error( + `SSH relay runtime draft upload existing asset size exceeded: ${expected.name}` + ) + } + hash.update(bytes) + } + if (size !== expected.size) { + throw new Error( + `SSH relay runtime draft upload existing asset size disagrees: ${expected.name}` + ) + } + if (`sha256:${hash.digest('hex')}` !== expected.sha256) { + throw new Error( + `SSH relay runtime draft upload existing asset SHA-256 disagrees: ${expected.name}` + ) + } +} + +async function reconcileDraft(context, signal) { + const remoteByName = await fetchDraft(context, signal) + const reusable = new Set() + for (const expected of context.assets) { + const remote = remoteByName.get(expected.name) + if (remote) { + await verifyExistingAsset(context, expected, remote, signal) + reusable.add(expected.name) + } + } + return reusable +} + +function retryableStatus(status) { + return status === 408 || status === 429 || status >= 500 +} + +async function defaultDelay(milliseconds, signal) { + await delay(milliseconds, undefined, { signal }) +} + +async function uploadAsset(context, asset, signal, delayImpl) { + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) { + await verifyLocalAsset(asset, signal) + let failure + let response + const body = createReadStream(asset.path) + try { + response = await context.fetchImpl( + `https://uploads.github.com/repos/${context.repo}/releases/${context.releaseId}/assets?name=${encodeURIComponent(asset.name)}`, + { + method: 'POST', + headers: { + ...authenticatedHeaders('application/vnd.github+json', context.token), + 'Content-Length': String(asset.size), + 'Content-Type': 'application/octet-stream' + }, + body, + duplex: 'half', + redirect: 'error', + signal + } + ) + } catch (error) { + signal.throwIfAborted() + failure = error + } finally { + // Why: injected failures and early HTTP responses may not consume the request stream. + body.destroy() + } + if (response?.status === 201) { + const result = await response.json() + if ( + result.name !== asset.name || + result.state !== 'uploaded' || + result.size !== asset.size || + !Number.isSafeInteger(result.id) || + result.id <= 0 + ) { + throw new Error(`SSH relay runtime draft upload returned invalid metadata: ${asset.name}`) + } + return 'uploaded' + } + if (response) { + failure = await responseError(response) + if (!retryableStatus(response.status)) { + throw failure + } + } + + // Why: a lost POST response may still have created the asset; reconcile before any retry. + const reusable = await reconcileDraft(context, signal) + if (reusable.has(asset.name)) { + return 'reused' + } + if (attempt === MAX_ATTEMPTS) { + throw new Error(`SSH relay runtime draft upload retry exhaustion: ${asset.name}`, { + cause: failure + }) + } + await delayImpl(2 ** (attempt - 1) * 1_000, signal) + } + throw new Error(`SSH relay runtime draft upload retry exhaustion: ${asset.name}`) +} + +export async function uploadSshRelayRuntimeDraftAssets({ + repo, + releaseId, + tag, + sourceCommit, + token, + assets, + fetchImpl = fetch, + delayImpl = defaultDelay, + signal +}) { + const effectiveSignal = signal + ? AbortSignal.any([signal, AbortSignal.timeout(UPLOAD_TIMEOUT_MS)]) + : AbortSignal.timeout(UPLOAD_TIMEOUT_MS) + effectiveSignal.throwIfAborted() + const context = { + ...normalizeInputs({ repo, releaseId, tag, sourceCommit, token, assets }), + fetchImpl + } + for (const asset of context.assets) { + await verifyLocalAsset(asset, effectiveSignal) + } + const initiallyReusable = await reconcileDraft(context, effectiveSignal) + const reusedAssets = [] + const uploadedAssets = [] + for (const asset of context.assets) { + const identity = { name: asset.name, sha256: asset.sha256, size: asset.size } + if (initiallyReusable.has(asset.name)) { + reusedAssets.push(identity) + continue + } + const outcome = await uploadAsset(context, asset, effectiveSignal, delayImpl) + const recordedAssets = outcome === 'reused' ? reusedAssets : uploadedAssets + recordedAssets.push(identity) + } + return { releaseId, tag, sourceCommit, reusedAssets, uploadedAssets } +} diff --git a/config/scripts/ssh-relay-runtime-draft-upload.test.mjs b/config/scripts/ssh-relay-runtime-draft-upload.test.mjs new file mode 100644 index 00000000000..9f598bd7809 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-draft-upload.test.mjs @@ -0,0 +1,254 @@ +import { createHash } from 'node:crypto' +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { uploadSshRelayRuntimeDraftAssets } from './ssh-relay-runtime-draft-upload.mjs' + +const REPO = 'stablyai/orca' +const RELEASE_ID = 42 +const TAG = 'v1.5.0-rc.1' +const SOURCE_COMMIT = 'a'.repeat(40) +const TOKEN = 'test-token' +const NAME = 'orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.xz' +const BYTES = Buffer.from('immutable relay runtime bytes') +const SECOND_NAME = 'orca-ssh-relay-runtime-manifest.json' +const SECOND_BYTES = Buffer.from('immutable signed manifest bytes') + +let root + +function digest(bytes = BYTES) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +function localAsset(overrides = {}) { + return { + name: NAME, + path: join(root, NAME), + sha256: digest(), + size: BYTES.length, + ...overrides + } +} + +function draft(assets = [], overrides = {}) { + return { + id: RELEASE_ID, + tag_name: TAG, + target_commitish: SOURCE_COMMIT, + draft: true, + assets, + ...overrides + } +} + +function uploadedAsset(overrides = {}) { + return { id: 101, name: NAME, state: 'uploaded', size: BYTES.length, ...overrides } +} + +function json(body, status = 200) { + return Response.json(body, { status }) +} + +async function requestBytes(options) { + const chunks = [] + for await (const chunk of options.body) { + chunks.push(Buffer.from(chunk)) + } + return Buffer.concat(chunks) +} + +function input(fetchImpl, overrides = {}) { + return { + repo: REPO, + releaseId: RELEASE_ID, + tag: TAG, + sourceCommit: SOURCE_COMMIT, + token: TOKEN, + assets: [localAsset()], + fetchImpl, + delayImpl: vi.fn(async () => {}), + ...overrides + } +} + +beforeEach(async () => { + root = await mkdtemp(join(tmpdir(), 'orca-relay-draft-upload-')) + await writeFile(join(root, NAME), BYTES) +}) + +afterEach(async () => { + await rm(root, { recursive: true, force: true }) + vi.restoreAllMocks() +}) + +describe('SSH relay runtime draft upload', () => { + it('uploads only locally verified immutable bytes to the exact draft', async () => { + const bodies = [] + const fetchImpl = vi.fn(async (url, options = {}) => { + if (options.method === 'POST') { + bodies.push(await requestBytes(options)) + return json(uploadedAsset(), 201) + } + return json(draft()) + }) + + await expect(uploadSshRelayRuntimeDraftAssets(input(fetchImpl))).resolves.toEqual({ + releaseId: RELEASE_ID, + tag: TAG, + sourceCommit: SOURCE_COMMIT, + reusedAssets: [], + uploadedAssets: [{ name: NAME, sha256: digest(), size: BYTES.length }] + }) + expect(bodies).toEqual([BYTES]) + const [url, options] = fetchImpl.mock.calls.find(([, value]) => value.method === 'POST') + expect(url).toBe( + `https://uploads.github.com/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${NAME}` + ) + expect(options.headers).toMatchObject({ + Authorization: `Bearer ${TOKEN}`, + 'Content-Length': String(BYTES.length), + 'Content-Type': 'application/octet-stream' + }) + }) + + it('reuses only same-draft assets whose downloaded bytes match', async () => { + const location = 'https://release-assets.githubusercontent.com/example/runtime?sig=signed' + const fetchImpl = vi + .fn() + .mockResolvedValueOnce(json(draft([uploadedAsset()]))) + .mockResolvedValueOnce(new Response(null, { status: 302, headers: { location } })) + .mockResolvedValueOnce(new Response(BYTES)) + + await expect(uploadSshRelayRuntimeDraftAssets(input(fetchImpl))).resolves.toMatchObject({ + reusedAssets: [{ name: NAME, sha256: digest(), size: BYTES.length }], + uploadedAssets: [] + }) + expect(fetchImpl).toHaveBeenCalledTimes(3) + expect(fetchImpl.mock.calls.some(([, options]) => options.method === 'POST')).toBe(false) + expect(fetchImpl.mock.calls[1][1].headers.Authorization).toBe(`Bearer ${TOKEN}`) + expect(fetchImpl.mock.calls[2][0]).toBe(location) + expect(fetchImpl.mock.calls[2][1].headers).not.toHaveProperty('Authorization') + }) + + it('fails closed on unsafe draft state or existing managed bytes', async () => { + for (const release of [ + draft([], { draft: false }), + draft([], { tag_name: 'v1.5.0-rc.2' }), + draft([], { target_commitish: 'b'.repeat(40) }), + draft([uploadedAsset({ name: 'orca-ssh-relay-runtime-unexpected.zip' })]) + ]) { + await expect( + uploadSshRelayRuntimeDraftAssets(input(vi.fn().mockResolvedValueOnce(json(release)))) + ).rejects.toThrow(/draft|tag|source commit|unexpected managed asset/i) + } + + const changedFetch = vi + .fn() + .mockResolvedValueOnce(json(draft([uploadedAsset()]))) + .mockResolvedValueOnce(new Response(Buffer.from('changed bytes'))) + await expect(uploadSshRelayRuntimeDraftAssets(input(changedFetch))).rejects.toThrow( + /size|sha-?256/i + ) + }) + + it('reconciles an uncertain retry before sending the same bytes again', async () => { + const fetchImpl = vi + .fn() + .mockResolvedValueOnce(json(draft())) + .mockResolvedValueOnce(new Response('unavailable', { status: 503 })) + .mockResolvedValueOnce(json(draft([uploadedAsset()]))) + .mockResolvedValueOnce(new Response(BYTES)) + + await expect(uploadSshRelayRuntimeDraftAssets(input(fetchImpl))).resolves.toMatchObject({ + reusedAssets: [{ name: NAME, sha256: digest(), size: BYTES.length }], + uploadedAssets: [] + }) + expect(fetchImpl.mock.calls.filter(([, options]) => options.method === 'POST')).toHaveLength(1) + }) + + it('leaves a partial draft recoverable without replacing uploaded bytes', async () => { + await writeFile(join(root, SECOND_NAME), SECOND_BYTES) + const assets = [ + localAsset(), + localAsset({ + name: SECOND_NAME, + path: join(root, SECOND_NAME), + sha256: digest(SECOND_BYTES), + size: SECOND_BYTES.length + }) + ] + const firstAttempt = vi + .fn() + .mockResolvedValueOnce(json(draft())) + .mockResolvedValueOnce(json(uploadedAsset(), 201)) + .mockResolvedValueOnce(new Response('denied', { status: 403 })) + await expect(uploadSshRelayRuntimeDraftAssets(input(firstAttempt, { assets }))).rejects.toThrow( + /403/i + ) + + const recovery = vi + .fn() + .mockResolvedValueOnce(json(draft([uploadedAsset()]))) + .mockResolvedValueOnce(new Response(BYTES)) + .mockResolvedValueOnce( + json(uploadedAsset({ id: 102, name: SECOND_NAME, size: SECOND_BYTES.length }), 201) + ) + await expect( + uploadSshRelayRuntimeDraftAssets(input(recovery, { assets })) + ).resolves.toMatchObject({ + reusedAssets: [{ name: NAME, sha256: digest(), size: BYTES.length }], + uploadedAssets: [ + { name: SECOND_NAME, sha256: digest(SECOND_BYTES), size: SECOND_BYTES.length } + ] + }) + }) + + it('bounds retries and detects local mutation before another upload attempt', async () => { + const retryingFetch = vi.fn(async (url, options = {}) => + options.method === 'POST' ? new Response('unavailable', { status: 503 }) : json(draft()) + ) + const delayImpl = vi.fn(async () => {}) + await expect( + uploadSshRelayRuntimeDraftAssets(input(retryingFetch, { delayImpl })) + ).rejects.toThrow(/retry exhaustion/i) + expect( + retryingFetch.mock.calls.filter(([, options]) => options.method === 'POST') + ).toHaveLength(3) + expect(delayImpl).toHaveBeenCalledTimes(2) + + const mutationFetch = vi.fn(async (url, options = {}) => + options.method === 'POST' ? new Response('unavailable', { status: 503 }) : json(draft()) + ) + const mutateBeforeRetry = vi.fn(async () => { + await writeFile(join(root, NAME), Buffer.from('mutated relay runtime bytes')) + }) + await expect( + uploadSshRelayRuntimeDraftAssets(input(mutationFetch, { delayImpl: mutateBeforeRetry })) + ).rejects.toThrow(/local.*size|local.*sha-?256/i) + expect( + mutationFetch.mock.calls.filter(([, options]) => options.method === 'POST') + ).toHaveLength(1) + }) + + it('does not retry authorization failures and honors cancellation before requests', async () => { + const deniedFetch = vi + .fn() + .mockResolvedValueOnce(json(draft())) + .mockResolvedValueOnce(new Response('denied', { status: 403 })) + await expect(uploadSshRelayRuntimeDraftAssets(input(deniedFetch))).rejects.toThrow(/403/i) + expect(deniedFetch.mock.calls.filter(([, options]) => options.method === 'POST')).toHaveLength( + 1 + ) + + const controller = new AbortController() + controller.abort(new Error('cancel draft upload')) + const cancelledFetch = vi.fn() + await expect( + uploadSshRelayRuntimeDraftAssets(input(cancelledFetch, { signal: controller.signal })) + ).rejects.toThrow(/cancel draft upload/i) + expect(cancelledFetch).not.toHaveBeenCalled() + }) +}) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index f9dfe7cf3d6..1e997d16d96 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -133,7 +133,8 @@ describe('SSH relay runtime artifact workflow', () => { 'release-stage-gate', 'draft-recovery', 'aggregate-input', - 'draft-readback' + 'draft-readback', + 'draft-upload' ]) { const script = `ssh-relay-runtime-${moduleName}` expect(source.split(`${script}.test.mjs`)).toHaveLength(5) diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md index 379ffe5558d..1edc9902843 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 @@ -9,15 +9,17 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 4 / Work Package 3 disconnected release failure/recovery contracts — +Active checkpoint: **Milestone 4 / Work Package 3 disconnected draft-asset upload/recovery command — in progress, 2026-07-15, Codex implementation owner.** Exact-head run [29428772206](https://github.com/stablyai/orca/actions/runs/29428772206) at `bb4b527e4` passes all six target-native artifact jobs. Both Linux runners prove the complete preinstalled toolchain and skip unreachable APT; the protected manifest-workflow source contract passes on every runner. PR Checks -and Golden E2E are green. Next, add purpose-named disconnected failure contracts for timeout, retry -exhaustion, approval failure, signing failure, partial output, aggregate failure, and exact-draft -recovery. Production keys/environment/seed, publication, desktop/default consumers, tuple -enablement, and merge to `main` remain disconnected. +and Golden E2E are green. Existing release-stage and draft-recovery contracts already cover the +failure taxonomy. Their missing bounded upload side is locally green under +`E-M4-DRAFT-UPLOAD-LOCAL-RED-001` and `E-M4-DRAFT-UPLOAD-LOCAL-001`: exact same-draft reuse, +immutable local-byte verification, retry reconciliation, partial-draft recovery, and no workflow +caller. Fresh all-six native CI is required. Production keys/environment/seed, publication, +desktop/default consumers, tuple enablement, and merge to `main` remain disconnected. ## Safety status @@ -263,6 +265,9 @@ enablement, and merge to `main` remain disconnected. accepted production key, environment, or seed is provisioned. - [ ] Embed the exact signed manifest and accepted keys in each desktop build. - [ ] Upload to a draft release, read back, re-hash, and execute the downloaded archives. + A disconnected bounded upload/recovery implementation is locally green under + `E-M4-DRAFT-UPLOAD-LOCAL-001`; no real release write or downloaded archive execution has + occurred, so this item remains open. - [ ] Test timeouts, retries, approval denial, signing failure, partial output, and draft recovery. ### WP4 — Desktop resolver and verified cache 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 18a3caeae13..0ed1149d67d 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    -Current phase: Milestone 4 / Work Package 3 disconnected release failure/recovery contracts — **In progress — 2026-07-15, Codex implementation owner**. The callable aggregate and protected manifest-signing workflow contract is closed locally and on all six exact-head native jobs under E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-001 and E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001. The active package must add purpose-named disconnected failure contracts for timeout, bounded retry success/exhaustion, approval absence/denial/timeout, signing failure, aggregate failure, partial output, and exact unpublished-draft recovery without connecting credentials, publication, desktop builds, or any tuple. Release-cut, desktop builds, publication, and every tuple stay disconnected; production/default behavior is unchanged.
    +Current phase: Milestone 4 / Work Package 3 disconnected draft-asset upload/recovery command — **In progress — 2026-07-15, Codex implementation owner**. The callable aggregate and protected manifest-signing workflow contract is closed locally and on all six exact-head native jobs under E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-001 and E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001. Existing E-M4-RELEASE-DAG and E-M4-AGGREGATE-READBACK contracts already cover the declared timeout, retry, approval, failure, recovery, and returned-byte taxonomy. The missing bounded draft upload side is locally green under E-M4-DRAFT-UPLOAD-LOCAL-RED-001 and E-M4-DRAFT-UPLOAD-LOCAL-001: exact local-byte hashing, same-draft/source recovery, immutable retry reconciliation, authenticated metadata/upload calls, partial-draft recovery, injected GitHub responses, and no workflow caller or release write in tests. Fresh all-six native CI is required. Release-cut, desktop builds, publication, and every tuple stay disconnected; production/default behavior is unchanged.
    Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `bb4b527e4ea030cbd94f11ca9b6e284900ab8c8a` passes all six target-native artifact jobs in run 29428772206 under E-M4-LINUX-PREREQUISITE-CAPABILITY-CI-001 and E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001. Linux x64/arm64 capability probes complete in about four seconds, never enter APT, and proceed through full runtime construction; the all-six workflow source contract and broad runtime suites pass. Both Linux oldest-userland supplements and Windows x64 floor pass. Windows arm64 runtime smoke succeeds but the floor job retains the expected fail-closed 26200-versus-26100 mismatch. PR Checks 29428772127 and Golden E2E 29428772389 are green. The user authorizes commits, pushes, draft-PR updates, CI reruns, and protected rehearsals within PRs, but not merging to `main`. No accepted production key, protected environment/seed, native-signing trust proof, release consumer, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -106,10 +106,11 @@ same change as the work it records. pass locally and on all six exact-head native jobs under E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-001 and E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001. The environment and seed remain unprovisioned; static and test-key proof do not count as live protected signing evidence. -- Active package: Work Package 3 disconnected release failure/recovery contracts. Add a purpose-named - RED and credential-free harness for the declared timeout, retry exhaustion, approval failure, - signing failure, aggregate failure, partial-output cleanup, and same-unpublished-draft recovery - states. Keep every real credential, release write, desktop consumer, and tuple disconnected. +- Active package: Work Package 3 disconnected draft-asset upload/recovery command. The purpose-named + missing-module RED and locally green implementation are recorded under + E-M4-DRAFT-UPLOAD-LOCAL-RED-001 and E-M4-DRAFT-UPLOAD-LOCAL-001. Require exact-head all-six native + execution before closing this package. Every workflow caller, real credential, release write, + desktop consumer, and tuple remains disconnected. - 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 @@ -11230,6 +11231,72 @@ src/main/ssh/ssh-relay-manifest-signature.test.ts src/main/ssh/ssh-relay-release signing/trust, aggregate artifact passage, publication/read-back, desktop embedding, SSH behavior, or any enabled tuple. Those gates remain open and fail closed. +### E-M4-DRAFT-UPLOAD-LOCAL-RED-001 — Bounded immutable draft uploader is absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted purpose suite atop exact pushed head + `9b83971866cd5f7fc71de20a3e09ab2a120e01bd`; draft PR #8741. +- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. Local temporary + files and injected responses only; no GitHub request, release write, credential, signing service, + SSH remote, desktop consumer, publication, or enabled tuple. +- Command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +config/scripts/ssh-relay-runtime-draft-upload.test.mjs`. +- Result: expected RED. The one suite fails before collecting tests because + `ssh-relay-runtime-draft-upload.mjs` does not exist. Vitest reports 143ms; wall time is 0.90s, + maximum RSS 131,727,360 bytes, peak footprint 95,819,008 bytes, and zero swaps. +- Oracle proved: the worktree has immutable stage, recovery-planning, and read-back contracts but no + executable boundary that hashes local files, authenticates exact-draft metadata, safely uploads + missing bytes, or reconciles an uncertain POST before retry. +- Does not prove: implementation, real GitHub behavior, a release write, retry settlement, partial + draft recovery, read-back, archive execution, publication, desktop embedding, SSH behavior, or an + enabled tuple. + +### E-M4-DRAFT-UPLOAD-LOCAL-001 — Exact-byte upload and partial-draft recovery pass locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: local implementation atop exact pushed head + `9b83971866cd5f7fc71de20a3e09ab2a120e01bd`; draft PR #8741. +- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. Local temporary + files and injected GitHub API/upload/CDN responses only. No network request, real token, release + write, signer, protected environment, SSH remote, desktop consumer, publication, or enabled tuple. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 config/scripts/ssh-relay-runtime-draft-upload.test.mjs` — PASS, 1 file / 7 tests in + 174ms Vitest and 0.85s wall, with 131,776,512-byte maximum RSS, 95,983,016-byte peak footprint, + and zero swaps. +- Adjacent release command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-draft-upload.test.mjs +config/scripts/ssh-relay-runtime-draft-recovery.test.mjs +config/scripts/ssh-relay-runtime-draft-readback.test.mjs +config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs +config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 5 files / 45 tests in 1.16s Vitest and + 2.27s wall, with 132,694,016-byte maximum RSS, 96,966,128-byte peak footprint, and zero swaps. +- Broad release command: `/usr/bin/time -l sh -c "rg --files config/scripts | rg +'/ssh-relay.*[.]test[.]mjs$' | xargs pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1"` — PASS, 52 files / 269 tests in 11.39s Vitest and 13.43s wall, with + 191,807,488-byte maximum RSS and zero swaps. +- Static gates: `node --check` passes both new files. `pnpm run typecheck` passes. Full `pnpm run +lint` passes all 41 reliability gates, the 355-entry max-lines ratchet, bundled-skill verification, + localization catalog/parity, and localization coverage; all 26 warnings remain in untouched + existing files. Focused `oxfmt --check` and `git diff --check` pass. +- Oracle proved: only bounded regular files with exact declared name, path, size, SHA-256, per-file + size, aggregate size, and count may enter. The release must remain the exact ID/tag/40-hex source + commit draft. Existing managed assets are reused only after authenticated metadata and complete + downloaded-byte verification; only an approved HTTPS CDN redirect drops authorization. Missing + assets upload as bounded streams with exact length and content type. Retryable 408/429/5xx or + transport ambiguity is reconciled against downloaded draft bytes before another POST, every retry + re-hashes the local file, three-attempt exhaustion fails closed, non-retryable authorization + failure does not retry, cancellation settles, and partial drafts recover without replacing exact + already uploaded bytes. The capability has no command-line entrypoint or workflow caller. +- Does not prove: Node 24 or Windows execution, live GitHub API/upload/CDN behavior, a real token or + release write, rate-limit timing, full-size artifact throughput/memory, final authenticated + read-back, downloaded archive execution, release publication, native signing/trust, desktop + embedding, SSH transfer/install, packaged RPC behavior, performance against legacy, or an enabled + tuple. +- Follow-up: checkpoint and push, then require exact-head syntax and seven-test execution on all six + native artifact jobs plus PR Checks and Golden E2E. Retain all production/default disconnections. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -11288,13 +11355,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Checkpoint and push E-M4-LINUX-PREREQUISITE-CAPABILITY-CI-001 and -E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001. Then begin a purpose-named disconnected RED for the -release failure/recovery harness: timeout, bounded retry success/exhaustion, approval -absence/denial/timeout, signing failure, aggregate failure, partial-output cleanup, and exact -unpublished-draft recovery. Reuse the reviewed budgets and same-draft identity constraints; do not -connect credentials, release writes, desktop consumers, or tuples. The protected environment, -accepted production keys, and secret remain unprovisioned, so live signing evidence stays BLOCKED. +Checkpoint and push the locally green E-M4-DRAFT-UPLOAD-LOCAL-001 implementation, then require +exact-head syntax and seven-test execution on all six native artifact jobs plus PR Checks and Golden +E2E. Do not add a command-line entrypoint or workflow caller and do not perform a real release write. +The protected environment, accepted production keys, and secret remain unprovisioned, so live +signing evidence stays BLOCKED. Keep Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. Do not merge to `main`; retain every production/default gate. From b37cacecf8f1c865f09c118f274ab62302e951ad Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 12:13:15 -0400 Subject: [PATCH 137/267] fix(ssh): bind relay drafts to exact tags Co-authored-by: Orca --- .../ssh-relay-runtime-draft-upload.mjs | 62 ++++++++++++++--- .../ssh-relay-runtime-draft-upload.test.mjs | 68 +++++++++++++++---- ...elease-implementation-checklist-summary.md | 7 +- ...github-release-implementation-checklist.md | 41 +++++------ 4 files changed, 135 insertions(+), 43 deletions(-) diff --git a/config/scripts/ssh-relay-runtime-draft-upload.mjs b/config/scripts/ssh-relay-runtime-draft-upload.mjs index 161701bc0c7..70bfa23bf6d 100644 --- a/config/scripts/ssh-relay-runtime-draft-upload.mjs +++ b/config/scripts/ssh-relay-runtime-draft-upload.mjs @@ -116,7 +116,7 @@ async function verifyLocalAsset(asset, signal) { } } -function validateDraft(release, expected, { releaseId, tag, sourceCommit }) { +function validateDraft(release, expected, { releaseId, tag }) { assertObject(release, 'release') if (release.id !== releaseId || release.draft !== true) { throw new Error('SSH relay runtime release must remain the requested draft during upload') @@ -124,9 +124,6 @@ function validateDraft(release, expected, { releaseId, tag, sourceCommit }) { if (release.tag_name !== tag) { throw new Error('SSH relay runtime draft upload tag does not match') } - if (release.target_commitish !== sourceCommit) { - throw new Error('SSH relay runtime draft upload source commit does not match') - } if (!Array.isArray(release.assets)) { throw new Error('SSH relay runtime draft upload release assets must be an array') } @@ -145,15 +142,62 @@ function validateDraft(release, expected, { releaseId, tag, sourceCommit }) { return existing } +async function fetchJson(context, url, signal) { + const response = await context.fetchImpl(url, { + headers: authenticatedHeaders(undefined, context.token), + redirect: 'error', + signal + }) + if (!response.ok) { + throw await responseError(response) + } + return response.json() +} + +async function resolveTagCommit(context, signal) { + const reference = await fetchJson( + context, + `https://api.github.com/repos/${context.repo}/git/ref/tags/${encodeURIComponent(context.tag)}`, + signal + ) + let object = reference?.object + const seen = new Set() + for (let depth = 0; depth < 5; depth += 1) { + if ( + !object || + typeof object !== 'object' || + typeof object.sha !== 'string' || + !COMMIT_PATTERN.test(object.sha) || + (object.type !== 'commit' && object.type !== 'tag') || + seen.has(object.sha) + ) { + throw new Error('SSH relay runtime draft upload tag reference is invalid') + } + if (object.type === 'commit') { + return object.sha + } + seen.add(object.sha) + const tag = await fetchJson( + context, + `https://api.github.com/repos/${context.repo}/git/tags/${object.sha}`, + signal + ) + object = tag?.object + } + throw new Error('SSH relay runtime draft upload annotated tag depth exceeded') +} + async function fetchDraft(context, signal) { - const response = await context.fetchImpl( + const release = await fetchJson( + context, `https://api.github.com/repos/${context.repo}/releases/${context.releaseId}`, - { headers: authenticatedHeaders(undefined, context.token), redirect: 'error', signal } + signal ) - if (!response.ok) { - throw await responseError(response) + const existing = validateDraft(release, context.assets, context) + if ((await resolveTagCommit(context, signal)) !== context.sourceCommit) { + throw new Error('SSH relay runtime draft upload source commit does not match the tag') } - return validateDraft(await response.json(), context.assets, context) + return existing } async function fetchExistingAsset(context, remote, signal) { diff --git a/config/scripts/ssh-relay-runtime-draft-upload.test.mjs b/config/scripts/ssh-relay-runtime-draft-upload.test.mjs index 9f598bd7809..3de52f73284 100644 --- a/config/scripts/ssh-relay-runtime-draft-upload.test.mjs +++ b/config/scripts/ssh-relay-runtime-draft-upload.test.mjs @@ -37,13 +37,17 @@ function draft(assets = [], overrides = {}) { return { id: RELEASE_ID, tag_name: TAG, - target_commitish: SOURCE_COMMIT, + target_commitish: 'main', draft: true, assets, ...overrides } } +function tagReference(sha = SOURCE_COMMIT, type = 'commit') { + return { object: { sha, type } } +} + function uploadedAsset(overrides = {}) { return { id: 101, name: NAME, state: 'uploaded', size: BYTES.length, ...overrides } } @@ -92,6 +96,9 @@ describe('SSH relay runtime draft upload', () => { bodies.push(await requestBytes(options)) return json(uploadedAsset(), 201) } + if (url.includes('/git/ref/tags/')) { + return json(tagReference()) + } return json(draft()) }) @@ -119,6 +126,7 @@ describe('SSH relay runtime draft upload', () => { const fetchImpl = vi .fn() .mockResolvedValueOnce(json(draft([uploadedAsset()]))) + .mockResolvedValueOnce(json(tagReference())) .mockResolvedValueOnce(new Response(null, { status: 302, headers: { location } })) .mockResolvedValueOnce(new Response(BYTES)) @@ -126,18 +134,34 @@ describe('SSH relay runtime draft upload', () => { reusedAssets: [{ name: NAME, sha256: digest(), size: BYTES.length }], uploadedAssets: [] }) - expect(fetchImpl).toHaveBeenCalledTimes(3) + expect(fetchImpl).toHaveBeenCalledTimes(4) expect(fetchImpl.mock.calls.some(([, options]) => options.method === 'POST')).toBe(false) - expect(fetchImpl.mock.calls[1][1].headers.Authorization).toBe(`Bearer ${TOKEN}`) - expect(fetchImpl.mock.calls[2][0]).toBe(location) - expect(fetchImpl.mock.calls[2][1].headers).not.toHaveProperty('Authorization') + expect(fetchImpl.mock.calls[2][1].headers.Authorization).toBe(`Bearer ${TOKEN}`) + expect(fetchImpl.mock.calls[3][0]).toBe(location) + expect(fetchImpl.mock.calls[3][1].headers).not.toHaveProperty('Authorization') + }) + + it('peels annotated tags to the exact source commit', async () => { + const tagSha = 'b'.repeat(40) + const fetchImpl = vi + .fn() + .mockResolvedValueOnce(json(draft())) + .mockResolvedValueOnce(json(tagReference(tagSha, 'tag'))) + .mockResolvedValueOnce(json(tagReference())) + .mockResolvedValueOnce(json(uploadedAsset(), 201)) + + await expect(uploadSshRelayRuntimeDraftAssets(input(fetchImpl))).resolves.toMatchObject({ + uploadedAssets: [{ name: NAME, sha256: digest(), size: BYTES.length }] + }) + expect(fetchImpl.mock.calls[2][0]).toBe( + `https://api.github.com/repos/${REPO}/git/tags/${tagSha}` + ) }) it('fails closed on unsafe draft state or existing managed bytes', async () => { for (const release of [ draft([], { draft: false }), draft([], { tag_name: 'v1.5.0-rc.2' }), - draft([], { target_commitish: 'b'.repeat(40) }), draft([uploadedAsset({ name: 'orca-ssh-relay-runtime-unexpected.zip' })]) ]) { await expect( @@ -145,9 +169,18 @@ describe('SSH relay runtime draft upload', () => { ).rejects.toThrow(/draft|tag|source commit|unexpected managed asset/i) } + const wrongSourceFetch = vi + .fn() + .mockResolvedValueOnce(json(draft())) + .mockResolvedValueOnce(json(tagReference('b'.repeat(40)))) + await expect(uploadSshRelayRuntimeDraftAssets(input(wrongSourceFetch))).rejects.toThrow( + /source commit.*tag/i + ) + const changedFetch = vi .fn() .mockResolvedValueOnce(json(draft([uploadedAsset()]))) + .mockResolvedValueOnce(json(tagReference())) .mockResolvedValueOnce(new Response(Buffer.from('changed bytes'))) await expect(uploadSshRelayRuntimeDraftAssets(input(changedFetch))).rejects.toThrow( /size|sha-?256/i @@ -158,8 +191,10 @@ describe('SSH relay runtime draft upload', () => { const fetchImpl = vi .fn() .mockResolvedValueOnce(json(draft())) + .mockResolvedValueOnce(json(tagReference())) .mockResolvedValueOnce(new Response('unavailable', { status: 503 })) .mockResolvedValueOnce(json(draft([uploadedAsset()]))) + .mockResolvedValueOnce(json(tagReference())) .mockResolvedValueOnce(new Response(BYTES)) await expect(uploadSshRelayRuntimeDraftAssets(input(fetchImpl))).resolves.toMatchObject({ @@ -183,6 +218,7 @@ describe('SSH relay runtime draft upload', () => { const firstAttempt = vi .fn() .mockResolvedValueOnce(json(draft())) + .mockResolvedValueOnce(json(tagReference())) .mockResolvedValueOnce(json(uploadedAsset(), 201)) .mockResolvedValueOnce(new Response('denied', { status: 403 })) await expect(uploadSshRelayRuntimeDraftAssets(input(firstAttempt, { assets }))).rejects.toThrow( @@ -192,6 +228,7 @@ describe('SSH relay runtime draft upload', () => { const recovery = vi .fn() .mockResolvedValueOnce(json(draft([uploadedAsset()]))) + .mockResolvedValueOnce(json(tagReference())) .mockResolvedValueOnce(new Response(BYTES)) .mockResolvedValueOnce( json(uploadedAsset({ id: 102, name: SECOND_NAME, size: SECOND_BYTES.length }), 201) @@ -207,9 +244,12 @@ describe('SSH relay runtime draft upload', () => { }) it('bounds retries and detects local mutation before another upload attempt', async () => { - const retryingFetch = vi.fn(async (url, options = {}) => - options.method === 'POST' ? new Response('unavailable', { status: 503 }) : json(draft()) - ) + const retryingFetch = vi.fn(async (url, options = {}) => { + if (options.method === 'POST') { + return new Response('unavailable', { status: 503 }) + } + return url.includes('/git/ref/tags/') ? json(tagReference()) : json(draft()) + }) const delayImpl = vi.fn(async () => {}) await expect( uploadSshRelayRuntimeDraftAssets(input(retryingFetch, { delayImpl })) @@ -219,9 +259,12 @@ describe('SSH relay runtime draft upload', () => { ).toHaveLength(3) expect(delayImpl).toHaveBeenCalledTimes(2) - const mutationFetch = vi.fn(async (url, options = {}) => - options.method === 'POST' ? new Response('unavailable', { status: 503 }) : json(draft()) - ) + const mutationFetch = vi.fn(async (url, options = {}) => { + if (options.method === 'POST') { + return new Response('unavailable', { status: 503 }) + } + return url.includes('/git/ref/tags/') ? json(tagReference()) : json(draft()) + }) const mutateBeforeRetry = vi.fn(async () => { await writeFile(join(root, NAME), Buffer.from('mutated relay runtime bytes')) }) @@ -237,6 +280,7 @@ describe('SSH relay runtime draft upload', () => { const deniedFetch = vi .fn() .mockResolvedValueOnce(json(draft())) + .mockResolvedValueOnce(json(tagReference())) .mockResolvedValueOnce(new Response('denied', { status: 403 })) await expect(uploadSshRelayRuntimeDraftAssets(input(deniedFetch))).rejects.toThrow(/403/i) expect(deniedFetch.mock.calls.filter(([, options]) => options.method === 'POST')).toHaveLength( 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 1edc9902843..ba1ae0e3db1 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 @@ -17,9 +17,10 @@ unreachable APT; the protected manifest-workflow source contract passes on every and Golden E2E are green. Existing release-stage and draft-recovery contracts already cover the failure taxonomy. Their missing bounded upload side is locally green under `E-M4-DRAFT-UPLOAD-LOCAL-RED-001` and `E-M4-DRAFT-UPLOAD-LOCAL-001`: exact same-draft reuse, -immutable local-byte verification, retry reconciliation, partial-draft recovery, and no workflow -caller. Fresh all-six native CI is required. Production keys/environment/seed, publication, -desktop/default consumers, tuple enablement, and merge to `main` remain disconnected. +immutable local-byte verification, authenticated lightweight/annotated tag-to-source binding, retry +reconciliation, partial-draft recovery, and no workflow caller. Fresh all-six native CI is required. +Production keys/environment/seed, publication, desktop/default consumers, tuple enablement, and +merge to `main` remain disconnected. ## Safety status 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 0ed1149d67d..9740ea00665 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    -Current phase: Milestone 4 / Work Package 3 disconnected draft-asset upload/recovery command — **In progress — 2026-07-15, Codex implementation owner**. The callable aggregate and protected manifest-signing workflow contract is closed locally and on all six exact-head native jobs under E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-001 and E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001. Existing E-M4-RELEASE-DAG and E-M4-AGGREGATE-READBACK contracts already cover the declared timeout, retry, approval, failure, recovery, and returned-byte taxonomy. The missing bounded draft upload side is locally green under E-M4-DRAFT-UPLOAD-LOCAL-RED-001 and E-M4-DRAFT-UPLOAD-LOCAL-001: exact local-byte hashing, same-draft/source recovery, immutable retry reconciliation, authenticated metadata/upload calls, partial-draft recovery, injected GitHub responses, and no workflow caller or release write in tests. Fresh all-six native CI is required. Release-cut, desktop builds, publication, and every tuple stay disconnected; production/default behavior is unchanged.
    +Current phase: Milestone 4 / Work Package 3 disconnected draft-asset upload/recovery command — **In progress — 2026-07-15, Codex implementation owner**. The callable aggregate and protected manifest-signing workflow contract is closed locally and on all six exact-head native jobs under E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-001 and E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001. Existing E-M4-RELEASE-DAG and E-M4-AGGREGATE-READBACK contracts already cover the declared timeout, retry, approval, failure, recovery, and returned-byte taxonomy. The missing bounded draft upload side is locally green under E-M4-DRAFT-UPLOAD-LOCAL-RED-001 and E-M4-DRAFT-UPLOAD-LOCAL-001: exact local-byte hashing, same-draft recovery, authenticated lightweight/annotated tag-to-source binding, immutable retry reconciliation, authenticated metadata/upload calls, partial-draft recovery, injected GitHub responses, and no workflow caller or release write in tests. Fresh all-six native CI is required. Release-cut, desktop builds, publication, and every tuple stay disconnected; production/default behavior is unchanged.
    Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `bb4b527e4ea030cbd94f11ca9b6e284900ab8c8a` passes all six target-native artifact jobs in run 29428772206 under E-M4-LINUX-PREREQUISITE-CAPABILITY-CI-001 and E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001. Linux x64/arm64 capability probes complete in about four seconds, never enter APT, and proceed through full runtime construction; the all-six workflow source contract and broad runtime suites pass. Both Linux oldest-userland supplements and Windows x64 floor pass. Windows arm64 runtime smoke succeeds but the floor job retains the expected fail-closed 26200-versus-26100 mismatch. PR Checks 29428772127 and Golden E2E 29428772389 are green. The user authorizes commits, pushes, draft-PR updates, CI reruns, and protected rehearsals within PRs, but not merging to `main`. No accepted production key, protected environment/seed, native-signing trust proof, release consumer, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -11256,45 +11256,48 @@ config/scripts/ssh-relay-runtime-draft-upload.test.mjs`. - Date: 2026-07-15 - Owner: Codex implementation owner -- Source: local implementation atop exact pushed head - `9b83971866cd5f7fc71de20a3e09ab2a120e01bd`; draft PR #8741. +- Source: implementation commit `8d9013cd106734ecd979f785b515dd9640841330` plus the local + authenticated tag-binding correction; draft PR #8741. - Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. Local temporary files and injected GitHub API/upload/CDN responses only. No network request, real token, release write, signer, protected environment, SSH remote, desktop consumer, publication, or enabled tuple. - Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts ---maxWorkers=1 config/scripts/ssh-relay-runtime-draft-upload.test.mjs` — PASS, 1 file / 7 tests in - 174ms Vitest and 0.85s wall, with 131,776,512-byte maximum RSS, 95,983,016-byte peak footprint, +--maxWorkers=1 config/scripts/ssh-relay-runtime-draft-upload.test.mjs` — PASS, 1 file / 8 tests in + 183ms Vitest and 0.87s wall, with 131,694,592-byte maximum RSS, 95,868,280-byte peak footprint, and zero swaps. - Adjacent release command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-draft-upload.test.mjs config/scripts/ssh-relay-runtime-draft-recovery.test.mjs config/scripts/ssh-relay-runtime-draft-readback.test.mjs config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs -config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 5 files / 45 tests in 1.16s Vitest and - 2.27s wall, with 132,694,016-byte maximum RSS, 96,966,128-byte peak footprint, and zero swaps. + config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 5 files / 46 tests in 1.80s Vitest and + 3.44s wall, with 131,940,352-byte maximum RSS, 96,179,648-byte peak footprint, and zero swaps. - Broad release command: `/usr/bin/time -l sh -c "rg --files config/scripts | rg '/ssh-relay.*[.]test[.]mjs$' | xargs pnpm exec vitest run --config config/vitest.config.ts ---maxWorkers=1"` — PASS, 52 files / 269 tests in 11.39s Vitest and 13.43s wall, with - 191,807,488-byte maximum RSS and zero swaps. + --maxWorkers=1"` — PASS, 52 files / 270 tests in 11.71s Vitest and 13.66s wall, with + 188,841,984-byte maximum RSS and zero swaps. - Static gates: `node --check` passes both new files. `pnpm run typecheck` passes. Full `pnpm run lint` passes all 41 reliability gates, the 355-entry max-lines ratchet, bundled-skill verification, localization catalog/parity, and localization coverage; all 26 warnings remain in untouched existing files. Focused `oxfmt --check` and `git diff --check` pass. - Oracle proved: only bounded regular files with exact declared name, path, size, SHA-256, per-file - size, aggregate size, and count may enter. The release must remain the exact ID/tag/40-hex source - commit draft. Existing managed assets are reused only after authenticated metadata and complete - downloaded-byte verification; only an approved HTTPS CDN redirect drops authorization. Missing - assets upload as bounded streams with exact length and content type. Retryable 408/429/5xx or - transport ambiguity is reconciled against downloaded draft bytes before another POST, every retry - re-hashes the local file, three-attempt exhaustion fails closed, non-retryable authorization - failure does not retry, cancellation settles, and partial drafts recover without replacing exact - already uploaded bytes. The capability has no command-line entrypoint or workflow caller. + size, aggregate size, and count may enter. The release must remain the exact ID/tag draft, and its + authenticated lightweight or recursively peeled annotated tag must resolve to the exact 40-hex + source commit; `target_commitish` is not trusted because existing-tag release creation does not + reliably preserve that SHA there. Existing managed assets are reused only after authenticated + metadata and complete downloaded-byte verification; only an approved HTTPS CDN redirect drops + authorization. Missing assets upload as bounded streams with exact length and content type. + Retryable 408/429/5xx or transport ambiguity is reconciled against downloaded draft bytes before + another POST, every retry re-hashes the local file, three-attempt exhaustion fails closed, + non-retryable authorization failure does not retry, cancellation settles, and partial drafts + recover without replacing exact already uploaded bytes. The capability has no command-line + entrypoint or workflow caller. - Does not prove: Node 24 or Windows execution, live GitHub API/upload/CDN behavior, a real token or release write, rate-limit timing, full-size artifact throughput/memory, final authenticated read-back, downloaded archive execution, release publication, native signing/trust, desktop embedding, SSH transfer/install, packaged RPC behavior, performance against legacy, or an enabled tuple. -- Follow-up: checkpoint and push, then require exact-head syntax and seven-test execution on all six +- Follow-up: checkpoint and push, then require exact-head syntax and eight-test execution on all six native artifact jobs plus PR Checks and Golden E2E. Retain all production/default disconnections. ## Accepted Gaps @@ -11356,7 +11359,7 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action Checkpoint and push the locally green E-M4-DRAFT-UPLOAD-LOCAL-001 implementation, then require -exact-head syntax and seven-test execution on all six native artifact jobs plus PR Checks and Golden +exact-head syntax and eight-test execution on all six native artifact jobs plus PR Checks and Golden E2E. Do not add a command-line entrypoint or workflow caller and do not perform a real release write. The protected environment, accepted production keys, and secret remain unprovisioned, so live signing evidence stays BLOCKED. From 88a77eee91a34f8936882d096e568f499e9160e6 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 12:34:24 -0400 Subject: [PATCH 138/267] docs: record relay draft upload CI proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 24 +++--- ...github-release-implementation-checklist.md | 85 ++++++++++++++++--- 2 files changed, 82 insertions(+), 27 deletions(-) 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 ba1ae0e3db1..c1a3d733041 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 @@ -9,18 +9,15 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 4 / Work Package 3 disconnected draft-asset upload/recovery command — +Active checkpoint: **Milestone 4 / Work Package 3 disconnected relay required-asset gate — in progress, 2026-07-15, Codex implementation owner.** Exact-head run -[29428772206](https://github.com/stablyai/orca/actions/runs/29428772206) at `bb4b527e4` passes all six -target-native artifact jobs. Both Linux runners prove the complete preinstalled toolchain and skip -unreachable APT; the protected manifest-workflow source contract passes on every runner. PR Checks -and Golden E2E are green. Existing release-stage and draft-recovery contracts already cover the -failure taxonomy. Their missing bounded upload side is locally green under -`E-M4-DRAFT-UPLOAD-LOCAL-RED-001` and `E-M4-DRAFT-UPLOAD-LOCAL-001`: exact same-draft reuse, -immutable local-byte verification, authenticated lightweight/annotated tag-to-source binding, retry -reconciliation, partial-draft recovery, and no workflow caller. Fresh all-six native CI is required. -Production keys/environment/seed, publication, desktop/default consumers, tuple enablement, and -merge to `main` remain disconnected. +[29431510990](https://github.com/stablyai/orca/actions/runs/29431510990) at `b37cacecf` passes all six +target-native artifact jobs and the uploader's eight-test suite on every Node 24 runner under +`E-M4-DRAFT-UPLOAD-CI-001`. PR Checks and Golden E2E are green; the artifact workflow is red only for +the retained Windows ARM build-26200-versus-26100 floor mismatch after successful runtime smoke. +Next, add the disconnected relay-specific release required-asset gate for exact manifest coverage, +names, sizes, signature, upload state, and channel identity. Production keys/environment/seed, +publication, desktop/default consumers, tuple enablement, and merge to `main` remain disconnected. ## Safety status @@ -267,8 +264,9 @@ merge to `main` remain disconnected. - [ ] Embed the exact signed manifest and accepted keys in each desktop build. - [ ] Upload to a draft release, read back, re-hash, and execute the downloaded archives. A disconnected bounded upload/recovery implementation is locally green under - `E-M4-DRAFT-UPLOAD-LOCAL-001`; no real release write or downloaded archive execution has - occurred, so this item remains open. + `E-M4-DRAFT-UPLOAD-LOCAL-001` and passes all six native Node 24 jobs under + `E-M4-DRAFT-UPLOAD-CI-001`; no real release write or downloaded archive execution has occurred, + so this item remains open. - [ ] Test timeouts, retries, approval denial, signing failure, partial output, and draft recovery. ### WP4 — Desktop resolver and verified cache 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 9740ea00665..8efefa5e629 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md @@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    -Current phase: Milestone 4 / Work Package 3 disconnected draft-asset upload/recovery command — **In progress — 2026-07-15, Codex implementation owner**. The callable aggregate and protected manifest-signing workflow contract is closed locally and on all six exact-head native jobs under E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-001 and E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001. Existing E-M4-RELEASE-DAG and E-M4-AGGREGATE-READBACK contracts already cover the declared timeout, retry, approval, failure, recovery, and returned-byte taxonomy. The missing bounded draft upload side is locally green under E-M4-DRAFT-UPLOAD-LOCAL-RED-001 and E-M4-DRAFT-UPLOAD-LOCAL-001: exact local-byte hashing, same-draft recovery, authenticated lightweight/annotated tag-to-source binding, immutable retry reconciliation, authenticated metadata/upload calls, partial-draft recovery, injected GitHub responses, and no workflow caller or release write in tests. Fresh all-six native CI is required. Release-cut, desktop builds, publication, and every tuple stay disconnected; production/default behavior is unchanged.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `bb4b527e4ea030cbd94f11ca9b6e284900ab8c8a` passes all six target-native artifact jobs in run 29428772206 under E-M4-LINUX-PREREQUISITE-CAPABILITY-CI-001 and E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001. Linux x64/arm64 capability probes complete in about four seconds, never enter APT, and proceed through full runtime construction; the all-six workflow source contract and broad runtime suites pass. Both Linux oldest-userland supplements and Windows x64 floor pass. Windows arm64 runtime smoke succeeds but the floor job retains the expected fail-closed 26200-versus-26100 mismatch. PR Checks 29428772127 and Golden E2E 29428772389 are green. The user authorizes commits, pushes, draft-PR updates, CI reruns, and protected rehearsals within PRs, but not merging to `main`. No accepted production key, protected environment/seed, native-signing trust proof, release consumer, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
    +Current phase: Milestone 4 / Work Package 3 disconnected relay required-asset gate — **In progress — 2026-07-15, Codex implementation owner**. The bounded draft upload/recovery capability is closed locally and on all six exact-head native jobs under E-M4-DRAFT-UPLOAD-LOCAL-RED-001, E-M4-DRAFT-UPLOAD-LOCAL-001, and E-M4-DRAFT-UPLOAD-CI-001. The active package must add a disconnected relay-specific release gate that consumes only an already verified signed manifest plus authenticated draft metadata and rejects missing, extra managed, non-uploaded, empty, wrong-size, uncovered, cross-tag/channel, or signature-disagreeing assets. Keep the existing desktop required-asset gate unchanged until composition is explicitly reviewed. Release-cut, desktop builds, publication, and every tuple stay disconnected; production/default behavior is unchanged.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `b37cacecf8f1c865f09c118f274ab62302e951ad` passes all six target-native artifact jobs and the eight-test uploader suite in run 29431510990 under E-M4-DRAFT-UPLOAD-CI-001. Both Linux oldest-userland supplements and Windows x64 floor pass. Windows arm64 runtime smoke passes in 7,752.6288ms with 49,684,480-byte RSS, then its floor job retains the expected fail-closed hosted-build 26200 versus required 26100 mismatch. PR Checks 29431510764 and Golden E2E 29431510883 are green. The user authorizes commits, pushes, draft-PR updates, CI reruns, and protected rehearsals within PRs, but not merging to `main`. No accepted production key, protected environment/seed, native-signing trust proof, release write, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
    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) @@ -106,11 +106,15 @@ same change as the work it records. pass locally and on all six exact-head native jobs under E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-001 and E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001. The environment and seed remain unprovisioned; static and test-key proof do not count as live protected signing evidence. -- Active package: Work Package 3 disconnected draft-asset upload/recovery command. The purpose-named - missing-module RED and locally green implementation are recorded under - E-M4-DRAFT-UPLOAD-LOCAL-RED-001 and E-M4-DRAFT-UPLOAD-LOCAL-001. Require exact-head all-six native - execution before closing this package. Every workflow caller, real credential, release write, - desktop consumer, and tuple remains disconnected. +- Completed Work Package 3 gate: disconnected draft-asset upload/recovery capability. Exact local + bytes, authenticated tag/source binding, safe reuse, bounded retry reconciliation, cancellation, + and partial-draft recovery pass locally and on all six exact-head native jobs under + E-M4-DRAFT-UPLOAD-LOCAL-RED-001, E-M4-DRAFT-UPLOAD-LOCAL-001, and E-M4-DRAFT-UPLOAD-CI-001. There + is no CLI entrypoint or workflow caller, and no real release write occurred. +- Active package: Work Package 3 disconnected relay required-asset gate. Begin with a purpose-named + missing-module RED for exact verified-manifest coverage, signature, name, size, upload state, + managed-asset closure, tag/channel identity, and consumer disconnection. Keep the existing desktop + required-asset gate unchanged in this slice. - 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 @@ -11256,8 +11260,8 @@ config/scripts/ssh-relay-runtime-draft-upload.test.mjs`. - Date: 2026-07-15 - Owner: Codex implementation owner -- Source: implementation commit `8d9013cd106734ecd979f785b515dd9640841330` plus the local - authenticated tag-binding correction; draft PR #8741. +- Source: implementation commits `8d9013cd106734ecd979f785b515dd9640841330` and + `b37cacecf8f1c865f09c118f274ab62302e951ad`; draft PR #8741. - Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. Local temporary files and injected GitHub API/upload/CDN responses only. No network request, real token, release write, signer, protected environment, SSH remote, desktop consumer, publication, or enabled tuple. @@ -11300,6 +11304,57 @@ lint` passes all 41 reliability gates, the 355-entry max-lines ratchet, bundled- - Follow-up: checkpoint and push, then require exact-head syntax and eight-test execution on all six native artifact jobs plus PR Checks and Golden E2E. Retain all production/default disconnections. +### E-M4-DRAFT-UPLOAD-CI-001 — Draft upload/recovery passes all six native Node 24 jobs + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `b37cacecf8f1c865f09c118f274ab62302e951ad`; draft PR #8741. +- Workflow: [SSH Relay Runtime Artifacts run 29431510990](https://github.com/stablyai/orca/actions/runs/29431510990), + 16:13:32Z–16:30:34Z. The aggregate conclusion is the expected failure only because the separate + Windows arm64 floor job rejects hosted build 26200 after successful runtime execution. +- Native build jobs and durations: + - Darwin x64 job 87407470012, `macos-15` image `20260629.0276.1`, runner 2.335.1, Azure `westus`, + 16:13:52Z–16:20:58Z, PASS in 7m06s. + - Darwin arm64 job 87407470027, `macos-15-arm64` image `20260706.0213.1`, runner 2.335.1, Azure + `westus`, 16:13:50Z–16:17:32Z, PASS in 3m42s. + - Linux arm64 job 87407470041, `ubuntu-24.04-arm` image `20260706.52.2`, runner 2.335.1, Azure + `northcentralus`, 16:13:53Z–16:18:11Z, PASS in 4m18s. + - Windows x64 job 87407470042, `windows-2022` image `20260706.237.1`, runner 2.335.1, Azure + `westus`, 16:13:51Z–16:19:32Z, PASS in 5m41s. + - Linux x64 job 87407470091, `ubuntu-24.04` image `20260714.240.1`, runner 2.335.1, Azure + `westus3`, 16:13:50Z–16:17:27Z, PASS in 3m37s. + - Windows arm64 job 87407470316, `windows-11-arm64` image `20260706.102.1`, runner 2.335.1, + Azure `northcentralus`, 16:13:51Z–16:24:34Z, PASS in 10m43s. +- Exact uploader suite: `config/scripts/ssh-relay-runtime-draft-upload.test.mjs` passes 8/8 under + Node 24.18.0 on every native job: Darwin x64 260ms, Darwin arm64 51ms, Linux arm64 78ms, Windows + x64 151ms, Linux x64 89ms, and Windows arm64 192ms. +- Broad contract counts: every POSIX job passes 51 files / 266 tests; Vitest durations are Darwin + x64 24.06s, Darwin arm64 8.27s, Linux arm64 6.59s, and Linux x64 6.43s. Each Windows job passes 52 + files / 260 tests plus 10 platform skips of 270; x64 takes 12.74s and arm64 17.32s. +- Downstream evidence: Linux arm64 supplement job 87409169300 passes in 54s; Linux x64 supplement + 87409169317 passes in 51s; Windows x64 floor job 87410005329 passes in 1m27s. Windows arm64 floor + job 87410005404 uses `windows-11-arm64` image `20260714.109.1`, executes the artifact successfully + in 7,752.6288ms with 49,684,480-byte RSS, and then fails exactly because OS/kernel build 26200 is + not required floor 26100. +- Adjacent exact-head workflows: PR Checks run + [29431510764](https://github.com/stablyai/orca/actions/runs/29431510764), job 87407413500, passes + 16:13:36Z–16:29:15Z, including lint, typecheck, Git compatibility, tests, unpacked build, and + packaged CLI smoke. Golden E2E run + [29431510883](https://github.com/stablyai/orca/actions/runs/29431510883) passes Linux job + 87407414410 and macOS job 87407414424. +- Oracle proved: the uploader module, tests, and POSIX/PowerShell workflow wiring parse and execute + consistently on all six native Node 24 runner families. Every family exercises regular-file + hashing, exact draft/tag/source binding including annotated-tag peeling, authenticated redirect + isolation, exact reuse, uncertain-POST reconciliation, retry exhaustion, mutation detection, + authorization failure, cancellation, and partial-draft recovery. Full native artifact construction + and adjacent regressions remain green. +- Does not prove: live GitHub API/upload/CDN behavior, a real token or release write, full-size upload + throughput/memory, a real retry/429, final read-back, downloaded archive execution, publication, + native signing/trust, oldest macOS/Linux kernel/Windows arm64 floor, desktop embedding, SSH + transfer/install, packaged RPC behavior, performance against legacy, or any enabled tuple. +- Checklist items satisfied: exact-head all-six CI gate for the disconnected draft upload/recovery + capability. The broader draft upload/read-back/execution and Work Package 3 boxes remain open. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -11358,11 +11413,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Checkpoint and push the locally green E-M4-DRAFT-UPLOAD-LOCAL-001 implementation, then require -exact-head syntax and eight-test execution on all six native artifact jobs plus PR Checks and Golden -E2E. Do not add a command-line entrypoint or workflow caller and do not perform a real release write. -The protected environment, accepted production keys, and secret remain unprovisioned, so live -signing evidence stays BLOCKED. +Checkpoint and push E-M4-DRAFT-UPLOAD-CI-001. Then begin a purpose-named disconnected RED for the +relay-specific release required-asset gate: consume only an already verified signed manifest and +authenticated draft metadata; require exact manifest/signature coverage, asset names, sizes, upload +state, managed-asset closure, and stable/RC/perf tag identity. Keep the existing desktop +`verify-release-required-assets.mjs` behavior unchanged until composition is separately reviewed. +Do not add a workflow caller or perform a real release write. The protected environment, accepted +production keys, and secret remain unprovisioned, so live signing evidence stays BLOCKED. Keep Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. Do not merge to `main`; retain every production/default gate. From 5460563ff48846cbf54218860fdca1cc3c7b8614 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 12:46:20 -0400 Subject: [PATCH 139/267] build(ssh): add relay release asset gate Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 8 + .../ssh-relay-runtime-release-assets.mjs | 289 ++++++++++++++++++ .../ssh-relay-runtime-release-assets.test.mjs | 193 ++++++++++++ .../ssh-relay-runtime-workflow.test.mjs | 3 +- ...elease-implementation-checklist-summary.md | 12 +- ...github-release-implementation-checklist.md | 106 ++++++- 6 files changed, 596 insertions(+), 15 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-release-assets.mjs create mode 100644 config/scripts/ssh-relay-runtime-release-assets.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 37d2c741edd..bab4b0d3517 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -220,6 +220,8 @@ jobs: node --check config/scripts/ssh-relay-runtime-draft-readback.test.mjs node --check config/scripts/ssh-relay-runtime-draft-upload.mjs node --check config/scripts/ssh-relay-runtime-draft-upload.test.mjs + node --check config/scripts/ssh-relay-runtime-release-assets.mjs + node --check config/scripts/ssh-relay-runtime-release-assets.test.mjs node --check config/scripts/ssh-relay-runtime-manifest-validation.mjs node --check config/scripts/ssh-relay-runtime-manifest-assembly.mjs node --check config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs @@ -276,6 +278,7 @@ jobs: config/scripts/ssh-relay-runtime-aggregate-input.test.mjs \ config/scripts/ssh-relay-runtime-draft-readback.test.mjs \ config/scripts/ssh-relay-runtime-draft-upload.test.mjs \ + config/scripts/ssh-relay-runtime-release-assets.test.mjs \ config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs \ config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs \ config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs \ @@ -641,6 +644,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime draft upload syntax check failed' } node --check config/scripts/ssh-relay-runtime-draft-upload.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime draft upload test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-release-assets.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime release assets syntax check failed' } + node --check config/scripts/ssh-relay-runtime-release-assets.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime release assets test syntax check failed' } node --check config/scripts/ssh-relay-runtime-manifest-validation.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime manifest validation syntax check failed' } node --check config/scripts/ssh-relay-runtime-manifest-assembly.mjs @@ -726,6 +733,7 @@ jobs: config/scripts/ssh-relay-runtime-aggregate-input.test.mjs ` config/scripts/ssh-relay-runtime-draft-readback.test.mjs ` config/scripts/ssh-relay-runtime-draft-upload.test.mjs ` + config/scripts/ssh-relay-runtime-release-assets.test.mjs ` config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs ` config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs ` config/scripts/ssh-relay-runtime-manifest-seed-signing.test.mjs ` diff --git a/config/scripts/ssh-relay-runtime-release-assets.mjs b/config/scripts/ssh-relay-runtime-release-assets.mjs new file mode 100644 index 00000000000..d3b22b55a2d --- /dev/null +++ b/config/scripts/ssh-relay-runtime-release-assets.mjs @@ -0,0 +1,289 @@ +import { parseSshRelayRuntimeUnsignedManifest } from './ssh-relay-runtime-manifest-validation.mjs' + +const ASSET_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,239}$/u +const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u +const SIGNATURE_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/u +const MANAGED_ASSET_PREFIX = 'orca-ssh-relay-runtime-' +const MANIFEST_NAME = 'orca-ssh-relay-runtime-manifest.json' +const SIGNATURE_NAME = 'orca-ssh-relay-runtime-manifest.sig' +const MAX_ASSET_BYTES = 100 * 1024 * 1024 +const MAX_TOTAL_BYTES = 1024 * 1024 * 1024 +const MAX_MANIFEST_BYTES = 1024 * 1024 +const MAX_SIGNATURE_BYTES = 4096 +const MAX_MANAGED_ASSETS = 26 +const MAX_RELEASE_ASSETS = 1000 +const MAX_SIGNATURES = 4 + +function assertObject(value, label) { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`SSH relay runtime release assets ${label} must be an object`) + } +} + +function assertExactFields(value, fields, label) { + assertObject(value, label) + const actual = Object.keys(value).sort() + const expected = [...fields].sort() + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + throw new Error(`SSH relay runtime release assets ${label} has unexpected or missing fields`) + } +} + +function parseReleaseTag(tag) { + if (typeof tag !== 'string') { + throw new Error('SSH relay runtime release assets tag is invalid') + } + for (const [pattern, channel] of [ + [/^v(\d+\.\d+\.\d+)$/u, 'stable'], + [/^v(\d+\.\d+\.\d+-rc\.\d+)$/u, 'rc'], + [/^v(\d+\.\d+\.\d+-rc\.\d+\.perf)$/u, 'perf'] + ]) { + const match = pattern.exec(tag) + if (match) { + return { tag, channel, version: match[1] } + } + } + throw new Error('SSH relay runtime release assets tag is invalid') +} + +function normalizeSignatures(signatures) { + if (!Array.isArray(signatures) || signatures.length === 0 || signatures.length > MAX_SIGNATURES) { + throw new Error('SSH relay runtime release assets verified manifest signatures are invalid') + } + const keyIds = new Set() + for (const [index, signature] of signatures.entries()) { + assertExactFields(signature, ['algorithm', 'keyId', 'signature'], `manifest signature ${index}`) + if (signature.algorithm !== 'ed25519-v1' || !DIGEST_PATTERN.test(signature.keyId)) { + throw new Error('SSH relay runtime release assets verified manifest signature is invalid') + } + if (typeof signature.signature !== 'string' || !SIGNATURE_PATTERN.test(signature.signature)) { + throw new Error('SSH relay runtime release assets verified manifest signature is invalid') + } + const signatureBytes = Buffer.from(signature.signature, 'base64') + if ( + signatureBytes.length !== 64 || + signatureBytes.toString('base64') !== signature.signature || + keyIds.has(signature.keyId) + ) { + throw new Error('SSH relay runtime release assets verified manifest signature is invalid') + } + keyIds.add(signature.keyId) + } + return [...keyIds].sort() +} + +function normalizeAssetDescriptor(asset, expectedName, label, maximum = MAX_ASSET_BYTES) { + assertExactFields(asset, ['name', 'sha256', 'size'], label) + if (asset.name !== expectedName || !ASSET_NAME_PATTERN.test(asset.name)) { + throw new Error(`SSH relay runtime release assets ${label} has an invalid name`) + } + if (!DIGEST_PATTERN.test(asset.sha256)) { + throw new Error(`SSH relay runtime release assets ${label} has an invalid SHA-256`) + } + if (!Number.isSafeInteger(asset.size) || asset.size <= 0 || asset.size > maximum) { + throw new Error(`SSH relay runtime release assets ${label} has an invalid size`) + } + return { ...asset } +} + +function normalizeSignatureAsset(asset, manifestAsset, manifestKeyIds) { + assertExactFields( + asset, + ['keyIds', 'manifestSha256', 'name', 'sha256', 'size'], + 'signature asset' + ) + const descriptor = normalizeAssetDescriptor( + { name: asset.name, sha256: asset.sha256, size: asset.size }, + SIGNATURE_NAME, + 'signature asset descriptor', + MAX_SIGNATURE_BYTES + ) + if ( + !Array.isArray(asset.keyIds) || + asset.keyIds.length === 0 || + asset.keyIds.length > MAX_SIGNATURES || + asset.keyIds.some((keyId) => typeof keyId !== 'string' || !DIGEST_PATTERN.test(keyId)) + ) { + throw new Error('SSH relay runtime release assets signature keys are invalid') + } + if (!DIGEST_PATTERN.test(asset.manifestSha256)) { + throw new Error('SSH relay runtime release assets signature manifest identity is invalid') + } + if (asset.manifestSha256 !== manifestAsset.sha256) { + throw new Error( + 'SSH relay runtime release assets signature manifest identity disagrees with the manifest' + ) + } + const keyIds = [...new Set(asset.keyIds)].sort() + if ( + keyIds.length !== asset.keyIds.length || + JSON.stringify(keyIds) !== JSON.stringify(manifestKeyIds) + ) { + // Why: the detached encoding stays separately gated, but it cannot claim different signers. + throw new Error('SSH relay runtime release assets signature keys disagree with the manifest') + } + return descriptor +} + +function normalizeVerifiedManifest(envelope) { + assertExactFields( + envelope, + ['manifest', 'manifestAsset', 'signatureAsset'], + 'verified manifest envelope' + ) + assertObject(envelope.manifest, 'verified manifest') + const { signatures, ...unsignedManifest } = envelope.manifest + const manifest = parseSshRelayRuntimeUnsignedManifest(unsignedManifest) + const manifestKeyIds = normalizeSignatures(signatures) + const manifestAsset = normalizeAssetDescriptor( + envelope.manifestAsset, + MANIFEST_NAME, + 'manifest asset', + MAX_MANIFEST_BYTES + ) + const signatureAsset = normalizeSignatureAsset( + envelope.signatureAsset, + manifestAsset, + manifestKeyIds + ) + return { manifest, manifestAsset, signatureAsset } +} + +function coveredManifestAssets(manifest) { + const assets = [] + for (const tuple of manifest.tuples) { + assets.push( + normalizeAssetDescriptor( + { + name: tuple.archive.name, + sha256: tuple.archive.sha256, + size: tuple.archive.size + }, + tuple.archive.name, + `${tuple.tupleId} archive` + ), + normalizeAssetDescriptor( + tuple.metadataAssets.sbom, + tuple.metadataAssets.sbom.name, + `${tuple.tupleId} SBOM` + ), + normalizeAssetDescriptor( + tuple.metadataAssets.provenance, + tuple.metadataAssets.provenance.name, + `${tuple.tupleId} provenance` + ) + ) + } + return assets +} + +function exactManagedAssets(verified) { + const assets = [ + ...coveredManifestAssets(verified.manifest), + verified.manifestAsset, + verified.signatureAsset + ] + if (assets.length === 0 || assets.length > MAX_MANAGED_ASSETS) { + throw new Error('SSH relay runtime release assets coverage must be a bounded non-empty set') + } + const names = new Set() + let totalBytes = 0 + for (const asset of assets) { + if (!asset.name.startsWith(MANAGED_ASSET_PREFIX) || names.has(asset.name)) { + throw new Error( + `SSH relay runtime release assets has duplicate or invalid coverage: ${asset.name}` + ) + } + names.add(asset.name) + totalBytes += asset.size + } + if (!Number.isSafeInteger(totalBytes) || totalBytes > MAX_TOTAL_BYTES) { + throw new Error('SSH relay runtime release assets coverage exceeds the total size limit') + } + return assets +} + +function validateBuildIdentity(manifest, expected) { + if (manifest.build.tag !== expected.tag) { + throw new Error('SSH relay runtime release assets manifest tag does not match the release') + } + if (manifest.build.channel !== expected.channel || manifest.build.version !== expected.version) { + throw new Error('SSH relay runtime release assets manifest build identity does not match') + } +} + +function validateRelease(release, releaseId, expectedIdentity, expectedAssets) { + assertObject(release, 'authenticated release metadata') + if (!Number.isSafeInteger(releaseId) || releaseId <= 0 || release.id !== releaseId) { + throw new Error('SSH relay runtime release assets release ID does not match') + } + if (release.draft !== true) { + throw new Error('SSH relay runtime release assets requires an exact unpublished draft') + } + if (release.tag_name !== expectedIdentity.tag) { + throw new Error('SSH relay runtime release assets release tag does not match') + } + const expectedPrerelease = expectedIdentity.channel !== 'stable' + if (release.prerelease !== expectedPrerelease) { + throw new Error('SSH relay runtime release assets prerelease state disagrees with its channel') + } + if (!Array.isArray(release.assets) || release.assets.length > MAX_RELEASE_ASSETS) { + throw new Error('SSH relay runtime release assets metadata must contain a bounded asset array') + } + + const expectedByName = new Map(expectedAssets.map((asset) => [asset.name, asset])) + const releaseByName = new Map() + for (const asset of release.assets) { + assertObject(asset, 'release asset') + if (typeof asset.name !== 'string' || !ASSET_NAME_PATTERN.test(asset.name)) { + throw new Error('SSH relay runtime release assets has an invalid release asset') + } + if (releaseByName.has(asset.name)) { + throw new Error( + `SSH relay runtime release assets has a duplicate release asset: ${asset.name}` + ) + } + releaseByName.set(asset.name, asset) + if (asset.name.startsWith(MANAGED_ASSET_PREFIX) && !expectedByName.has(asset.name)) { + throw new Error( + `SSH relay runtime release assets has an unexpected managed asset: ${asset.name}` + ) + } + } + + for (const expected of expectedAssets) { + const actual = releaseByName.get(expected.name) + if (!actual) { + throw new Error(`SSH relay runtime release assets is missing managed asset: ${expected.name}`) + } + if (!Number.isSafeInteger(actual.id) || actual.id <= 0) { + throw new Error(`SSH relay runtime release asset ID is invalid: ${expected.name}`) + } + if (actual.state !== 'uploaded') { + throw new Error(`SSH relay runtime release asset is not uploaded: ${expected.name}`) + } + if (actual.size === 0) { + throw new Error(`SSH relay runtime release asset is empty: ${expected.name}`) + } + if (actual.size !== expected.size) { + throw new Error(`SSH relay runtime release asset size disagrees: ${expected.name}`) + } + } +} + +export function verifySshRelayRuntimeReleaseAssets({ releaseId, tag, release, verifiedManifest }) { + const expectedIdentity = parseReleaseTag(tag) + const verified = normalizeVerifiedManifest(verifiedManifest) + validateBuildIdentity(verified.manifest, expectedIdentity) + const expectedAssets = exactManagedAssets(verified) + // Why: this pure boundary can consume authenticated metadata without gaining release authority. + validateRelease(release, releaseId, expectedIdentity, expectedAssets) + return { + releaseId: release.id, + tag, + channel: expectedIdentity.channel, + draft: true, + prerelease: release.prerelease, + checked: expectedAssets.map((asset) => asset.name).sort() + } +} diff --git a/config/scripts/ssh-relay-runtime-release-assets.test.mjs b/config/scripts/ssh-relay-runtime-release-assets.test.mjs new file mode 100644 index 00000000000..cef9cffb4c7 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-release-assets.test.mjs @@ -0,0 +1,193 @@ +import { describe, expect, it, vi } from 'vitest' + +import { createSshRelayArtifactTestManifest } from '../../src/main/ssh/ssh-relay-artifact-test-manifest' +import { verifySshRelayRuntimeReleaseAssets } from './ssh-relay-runtime-release-assets.mjs' + +const MANIFEST_NAME = 'orca-ssh-relay-runtime-manifest.json' +const SIGNATURE_NAME = 'orca-ssh-relay-runtime-manifest.sig' +const digest = (character) => `sha256:${character.repeat(64)}` + +function buildIdentity(tag) { + for (const [pattern, channel] of [ + [/^v(\d+\.\d+\.\d+)$/u, 'stable'], + [/^v(\d+\.\d+\.\d+-rc\.\d+)$/u, 'rc'], + [/^v(\d+\.\d+\.\d+-rc\.\d+\.perf)$/u, 'perf'] + ]) { + const match = pattern.exec(tag) + if (match) { + return { tag, channel, version: match[1] } + } + } + throw new Error(`Unsupported test tag: ${tag}`) +} + +function fixture(tag = 'v1.4.140-rc.1') { + const manifest = createSshRelayArtifactTestManifest() + manifest.build = { ...manifest.build, ...buildIdentity(tag) } + const manifestAsset = { name: MANIFEST_NAME, size: 2_048, sha256: digest('e') } + const signatureAsset = { + name: SIGNATURE_NAME, + size: 256, + sha256: digest('f'), + manifestSha256: manifestAsset.sha256, + keyIds: manifest.signatures.map((signature) => signature.keyId) + } + const expectedAssets = [ + manifest.tuples[0].archive, + manifest.tuples[0].metadataAssets.sbom, + manifest.tuples[0].metadataAssets.provenance, + manifestAsset, + signatureAsset + ] + const release = { + id: 42, + tag_name: tag, + draft: true, + prerelease: manifest.build.channel !== 'stable', + assets: [ + ...expectedAssets.map((asset, index) => ({ + id: index + 1, + name: asset.name, + state: 'uploaded', + size: asset.size + })), + { id: 99, name: 'orca-windows-setup.exe', state: 'uploaded', size: 3_000 } + ] + } + return { + releaseId: release.id, + tag, + release, + verifiedManifest: { manifest, manifestAsset, signatureAsset } + } +} + +function managedAsset(input, name) { + return input.release.assets.find((asset) => asset.name === name) +} + +describe('SSH relay runtime release required assets', () => { + it.each([ + ['v1.4.140', 'stable', false], + ['v1.4.140-rc.2', 'rc', true], + ['v1.4.140-rc.2.perf', 'perf', true] + ])( + 'accepts exact %s manifest coverage and authenticated draft metadata', + (tag, channel, prerelease) => { + const input = fixture(tag) + + expect(verifySshRelayRuntimeReleaseAssets(input)).toEqual({ + releaseId: 42, + tag, + channel, + draft: true, + prerelease, + checked: expect.arrayContaining([ + input.verifiedManifest.manifest.tuples[0].archive.name, + input.verifiedManifest.manifest.tuples[0].metadataAssets.sbom.name, + input.verifiedManifest.manifest.tuples[0].metadataAssets.provenance.name, + MANIFEST_NAME, + SIGNATURE_NAME + ]) + }) + } + ) + + it.each([ + ['missing', (input) => input.release.assets.splice(0, 1), /missing managed asset/i], + [ + 'extra managed', + (input) => + input.release.assets.push({ + id: 100, + name: 'orca-ssh-relay-runtime-uncovered.zip', + state: 'uploaded', + size: 1 + }), + /unexpected managed asset/i + ], + [ + 'duplicate', + (input) => input.release.assets.push(structuredClone(input.release.assets[0])), + /duplicate release asset/i + ] + ])('rejects a %s managed asset set', (_label, mutate, message) => { + const input = fixture() + mutate(input) + expect(() => verifySshRelayRuntimeReleaseAssets(input)).toThrow(message) + }) + + it.each([ + ['non-uploaded', (asset) => (asset.state = 'new'), /not uploaded/i], + ['empty', (asset) => (asset.size = 0), /empty/i], + ['wrong-size', (asset) => (asset.size += 1), /size disagrees/i] + ])('rejects a %s covered asset', (_label, mutate, message) => { + const input = fixture() + mutate(managedAsset(input, MANIFEST_NAME)) + expect(() => verifySshRelayRuntimeReleaseAssets(input)).toThrow(message) + }) + + it('rejects release, manifest, or prerelease identity crossing tags and channels', () => { + const crossRelease = fixture() + crossRelease.release.tag_name = 'v1.4.140-rc.2' + expect(() => verifySshRelayRuntimeReleaseAssets(crossRelease)).toThrow(/release tag/i) + + const crossDraft = fixture() + crossDraft.release.id += 1 + expect(() => verifySshRelayRuntimeReleaseAssets(crossDraft)).toThrow(/release ID/i) + + const crossManifest = fixture() + Object.assign(crossManifest.verifiedManifest.manifest.build, buildIdentity('v1.4.140-rc.2')) + expect(() => verifySshRelayRuntimeReleaseAssets(crossManifest)).toThrow(/manifest tag/i) + + const crossChannel = fixture() + crossChannel.verifiedManifest.manifest.build.channel = 'stable' + expect(() => verifySshRelayRuntimeReleaseAssets(crossChannel)).toThrow(/manifest.*identity/i) + + const wrongPrerelease = fixture() + wrongPrerelease.release.prerelease = false + expect(() => verifySshRelayRuntimeReleaseAssets(wrongPrerelease)).toThrow(/prerelease/i) + }) + + it('rejects a detached signature asset that disagrees with verified manifest signatures', () => { + const input = fixture() + input.verifiedManifest.signatureAsset.keyIds = [digest('0')] + + expect(() => verifySshRelayRuntimeReleaseAssets(input)).toThrow(/signature keys disagree/i) + + const crossManifest = fixture() + crossManifest.verifiedManifest.signatureAsset.manifestSha256 = digest('0') + expect(() => verifySshRelayRuntimeReleaseAssets(crossManifest)).toThrow( + /signature manifest identity disagrees/i + ) + }) + + it('rejects malformed or incomplete verified-manifest asset bindings', () => { + const malformedDigest = fixture() + malformedDigest.verifiedManifest.manifestAsset.sha256 = digest('A') + expect(() => verifySshRelayRuntimeReleaseAssets(malformedDigest)).toThrow(/sha-256/i) + + const missingSignature = fixture() + missingSignature.verifiedManifest.manifest.signatures = [] + expect(() => verifySshRelayRuntimeReleaseAssets(missingSignature)).toThrow(/signature/i) + + const malformedSignature = fixture() + malformedSignature.verifiedManifest.manifest.signatures[0].signature = 42 + expect(() => verifySshRelayRuntimeReleaseAssets(malformedSignature)).toThrow(/signature/i) + + const unexpectedField = fixture() + unexpectedField.verifiedManifest.manifestAsset.path = '/tmp/manifest' + expect(() => verifySshRelayRuntimeReleaseAssets(unexpectedField)).toThrow(/fields/i) + }) + + it('is a disconnected metadata gate with no network request', () => { + const fetchMock = vi.fn(() => { + throw new Error('network must remain disconnected') + }) + vi.stubGlobal('fetch', fetchMock) + + expect(() => verifySshRelayRuntimeReleaseAssets(fixture())).not.toThrow() + expect(fetchMock).not.toHaveBeenCalled() + vi.unstubAllGlobals() + }) +}) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 1e997d16d96..fb28af32c32 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -134,7 +134,8 @@ describe('SSH relay runtime artifact workflow', () => { 'draft-recovery', 'aggregate-input', 'draft-readback', - 'draft-upload' + 'draft-upload', + 'release-assets' ]) { const script = `ssh-relay-runtime-${moduleName}` expect(source.split(`${script}.test.mjs`)).toHaveLength(5) diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md index c1a3d733041..9da4b6a3256 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md @@ -15,9 +15,11 @@ in progress, 2026-07-15, Codex implementation owner.** Exact-head run target-native artifact jobs and the uploader's eight-test suite on every Node 24 runner under `E-M4-DRAFT-UPLOAD-CI-001`. PR Checks and Golden E2E are green; the artifact workflow is red only for the retained Windows ARM build-26200-versus-26100 floor mismatch after successful runtime smoke. -Next, add the disconnected relay-specific release required-asset gate for exact manifest coverage, -names, sizes, signature, upload state, and channel identity. Production keys/environment/seed, -publication, desktop/default consumers, tuple enablement, and merge to `main` remain disconnected. +The disconnected relay-specific release required-asset gate is locally green under +`E-M4-RELEASE-ASSETS-LOCAL-RED-001` and `E-M4-RELEASE-ASSETS-LOCAL-001`: exact manifest coverage, +names, sizes, signature binding, upload state, and stable/RC/perf identity. Exact-head all-six native +CI is required. Production keys/environment/seed, publication, desktop/default consumers, tuple +enablement, and merge to `main` remain disconnected. ## Safety status @@ -261,6 +263,10 @@ publication, desktop/default consumers, tuple enablement, and merge to `main` re `E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-001`, and exact-head execution passes on all six native jobs under `E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001`. Live protected signing remains open; no accepted production key, environment, or seed is provisioned. +- [ ] Add the relay-specific required-asset gate to the release composition. The disconnected pure + gate is locally green under `E-M4-RELEASE-ASSETS-LOCAL-RED-001` and + `E-M4-RELEASE-ASSETS-LOCAL-001`; exact-head all-six native CI is required before closing the + capability, and release/default workflow composition remains separately gated. - [ ] Embed the exact signed manifest and accepted keys in each desktop build. - [ ] Upload to a draft release, read back, re-hash, and execute the downloaded archives. A disconnected bounded upload/recovery implementation is locally green under diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md index 8efefa5e629..bfee6a13e56 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md @@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    -Current phase: Milestone 4 / Work Package 3 disconnected relay required-asset gate — **In progress — 2026-07-15, Codex implementation owner**. The bounded draft upload/recovery capability is closed locally and on all six exact-head native jobs under E-M4-DRAFT-UPLOAD-LOCAL-RED-001, E-M4-DRAFT-UPLOAD-LOCAL-001, and E-M4-DRAFT-UPLOAD-CI-001. The active package must add a disconnected relay-specific release gate that consumes only an already verified signed manifest plus authenticated draft metadata and rejects missing, extra managed, non-uploaded, empty, wrong-size, uncovered, cross-tag/channel, or signature-disagreeing assets. Keep the existing desktop required-asset gate unchanged until composition is explicitly reviewed. Release-cut, desktop builds, publication, and every tuple stay disconnected; production/default behavior is unchanged.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `b37cacecf8f1c865f09c118f274ab62302e951ad` passes all six target-native artifact jobs and the eight-test uploader suite in run 29431510990 under E-M4-DRAFT-UPLOAD-CI-001. Both Linux oldest-userland supplements and Windows x64 floor pass. Windows arm64 runtime smoke passes in 7,752.6288ms with 49,684,480-byte RSS, then its floor job retains the expected fail-closed hosted-build 26200 versus required 26100 mismatch. PR Checks 29431510764 and Golden E2E 29431510883 are green. The user authorizes commits, pushes, draft-PR updates, CI reruns, and protected rehearsals within PRs, but not merging to `main`. No accepted production key, protected environment/seed, native-signing trust proof, release write, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
    +Current phase: Milestone 4 / Work Package 3 disconnected relay required-asset gate — **In progress — 2026-07-15, Codex implementation owner**. The bounded draft upload/recovery capability is closed locally and on all six exact-head native jobs under E-M4-DRAFT-UPLOAD-LOCAL-RED-001, E-M4-DRAFT-UPLOAD-LOCAL-001, and E-M4-DRAFT-UPLOAD-CI-001. The active package's purpose-named missing-module RED and locally green implementation are recorded under E-M4-RELEASE-ASSETS-LOCAL-RED-001 and E-M4-RELEASE-ASSETS-LOCAL-001. Exact-head execution on all six native Node 24 jobs is required before closing this package. The gate remains a disconnected pure metadata boundary; detached-signature byte encoding, the existing desktop required-asset gate, and workflow composition are unchanged. Release-cut, desktop builds, publication, and every tuple stay disconnected; production/default behavior is unchanged.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — the locally green release-asset gate pins exact release ID/tag/channel/prerelease, derives the complete managed set from a validated already-verified signed-manifest envelope, binds opaque manifest/signature descriptors by hash and signer IDs, and rejects missing, extra managed, duplicate, non-uploaded, empty, wrong-size, uncovered, or cross-identity metadata under E-M4-RELEASE-ASSETS-LOCAL-001. The final local run passes 13 focused tests, 64 adjacent release tests, all 283 SSH-relay script tests, typecheck, lint, formatting, and diff checks. Only syntax/test execution is added to the six native artifact jobs; there is no command-line entrypoint, release API call, upload/publication caller, desktop consumer, or tuple enablement. The user authorizes commits, pushes, draft-PR updates, CI reruns, and protected rehearsals within PRs, but not merging to `main`. No accepted production key, protected environment/seed, native-signing trust proof, release write, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -113,8 +113,11 @@ same change as the work it records. is no CLI entrypoint or workflow caller, and no real release write occurred. - Active package: Work Package 3 disconnected relay required-asset gate. Begin with a purpose-named missing-module RED for exact verified-manifest coverage, signature, name, size, upload state, - managed-asset closure, tag/channel identity, and consumer disconnection. Keep the existing desktop - required-asset gate unchanged in this slice. + managed-asset closure, tag/channel identity, and consumer disconnection. The missing-module RED is + recorded under E-M4-RELEASE-ASSETS-LOCAL-RED-001. The locally green pure gate and test-only native + workflow wiring are recorded under E-M4-RELEASE-ASSETS-LOCAL-001; require exact-head all-six CI. + Keep the existing desktop required-asset gate and detached-signature byte encoding unchanged in + this slice. - Completed Work Package 2 gate: target-native Windows source-signature reports from exact-head artifact jobs 87267322867 and 87267322870 were independently downloaded and matched to their identities and signing-stage reports under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001. PR Checks @@ -11355,6 +11358,88 @@ lint` passes all 41 reliability gates, the 355-entry max-lines ratchet, bundled- - Checklist items satisfied: exact-head all-six CI gate for the disconnected draft upload/recovery capability. The broader draft upload/read-back/execution and Work Package 3 boxes remain open. +### E-M4-RELEASE-ASSETS-LOCAL-RED-001 — Relay-specific release required-asset gate is absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted purpose-named test + `config/scripts/ssh-relay-runtime-release-assets.test.mjs`; draft PR #8741. +- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. No network request, + credential, release write, signer, protected environment, SSH remote, desktop consumer, + publication, or enabled tuple. +- Command: + - `/usr/bin/time -l pnpm exec vitest run config/scripts/ssh-relay-runtime-release-assets.test.mjs` +- Result: expected FAIL, one suite / zero imported tests, 200ms Vitest and 0.97s wall. Maximum RSS + 132,022,272 bytes; peak memory footprint 95,950,176 bytes. +- Failure oracle: Vitest reports only `Cannot find module './ssh-relay-runtime-release-assets.mjs'` + at the purpose-named import. No production module existed that derived exact archive/SBOM/ + provenance/manifest/signature coverage from an already verified signed manifest and compared it + with authenticated draft metadata. +- Does not prove: implementation correctness, exact tag/channel closure, managed-asset closure, + upload state/size enforcement, signature-asset binding, native runner behavior, real GitHub + metadata, read-back byte identity, publication, desktop embedding, SSH behavior, or an enabled + tuple. +- Follow-up: implement only the disconnected metadata gate, retain the detached-signature encoding + decision and existing desktop release gate unchanged, then run the focused GREEN and broader + local regressions before adding native test-job integration. + +### E-M4-RELEASE-ASSETS-LOCAL-001 — Exact signed-manifest release coverage passes locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted `config/scripts/ssh-relay-runtime-release-assets.mjs`, its purpose-named test, + and POSIX/Windows native test-job wiring atop exact pushed head + `88a77eee91a34f8936882d096e568f499e9160e6`; draft PR #8741. +- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. In-memory manifest and + authenticated-release-metadata fixtures only. No network request, credential, release write, + signer, protected environment, SSH remote, desktop consumer, publication, or enabled tuple. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 config/scripts/ssh-relay-runtime-release-assets.test.mjs` — PASS, one file / 13 + tests in 203ms Vitest and 0.92s wall; maximum RSS 152,403,968 bytes, peak footprint 96,114,064 + bytes, and zero swaps. +- Adjacent release command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-release-assets.test.mjs +config/scripts/ssh-relay-runtime-draft-upload.test.mjs +config/scripts/ssh-relay-runtime-draft-recovery.test.mjs +config/scripts/ssh-relay-runtime-draft-readback.test.mjs +config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs +config/scripts/ssh-relay-runtime-workflow.test.mjs +config/scripts/verify-release-required-assets.test.mjs` — PASS, 7 files / 64 tests in 988ms + Vitest and 1.72s wall; maximum RSS 135,806,976 bytes, peak footprint 95,802,744 bytes, and zero + swaps. +- Broad release command: `/usr/bin/time -l sh -c "rg --files config/scripts | rg +'/ssh-relay.*[.]test[.]mjs$' | xargs pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1"` — PASS, 53 files / 283 tests in 9.97s Vitest and 10.94s wall; maximum RSS + 189,071,360 bytes and zero swaps. +- Static gates: `node --check` passes both new files. `pnpm run typecheck` passes. `pnpm run lint` + passes all 41 reliability gates, the 355-entry max-lines ratchet, bundled-skill verification, + localization catalog/parity, and localization coverage; all 26 warnings remain in untouched + existing files. Focused `oxfmt --check` and `git diff --check` pass. The protected Node/npm + resolver files and existing `verify-release-required-assets.mjs` plus its tests have zero diff. +- Oracle proved: the pure gate accepts only an exact release ID and stable/RC/perf tag whose + authenticated metadata remains a draft with channel-correct prerelease state. It revalidates the + already-verified signed-manifest schema, derives every tuple archive/SBOM/provenance plus exact + manifest/signature descriptors, bounds count/per-file/aggregate sizes, and requires unique + managed names. The opaque detached-signature descriptor must bind the exact manifest SHA-256 and + exact embedded signer key IDs without deciding its byte encoding. Missing, unexpected managed, + duplicate, non-uploaded, empty, wrong-size, malformed, signature-disagreeing, cross-release, + cross-tag, and cross-channel states fail closed; unrelated desktop assets remain untouched. +- Consumer-disconnection oracle: repository search finds the new production module only in its own + focused test and POSIX/Windows artifact-job syntax checks; only the test file is executed. There is + no CLI entrypoint, API fetch, release-cut/default workflow caller, upload, publication, desktop + consumer, tuple enablement, or change to the legacy SSH path. +- Does not prove: Node 24/native-runner behavior, real authenticated GitHub metadata, cryptographic + verification before envelope construction, final detached-signature byte encoding, byte + read-back, downloaded archive execution, a real release draft/write, native signing/trust, + publication, desktop embedding, SSH transfer/install, performance against legacy, or any enabled + tuple. +- Checklist items satisfied: local implementation gate for the disconnected relay-specific + required-asset capability. The broader Milestone 4 release composition/publication box remains + open. +- Follow-up: commit and push the exact package, then require syntax and 13-test execution on all six + exact-head native Node 24 jobs plus PR Checks and Golden E2E. Keep every production/default + consumer disconnected. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -11413,13 +11498,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Checkpoint and push E-M4-DRAFT-UPLOAD-CI-001. Then begin a purpose-named disconnected RED for the -relay-specific release required-asset gate: consume only an already verified signed manifest and -authenticated draft metadata; require exact manifest/signature coverage, asset names, sizes, upload -state, managed-asset closure, and stable/RC/perf tag identity. Keep the existing desktop -`verify-release-required-assets.mjs` behavior unchanged until composition is separately reviewed. -Do not add a workflow caller or perform a real release write. The protected environment, accepted -production keys, and secret remain unprovisioned, so live signing evidence stays BLOCKED. +Checkpoint and push E-M4-RELEASE-ASSETS-LOCAL-RED-001 and E-M4-RELEASE-ASSETS-LOCAL-001. Require +exact-head syntax and 13-test execution on all six native artifact jobs plus PR Checks and Golden +E2E. Do not add a workflow caller or perform a real release write; keep the existing desktop +`verify-release-required-assets.mjs` behavior and detached-signature byte encoding unchanged until +composition is separately reviewed. The protected environment, accepted production keys, and +secret remain unprovisioned, so live signing evidence stays BLOCKED. Keep Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. Do not merge to `main`; retain every production/default gate. From 16edb7e755d35eeec561adbfff07587f5fc0bdf2 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 13:04:57 -0400 Subject: [PATCH 140/267] docs: record relay release asset CI proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 27 +++--- ...github-release-implementation-checklist.md | 82 +++++++++++++++---- 2 files changed, 80 insertions(+), 29 deletions(-) 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 9da4b6a3256..a5e8b0a7630 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 @@ -9,17 +9,16 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 4 / Work Package 3 disconnected relay required-asset gate — +Active checkpoint: **Milestone 4 / Work Package 3 disconnected authenticated draft read-back +materialization — in progress, 2026-07-15, Codex implementation owner.** Exact-head run -[29431510990](https://github.com/stablyai/orca/actions/runs/29431510990) at `b37cacecf` passes all six -target-native artifact jobs and the uploader's eight-test suite on every Node 24 runner under -`E-M4-DRAFT-UPLOAD-CI-001`. PR Checks and Golden E2E are green; the artifact workflow is red only for -the retained Windows ARM build-26200-versus-26100 floor mismatch after successful runtime smoke. -The disconnected relay-specific release required-asset gate is locally green under -`E-M4-RELEASE-ASSETS-LOCAL-RED-001` and `E-M4-RELEASE-ASSETS-LOCAL-001`: exact manifest coverage, -names, sizes, signature binding, upload state, and stable/RC/perf identity. Exact-head all-six native -CI is required. Production keys/environment/seed, publication, desktop/default consumers, tuple -enablement, and merge to `main` remain disconnected. +[29433808457](https://github.com/stablyai/orca/actions/runs/29433808457) at `5460563ff` passes all six +target-native artifact jobs and the required-asset gate's 13-test suite on every Node 24 runner under +`E-M4-RELEASE-ASSETS-CI-001`. PR Checks and Golden E2E are green; the artifact workflow is red only +for the retained Windows ARM build-26200-versus-26100 floor mismatch after successful runtime smoke. +Next, add exclusive local materialization to the existing authenticated read-back without a second +download. Production keys/environment/seed, publication, desktop/default consumers, tuple enablement, +and merge to `main` remain disconnected. ## Safety status @@ -263,10 +262,10 @@ enablement, and merge to `main` remain disconnected. `E-M4-PROTECTED-MANIFEST-WORKFLOW-LOCAL-001`, and exact-head execution passes on all six native jobs under `E-M4-PROTECTED-MANIFEST-WORKFLOW-CI-001`. Live protected signing remains open; no accepted production key, environment, or seed is provisioned. -- [ ] Add the relay-specific required-asset gate to the release composition. The disconnected pure - gate is locally green under `E-M4-RELEASE-ASSETS-LOCAL-RED-001` and - `E-M4-RELEASE-ASSETS-LOCAL-001`; exact-head all-six native CI is required before closing the - capability, and release/default workflow composition remains separately gated. +- [x] Add the disconnected relay-specific required-asset capability. It passes locally and on all + six native jobs under `E-M4-RELEASE-ASSETS-LOCAL-RED-001`, + `E-M4-RELEASE-ASSETS-LOCAL-001`, and `E-M4-RELEASE-ASSETS-CI-001`. Release/default workflow + composition remains separately gated. - [ ] Embed the exact signed manifest and accepted keys in each desktop build. - [ ] Upload to a draft release, read back, re-hash, and execute the downloaded archives. A disconnected bounded upload/recovery implementation is locally green under diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md index bfee6a13e56..93f6ae371ac 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md @@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    -Current phase: Milestone 4 / Work Package 3 disconnected relay required-asset gate — **In progress — 2026-07-15, Codex implementation owner**. The bounded draft upload/recovery capability is closed locally and on all six exact-head native jobs under E-M4-DRAFT-UPLOAD-LOCAL-RED-001, E-M4-DRAFT-UPLOAD-LOCAL-001, and E-M4-DRAFT-UPLOAD-CI-001. The active package's purpose-named missing-module RED and locally green implementation are recorded under E-M4-RELEASE-ASSETS-LOCAL-RED-001 and E-M4-RELEASE-ASSETS-LOCAL-001. Exact-head execution on all six native Node 24 jobs is required before closing this package. The gate remains a disconnected pure metadata boundary; detached-signature byte encoding, the existing desktop required-asset gate, and workflow composition are unchanged. Release-cut, desktop builds, publication, and every tuple stay disconnected; production/default behavior is unchanged.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — the locally green release-asset gate pins exact release ID/tag/channel/prerelease, derives the complete managed set from a validated already-verified signed-manifest envelope, binds opaque manifest/signature descriptors by hash and signer IDs, and rejects missing, extra managed, duplicate, non-uploaded, empty, wrong-size, uncovered, or cross-identity metadata under E-M4-RELEASE-ASSETS-LOCAL-001. The final local run passes 13 focused tests, 64 adjacent release tests, all 283 SSH-relay script tests, typecheck, lint, formatting, and diff checks. Only syntax/test execution is added to the six native artifact jobs; there is no command-line entrypoint, release API call, upload/publication caller, desktop consumer, or tuple enablement. The user authorizes commits, pushes, draft-PR updates, CI reruns, and protected rehearsals within PRs, but not merging to `main`. No accepted production key, protected environment/seed, native-signing trust proof, release write, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
    +Current phase: Milestone 4 / Work Package 3 disconnected authenticated draft read-back materialization — **In progress — 2026-07-15, Codex implementation owner**. The relay-specific required-asset capability is closed locally and on all six exact-head native Node 24 jobs under E-M4-RELEASE-ASSETS-LOCAL-RED-001, E-M4-RELEASE-ASSETS-LOCAL-001, and E-M4-RELEASE-ASSETS-CI-001. It remains a disconnected pure metadata boundary; detached-signature byte encoding, the existing desktop required-asset gate, and workflow composition are unchanged. The next slice must add an exclusive local materialization boundary to the existing authenticated read-back so exact verified bytes can later enter archive extraction/execution without a second download. Begin with a purpose-named RED; keep release writes, publication, desktop consumers, and every tuple disconnected.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `5460563ff48846cbf54218860fdca1cc3c7b8614` passes the new 13-test release-asset gate and full runtime construction on all six native jobs in run 29433808457 under E-M4-RELEASE-ASSETS-CI-001. PR Checks 29433808934 and both Golden E2E jobs in 29433808372 are green. Linux supplements and the Windows x64 floor pass. Windows arm64 runtime smoke passes in 7,936.874ms with 49,553,408-byte RSS; its separate floor job then retains the expected fail-closed hosted-build 26200 versus required 26100 mismatch. There is no command-line entrypoint, release API call, upload/publication caller, desktop consumer, tuple enablement, or production/default behavior change. The user authorizes commits, pushes, draft-PR updates, CI reruns, and protected rehearsals within PRs, but not merging to `main`. No accepted production key, protected environment/seed, native-signing trust proof, release write, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -111,13 +111,15 @@ same change as the work it records. and partial-draft recovery pass locally and on all six exact-head native jobs under E-M4-DRAFT-UPLOAD-LOCAL-RED-001, E-M4-DRAFT-UPLOAD-LOCAL-001, and E-M4-DRAFT-UPLOAD-CI-001. There is no CLI entrypoint or workflow caller, and no real release write occurred. -- Active package: Work Package 3 disconnected relay required-asset gate. Begin with a purpose-named - missing-module RED for exact verified-manifest coverage, signature, name, size, upload state, - managed-asset closure, tag/channel identity, and consumer disconnection. The missing-module RED is - recorded under E-M4-RELEASE-ASSETS-LOCAL-RED-001. The locally green pure gate and test-only native - workflow wiring are recorded under E-M4-RELEASE-ASSETS-LOCAL-001; require exact-head all-six CI. - Keep the existing desktop required-asset gate and detached-signature byte encoding unchanged in - this slice. +- Completed Work Package 3 gate: disconnected relay required-asset capability. Exact manifest + coverage, signature binding, name, size, upload state, managed-asset closure, stable/RC/perf + identity, and consumer disconnection pass locally and on all six exact-head jobs under + E-M4-RELEASE-ASSETS-LOCAL-RED-001, E-M4-RELEASE-ASSETS-LOCAL-001, and + E-M4-RELEASE-ASSETS-CI-001. Release workflow composition remains open. +- Active package: Work Package 3 disconnected authenticated draft read-back materialization. Begin + with a purpose-named RED for exclusive output, exact byte persistence during the existing bounded + authenticated download, mutation cleanup, cancellation, and consumer disconnection. Do not add + archive execution, release publication, or a production caller in this slice. - Completed Work Package 2 gate: target-native Windows source-signature reports from exact-head artifact jobs 87267322867 and 87267322870 were independently downloaded and matched to their identities and signing-stage reports under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001. PR Checks @@ -11440,6 +11442,54 @@ config/scripts/verify-release-required-assets.test.mjs` — PASS, 7 files / 64 t exact-head native Node 24 jobs plus PR Checks and Golden E2E. Keep every production/default consumer disconnected. +### E-M4-RELEASE-ASSETS-CI-001 — Required-asset gate passes all six native Node 24 jobs + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `5460563ff48846cbf54218860fdca1cc3c7b8614`; draft PR #8741. +- Workflow: [SSH Relay Runtime Artifacts run 29433808457](https://github.com/stablyai/orca/actions/runs/29433808457), + 16:46:33Z–17:00:13Z. The aggregate conclusion is the expected failure only because the separate + Windows arm64 floor job rejects hosted build 26200 after successful runtime execution. +- Native build jobs, runner 2.335.1 throughout: + - Darwin arm64 job 87415257002, `macos-15-arm64` image `20260706.0213.1`, PASS in 5m12s. + - Windows x64 job 87415257023, `windows-2022` image `20260714.244.1`, PASS in 8m37s. + - Linux arm64 job 87415257044, `ubuntu-24.04-arm` image `20260714.61.1`, PASS in 4m24s. + - Linux x64 job 87415257081, `ubuntu-24.04` image `20260714.240.1`, PASS in 3m54s. + - Darwin x64 job 87415257094, `macos-15` image `20260629.0276.1`, PASS in 5m33s. + - Windows arm64 job 87415257120, `windows-11-arm64` image `20260706.102.1`, PASS in 9m30s. +- Exact required-asset suite: `config/scripts/ssh-relay-runtime-release-assets.test.mjs` passes + 13/13 under Node 24.18.0 on every native job: Darwin arm64 26ms, Windows x64 41ms, Linux arm64 + 31ms, Linux x64 66ms, Darwin x64 66ms, and Windows arm64 38ms. +- Broad contract counts: every POSIX job passes 52 files / 279 tests; durations are Darwin arm64 + 15.51s, Linux arm64 6.18s, Linux x64 8.05s, and Darwin x64 27.03s. Each Windows job passes 53 + files / 273 tests plus 10 platform skips of 283; x64 takes 25.34s and arm64 14.83s. +- Downstream evidence: Linux x64 supplement job 87416554990 passes in 43s; Linux arm64 supplement + 87416555037 passes in 50s; Windows x64 floor job 87417454621 passes in 1m35s. Windows arm64 floor + job 87417454470 uses `windows-11-arm64` image `20260706.102.1`, executes bundled Node 24.18.0 and + native PTY/watcher smoke in 7,936.874ms with 49,553,408-byte RSS, and then fails exactly because + OS/kernel build 26200 is not the required floor 26100. +- Adjacent exact-head workflows: PR Checks run + [29433808934](https://github.com/stablyai/orca/actions/runs/29433808934), job 87415230751, passes + 16:46:37Z–17:01:03Z including lint, typecheck, Git compatibility, tests, unpacked app build, and + packaged CLI smoke. Golden E2E run + [29433808372](https://github.com/stablyai/orca/actions/runs/29433808372) passes Linux job + 87415226359 and macOS job 87415226314. +- Oracle proved: the pure required-asset module, purpose-named tests, and POSIX/PowerShell + syntax/test wiring parse and execute consistently on all six native Node 24 runner families. + Every family covers exact stable/RC/perf identity, manifest-derived managed closure, manifest and + opaque signature binding, uploaded/nonempty/exact-size metadata, extra/missing/duplicate rejection, + and zero network use. Full target-native runtime construction and adjacent product regressions + remain green. +- Does not prove: real authenticated GitHub metadata, cryptographic verification before envelope + construction, final detached-signature byte encoding, real release upload/materialization, + read-back archive execution, a release write, publication, native signing/trust, oldest macOS/ + Linux kernel/Windows arm64 floor, desktop embedding, SSH transfer/install, performance against + legacy, or any enabled tuple. +- Checklist items satisfied: exact-head all-six CI gate for the disconnected relay-specific + required-asset capability. Release composition/publication remains open. +- Follow-up: checkpoint and push this evidence, then begin the disconnected authenticated draft + read-back materialization RED. Retain all production/default disconnections. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -11498,12 +11548,14 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Checkpoint and push E-M4-RELEASE-ASSETS-LOCAL-RED-001 and E-M4-RELEASE-ASSETS-LOCAL-001. Require -exact-head syntax and 13-test execution on all six native artifact jobs plus PR Checks and Golden -E2E. Do not add a workflow caller or perform a real release write; keep the existing desktop -`verify-release-required-assets.mjs` behavior and detached-signature byte encoding unchanged until -composition is separately reviewed. The protected environment, accepted production keys, and -secret remain unprovisioned, so live signing evidence stays BLOCKED. +Checkpoint and push E-M4-RELEASE-ASSETS-CI-001. Then add a purpose-named RED for exclusive local +materialization during the existing authenticated draft read-back: create only an absent output +directory, persist the exact already-streamed bytes without a second download, publish no partial +file before size/SHA-256 success, clean up on failure/cancellation, and return bounded asset paths +for the later archive-execution slice. Do not add archive execution, a workflow caller, or a real +release write in this package. Keep the existing desktop `verify-release-required-assets.mjs` +behavior and detached-signature byte encoding unchanged. The protected environment, accepted +production keys, and secret remain unprovisioned, so live signing evidence stays BLOCKED. Keep Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. Do not merge to `main`; retain every production/default gate. From 0fbbac118e08a17f8263ad48f116a8d61362fe92 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 13:16:24 -0400 Subject: [PATCH 141/267] build(ssh): materialize verified relay draft assets Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 5 + ...me-draft-readback-materialization.test.mjs | 158 +++++++++++++ .../ssh-relay-runtime-draft-readback.mjs | 218 ++++++++++++++++-- ...elease-implementation-checklist-summary.md | 15 +- ...github-release-implementation-checklist.md | 114 +++++++-- 5 files changed, 470 insertions(+), 40 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index bab4b0d3517..7dbaa410423 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -218,6 +218,7 @@ jobs: node --check config/scripts/ssh-relay-runtime-aggregate-input.test.mjs node --check config/scripts/ssh-relay-runtime-draft-readback.mjs node --check config/scripts/ssh-relay-runtime-draft-readback.test.mjs + node --check config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs node --check config/scripts/ssh-relay-runtime-draft-upload.mjs node --check config/scripts/ssh-relay-runtime-draft-upload.test.mjs node --check config/scripts/ssh-relay-runtime-release-assets.mjs @@ -277,6 +278,7 @@ jobs: config/scripts/ssh-relay-runtime-draft-recovery.test.mjs \ config/scripts/ssh-relay-runtime-aggregate-input.test.mjs \ config/scripts/ssh-relay-runtime-draft-readback.test.mjs \ + config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs \ config/scripts/ssh-relay-runtime-draft-upload.test.mjs \ config/scripts/ssh-relay-runtime-release-assets.test.mjs \ config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs \ @@ -640,6 +642,8 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime draft read-back syntax check failed' } node --check config/scripts/ssh-relay-runtime-draft-readback.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime draft read-back test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime draft read-back materialization test syntax check failed' } node --check config/scripts/ssh-relay-runtime-draft-upload.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime draft upload syntax check failed' } node --check config/scripts/ssh-relay-runtime-draft-upload.test.mjs @@ -732,6 +736,7 @@ jobs: config/scripts/ssh-relay-runtime-draft-recovery.test.mjs ` config/scripts/ssh-relay-runtime-aggregate-input.test.mjs ` config/scripts/ssh-relay-runtime-draft-readback.test.mjs ` + config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs ` config/scripts/ssh-relay-runtime-draft-upload.test.mjs ` config/scripts/ssh-relay-runtime-release-assets.test.mjs ` config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs ` diff --git a/config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs b/config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs new file mode 100644 index 00000000000..dd1a52ca0c3 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs @@ -0,0 +1,158 @@ +import { createHash } from 'node:crypto' +import { mkdir, mkdtemp, readFile, readdir, realpath, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { materializeSshRelayRuntimeDraftReadback } from './ssh-relay-runtime-draft-readback.mjs' + +const REPO = 'stablyai/orca' +const RELEASE_ID = 42 +const TAG = 'v1.4.140-rc.1' +const TOKEN = 'secret-token' +const NAME = 'orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.xz' +const BYTES = Buffer.from('immutable release bytes') +const SECOND_NAME = 'orca-ssh-relay-runtime-manifest.json' +const SECOND_BYTES = Buffer.from('immutable manifest bytes') + +let root + +function expectedAsset(name = NAME, bytes = BYTES) { + return { + name, + sha256: `sha256:${createHash('sha256').update(bytes).digest('hex')}`, + size: bytes.length + } +} + +function release(expectedAssets) { + return { + id: RELEASE_ID, + tag_name: TAG, + draft: true, + assets: expectedAssets.map((asset, index) => ({ + id: 101 + index, + name: asset.name, + state: 'uploaded', + size: asset.size + })) + } +} + +function fetchFixture(expectedAssets, assetResponses) { + const fetchImpl = vi.fn().mockResolvedValueOnce(Response.json(release(expectedAssets))) + for (const response of assetResponses) { + fetchImpl.mockResolvedValueOnce(response) + } + return fetchImpl +} + +function input(expectedAssets, fetchImpl, overrides = {}) { + return { + repo: REPO, + releaseId: RELEASE_ID, + tag: TAG, + token: TOKEN, + expectedAssets, + outputDirectory: join(root, 'readback'), + fetchImpl, + ...overrides + } +} + +beforeEach(async () => { + root = await mkdtemp(join(tmpdir(), 'orca-relay-draft-readback-')) +}) + +afterEach(async () => { + await rm(root, { recursive: true, force: true }) + vi.restoreAllMocks() +}) + +describe('SSH relay runtime draft read-back materialization', () => { + it('streams each asset once and exposes its final name only after exact verification', async () => { + const expected = expectedAsset() + let finish + const body = new ReadableStream({ + start(controller) { + controller.enqueue(BYTES.subarray(0, 5)) + finish = () => { + controller.enqueue(BYTES.subarray(5)) + controller.close() + } + } + }) + const fetchImpl = fetchFixture([expected], [new Response(body)]) + const outputDirectory = join(root, 'readback') + const physicalOutput = join(await realpath(root), 'readback') + const materialization = materializeSshRelayRuntimeDraftReadback( + input([expected], fetchImpl, { outputDirectory }) + ) + + await vi.waitFor(async () => expect((await readdir(outputDirectory)).length).toBe(1)) + const namesDuringDownload = await readdir(outputDirectory) + expect(namesDuringDownload).not.toContain(NAME) + finish() + + await expect(materialization).resolves.toEqual({ + releaseId: RELEASE_ID, + tag: TAG, + materializedAssets: [{ ...expected, path: join(physicalOutput, NAME) }] + }) + expect(fetchImpl).toHaveBeenCalledTimes(2) + expect(await readdir(outputDirectory)).toEqual([NAME]) + expect(await readFile(join(outputDirectory, NAME))).toEqual(BYTES) + }) + + it('requires an absent output directory below an existing real parent before requesting GitHub', async () => { + const expected = expectedAsset() + const existingOutput = join(root, 'existing') + await mkdir(existingOutput) + + for (const outputDirectory of [existingOutput, join(root, 'missing-parent', 'readback')]) { + const fetchImpl = vi.fn() + await expect( + materializeSshRelayRuntimeDraftReadback(input([expected], fetchImpl, { outputDirectory })) + ).rejects.toThrow(/absent|parent|no such file/i) + expect(fetchImpl).not.toHaveBeenCalled() + } + }) + + it('removes the entire output when a later asset fails verification', async () => { + const expectedAssets = [expectedAsset(), expectedAsset(SECOND_NAME, SECOND_BYTES)] + const fetchImpl = fetchFixture(expectedAssets, [ + new Response(BYTES), + new Response(Buffer.from('changed manifest bytes')) + ]) + const outputDirectory = join(root, 'readback') + + await expect( + materializeSshRelayRuntimeDraftReadback(input(expectedAssets, fetchImpl, { outputDirectory })) + ).rejects.toThrow(/size|sha-?256/i) + await expect(readdir(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' }) + }) + + it('removes partial bytes when cancellation interrupts a streamed asset', async () => { + const expected = expectedAsset() + const controller = new AbortController() + const body = new ReadableStream({ + start(stream) { + stream.enqueue(BYTES.subarray(0, 5)) + } + }) + const outputDirectory = join(root, 'readback') + const materialization = materializeSshRelayRuntimeDraftReadback( + input([expected], fetchFixture([expected], [new Response(body)]), { + outputDirectory, + signal: controller.signal + }) + ) + + await vi.waitFor(async () => expect((await readdir(outputDirectory)).length).toBe(1)) + controller.abort(new Error('cancel materialization')) + + await expect(materialization).rejects.toThrow(/cancel materialization/i) + await expect(readdir(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' }) + }) +}) diff --git a/config/scripts/ssh-relay-runtime-draft-readback.mjs b/config/scripts/ssh-relay-runtime-draft-readback.mjs index 47b14ed2b9d..f765f7d4239 100644 --- a/config/scripts/ssh-relay-runtime-draft-readback.mjs +++ b/config/scripts/ssh-relay-runtime-draft-readback.mjs @@ -1,4 +1,6 @@ import { createHash } from 'node:crypto' +import { lstat, mkdir, open, realpath, rename, rm } from 'node:fs/promises' +import { basename, dirname, join, resolve } from 'node:path' const API_VERSION = '2022-11-28' const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u @@ -174,7 +176,18 @@ async function fetchAsset({ repo, assetId, token, fetchImpl, signal }) { return redirected } -async function hashResponse(response, expected, signal) { +async function writeCompleteChunk(file, buffer) { + let offset = 0 + while (offset < buffer.length) { + const { bytesWritten } = await file.write(buffer, offset, buffer.length - offset, null) + if (bytesWritten <= 0) { + throw new Error('SSH relay runtime draft read-back could not persist returned bytes') + } + offset += bytesWritten + } +} + +async function hashResponse(response, expected, signal, file) { const contentLength = response.headers.get('content-length') if (contentLength !== null) { const parsed = Number(contentLength) @@ -189,14 +202,37 @@ async function hashResponse(response, expected, signal) { } const hash = createHash('sha256') let bytes = 0 - for await (const chunk of response.body) { - signal?.throwIfAborted() - const buffer = Buffer.from(chunk) - bytes += buffer.length - if (bytes > expected.size || bytes > MAX_ASSET_BYTES) { - throw new Error(`SSH relay runtime draft read-back size exceeded: ${expected.name}`) + const reader = response.body.getReader() + let bodyComplete = false + const cancelRead = () => { + void reader.cancel(signal.reason).catch(() => {}) + } + signal.addEventListener('abort', cancelRead, { once: true }) + try { + while (true) { + signal.throwIfAborted() + const { done, value } = await reader.read() + signal.throwIfAborted() + if (done) { + bodyComplete = true + break + } + const buffer = Buffer.from(value) + bytes += buffer.length + if (bytes > expected.size || bytes > MAX_ASSET_BYTES) { + throw new Error(`SSH relay runtime draft read-back size exceeded: ${expected.name}`) + } + hash.update(buffer) + if (file) { + await writeCompleteChunk(file, buffer) + } } - hash.update(buffer) + } finally { + signal.removeEventListener('abort', cancelRead) + if (!bodyComplete) { + await reader.cancel(signal.aborted ? signal.reason : undefined).catch(() => {}) + } + reader.releaseLock() } if (bytes !== expected.size) { throw new Error(`SSH relay runtime draft read-back size mismatch: ${expected.name}`) @@ -207,19 +243,13 @@ async function hashResponse(response, expected, signal) { } } -export async function verifySshRelayRuntimeDraftReadback({ - repo, - releaseId, - tag, - token, - expectedAssets, - fetchImpl = fetch, - signal -}) { - const effectiveSignal = signal +function effectiveReadbackSignal(signal) { + return signal ? AbortSignal.any([signal, AbortSignal.timeout(READBACK_TIMEOUT_MS)]) : AbortSignal.timeout(READBACK_TIMEOUT_MS) - effectiveSignal.throwIfAborted() +} + +function validateReadbackInput({ repo, releaseId, tag, token, expectedAssets }) { if (typeof repo !== 'string' || !REPOSITORY_PATTERN.test(repo)) { throw new Error('SSH relay runtime draft read-back repository is invalid') } @@ -232,15 +262,99 @@ export async function verifySshRelayRuntimeDraftReadback({ if (typeof token !== 'string' || token.length === 0) { throw new Error('SSH relay runtime draft read-back token is required') } - const expected = normalizeExpectedAssets(expectedAssets) - const release = await fetchRelease({ + return normalizeExpectedAssets(expectedAssets) +} + +async function validatedReleaseAssets({ + repo, + releaseId, + tag, + token, + expected, + fetchImpl, + signal +}) { + const release = await fetchRelease({ repo, releaseId, token, fetchImpl, signal }) + return validateRelease(release, releaseId, tag, expected) +} + +async function exclusiveOutputDirectory(outputDirectory) { + if (typeof outputDirectory !== 'string' || outputDirectory.length === 0) { + throw new Error('SSH relay runtime draft read-back output directory is required') + } + const absolute = resolve(outputDirectory) + let parent + try { + parent = resolve(await realpath(dirname(absolute))) + } catch (error) { + throw new Error( + 'SSH relay runtime draft read-back output parent must be an existing real directory', + { cause: error } + ) + } + const parentMetadata = await lstat(parent) + if (!parentMetadata.isDirectory() || parentMetadata.isSymbolicLink()) { + throw new Error('SSH relay runtime draft read-back output parent must be a real directory') + } + const output = resolve(parent, basename(absolute)) + try { + await lstat(output) + } catch (error) { + if (error.code === 'ENOENT') { + return output + } + throw error + } + throw new Error('SSH relay runtime draft read-back requires an exclusive absent output directory') +} + +async function downloadToTemporaryFile({ + repo, + token, + expected, + releaseAsset, + temporaryPath, + fetchImpl, + signal +}) { + const response = await fetchAsset({ + repo, + assetId: releaseAsset.id, + token, + fetchImpl, + signal + }) + const file = await open(temporaryPath, 'wx', 0o600) + try { + await hashResponse(response, expected, signal, file) + // Why: final names must expose only bytes that survived the complete streamed verification. + await file.sync() + } finally { + await file.close() + } +} + +export async function verifySshRelayRuntimeDraftReadback({ + repo, + releaseId, + tag, + token, + expectedAssets, + fetchImpl = fetch, + signal +}) { + const effectiveSignal = effectiveReadbackSignal(signal) + effectiveSignal.throwIfAborted() + const expected = validateReadbackInput({ repo, releaseId, tag, token, expectedAssets }) + const releaseAssets = await validatedReleaseAssets({ repo, releaseId, + tag, token, + expected, fetchImpl, signal: effectiveSignal }) - const releaseAssets = validateRelease(release, releaseId, tag, expected) for (const asset of expected) { effectiveSignal.throwIfAborted() const response = await fetchAsset({ @@ -254,3 +368,63 @@ export async function verifySshRelayRuntimeDraftReadback({ } return { releaseId, tag, downloadedAssets: expected } } + +export async function materializeSshRelayRuntimeDraftReadback({ + repo, + releaseId, + tag, + token, + expectedAssets, + outputDirectory, + fetchImpl = fetch, + signal +}) { + const effectiveSignal = effectiveReadbackSignal(signal) + effectiveSignal.throwIfAborted() + const expected = validateReadbackInput({ repo, releaseId, tag, token, expectedAssets }) + const output = await exclusiveOutputDirectory(outputDirectory) + let created = false + try { + await mkdir(output, { mode: 0o700 }) + created = true + const releaseAssets = await validatedReleaseAssets({ + repo, + releaseId, + tag, + token, + expected, + fetchImpl, + signal: effectiveSignal + }) + const temporaryAssets = [] + for (const [index, asset] of expected.entries()) { + effectiveSignal.throwIfAborted() + const temporaryPath = join(output, `.readback-partial-${index}`) + await downloadToTemporaryFile({ + repo, + token, + expected: asset, + releaseAsset: releaseAssets.get(asset.name), + temporaryPath, + fetchImpl, + signal: effectiveSignal + }) + temporaryAssets.push({ asset, temporaryPath, path: join(output, asset.name) }) + } + effectiveSignal.throwIfAborted() + for (const materialized of temporaryAssets) { + await rename(materialized.temporaryPath, materialized.path) + } + return { + releaseId, + tag, + materializedAssets: temporaryAssets.map(({ asset, path }) => ({ ...asset, path })) + } + } catch (error) { + if (created) { + // Why: later archive execution may consume only a complete read-back transaction. + await rm(output, { recursive: true, force: true }) + } + throw error + } +} 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 a5e8b0a7630..5141d7f6af0 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 @@ -16,9 +16,12 @@ in progress, 2026-07-15, Codex implementation owner.** Exact-head run target-native artifact jobs and the required-asset gate's 13-test suite on every Node 24 runner under `E-M4-RELEASE-ASSETS-CI-001`. PR Checks and Golden E2E are green; the artifact workflow is red only for the retained Windows ARM build-26200-versus-26100 floor mismatch after successful runtime smoke. -Next, add exclusive local materialization to the existing authenticated read-back without a second -download. Production keys/environment/seed, publication, desktop/default consumers, tuple enablement, -and merge to `main` remain disconnected. +Exclusive one-pass local materialization is now locally green under +`E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001`; exact-head proof on all six native Node 24 jobs is +next. Production keys/environment/seed, publication, desktop/default consumers, tuple enablement, +and merge to `main` remain disconnected. Current pushed branch head is `16edb7e75`; the +implementation owner is authorized to drive commits, pushes, CI, and PR-contained rehearsals end to +end without merging. ## Safety status @@ -271,7 +274,11 @@ and merge to `main` remain disconnected. A disconnected bounded upload/recovery implementation is locally green under `E-M4-DRAFT-UPLOAD-LOCAL-001` and passes all six native Node 24 jobs under `E-M4-DRAFT-UPLOAD-CI-001`; no real release write or downloaded archive execution has occurred, - so this item remains open. + so this item remains open. The purpose-named transactional materialization RED is recorded + under `E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001`; one-pass persistence, exclusive + temporary/final naming, cancellation/failure cleanup, and exact returned paths pass locally + under `E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001`. Exact-head all-six Node 24 proof and + downloaded archive execution are still required. - [ ] Test timeouts, retries, approval denial, signing failure, partial output, and draft recovery. ### WP4 — Desktop resolver and verified cache 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 93f6ae371ac..5e4c79c877d 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md @@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    -Current phase: Milestone 4 / Work Package 3 disconnected authenticated draft read-back materialization — **In progress — 2026-07-15, Codex implementation owner**. The relay-specific required-asset capability is closed locally and on all six exact-head native Node 24 jobs under E-M4-RELEASE-ASSETS-LOCAL-RED-001, E-M4-RELEASE-ASSETS-LOCAL-001, and E-M4-RELEASE-ASSETS-CI-001. It remains a disconnected pure metadata boundary; detached-signature byte encoding, the existing desktop required-asset gate, and workflow composition are unchanged. The next slice must add an exclusive local materialization boundary to the existing authenticated read-back so exact verified bytes can later enter archive extraction/execution without a second download. Begin with a purpose-named RED; keep release writes, publication, desktop consumers, and every tuple disconnected.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `5460563ff48846cbf54218860fdca1cc3c7b8614` passes the new 13-test release-asset gate and full runtime construction on all six native jobs in run 29433808457 under E-M4-RELEASE-ASSETS-CI-001. PR Checks 29433808934 and both Golden E2E jobs in 29433808372 are green. Linux supplements and the Windows x64 floor pass. Windows arm64 runtime smoke passes in 7,936.874ms with 49,553,408-byte RSS; its separate floor job then retains the expected fail-closed hosted-build 26200 versus required 26100 mismatch. There is no command-line entrypoint, release API call, upload/publication caller, desktop consumer, tuple enablement, or production/default behavior change. The user authorizes commits, pushes, draft-PR updates, CI reruns, and protected rehearsals within PRs, but not merging to `main`. No accepted production key, protected environment/seed, native-signing trust proof, release write, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
    +Current phase: Milestone 4 / Work Package 3 disconnected authenticated draft read-back materialization — **In progress — 2026-07-15, Codex implementation owner**. The relay-specific required-asset capability is closed locally and on all six exact-head native Node 24 jobs under E-M4-RELEASE-ASSETS-LOCAL-RED-001, E-M4-RELEASE-ASSETS-LOCAL-001, and E-M4-RELEASE-ASSETS-CI-001. The exclusive local materialization boundary is now locally green under E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001 and E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001; exact-head all-six native Node 24 proof remains required before this slice closes. It persists the existing authenticated response stream without a second download, exposes final names only after verification, and removes the complete output on failure or cancellation. Release writes, archive execution, publication, desktop consumers, and every tuple remain disconnected.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — current pushed branch head `16edb7e755d35eeec561adbfff07587f5fc0bdf2` records E-M4-RELEASE-ASSETS-CI-001; its exact code head `5460563ff48846cbf54218860fdca1cc3c7b8614` passes the new 13-test release-asset gate and full runtime construction on all six native jobs in run 29433808457. PR Checks 29433808934 and both Golden E2E jobs in 29433808372 are green. Linux supplements and the Windows x64 floor pass. Windows arm64 runtime smoke passes in 7,936.874ms with 49,553,408-byte RSS; its separate floor job then retains the expected fail-closed hosted-build 26200 versus required 26100 mismatch. The intended materialization module/test, native test wiring, and living-checklist updates are locally green and awaiting commit/push plus exact-head CI; `git diff --check` passes and the protected resolver/desktop gate files have zero diff. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no command-line entrypoint, release API write, upload/publication caller, archive execution, desktop consumer, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, release write, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -116,10 +116,12 @@ same change as the work it records. identity, and consumer disconnection pass locally and on all six exact-head jobs under E-M4-RELEASE-ASSETS-LOCAL-RED-001, E-M4-RELEASE-ASSETS-LOCAL-001, and E-M4-RELEASE-ASSETS-CI-001. Release workflow composition remains open. -- Active package: Work Package 3 disconnected authenticated draft read-back materialization. Begin - with a purpose-named RED for exclusive output, exact byte persistence during the existing bounded - authenticated download, mutation cleanup, cancellation, and consumer disconnection. Do not add - archive execution, release publication, or a production caller in this slice. +- Active package: Work Package 3 disconnected authenticated draft read-back materialization. The + purpose-named RED and local GREEN are recorded under + E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001 and + E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001. Exact-head all-six native Node 24 execution is the + next gate. Do not add archive execution, release publication, or a production caller in this + slice. - Completed Work Package 2 gate: target-native Windows source-signature reports from exact-head artifact jobs 87267322867 and 87267322870 were independently downloaded and matched to their identities and signing-stage reports under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001. PR Checks @@ -11490,6 +11492,91 @@ config/scripts/verify-release-required-assets.test.mjs` — PASS, 7 files / 64 t - Follow-up: checkpoint and push this evidence, then begin the disconnected authenticated draft read-back materialization RED. Retain all production/default disconnections. +### E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001 — Transactional materialization entrypoint is absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Worktree head before implementation: `16edb7e755d35eeec561adbfff07587f5fc0bdf2`; draft PR + #8741. The working tree contains only the session-checkpoint update, purpose-named RED test, and + POSIX/Windows native-job test wiring. +- Command: + `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs` +- Result: expected FAIL in 0.862s; 1 file / 4 tests fail only because + `materializeSshRelayRuntimeDraftReadback` is not a function. The existing verification-only + entrypoint remains present and unchanged. +- Contract pinned by the RED: one existing authenticated streamed download must persist exact bytes; + partial bytes cannot use the final asset name; the output directory must be absent beneath an + existing real parent before any GitHub request; a later asset failure or cancellation must remove + the entire output; success must return exact bounded asset paths. +- Does not prove: implementation, local GREEN, Node 24/native-runner portability, real authenticated + GitHub bytes, archive execution, release write/publication, desktop consumption, native trust, + SSH transfer/install, performance, or any enabled tuple. +- Follow-up: implement the new materialization entrypoint by sharing the existing bounded download + and hash pass. Preserve the verification-only API and every production/default disconnection. + +### E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001 — One-pass verified materialization passes locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted implementation and purpose-named test atop exact pushed head + `16edb7e755d35eeec561adbfff07587f5fc0bdf2`; draft PR #8741. +- Runner/remote/network: local macOS 26.2 arm64, Node v26.0.0 and pnpm 10.24.0. In-memory + authenticated release/asset response fixtures and local temporary files only. No real network, + credential, release write, signer, protected environment, SSH remote, desktop consumer, + publication, or enabled tuple. +- First implementation command: `node --check config/scripts/ssh-relay-runtime-draft-readback.mjs +&& node --check config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs && pnpm +exec vitest run --config config/vitest.config.ts +config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs +config/scripts/ssh-relay-runtime-draft-readback.test.mjs` — expected correction signal: 1 failed / 13 + passed because the test expected macOS's logical `/var` alias while the security boundary correctly + returned physical `/private/var`. The test now derives the physical parent with `realpath`; no + production path check was weakened. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs +config/scripts/ssh-relay-runtime-draft-readback.test.mjs` — PASS, 2 files / 14 tests in 739ms + Vitest and 2.16s wall; maximum RSS 132,464,640 bytes, peak footprint 96,703,936 bytes, and zero + swaps. +- Adjacent release command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 +config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs +config/scripts/ssh-relay-runtime-draft-readback.test.mjs +config/scripts/ssh-relay-runtime-draft-upload.test.mjs +config/scripts/ssh-relay-runtime-draft-recovery.test.mjs +config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs +config/scripts/ssh-relay-runtime-workflow.test.mjs +config/scripts/ssh-relay-runtime-release-assets.test.mjs +config/scripts/verify-release-required-assets.test.mjs` — PASS, 8 files / 68 tests in 2.02s + Vitest and 3.52s wall; maximum RSS 132,612,096 bytes, peak footprint 96,802,168 bytes, and zero + swaps. +- Broad release command: `/usr/bin/time -l sh -c "rg --files config/scripts | rg +'/ssh-relay.*[.]test[.]mjs$' | xargs pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1"` — PASS, 54 files / 287 tests in 10.73s Vitest and 11.98s wall; maximum RSS + 189,743,104 bytes and zero swaps. +- Static gates: `node --check` passes the production module and purpose-named test. `/usr/bin/time -l +pnpm run typecheck` passes in 2.83s wall with 1,257,357,312-byte maximum RSS and zero swaps. + `/usr/bin/time -l pnpm run lint` passes in 10.78s wall with 2,021,834,752-byte maximum RSS and + zero swaps; all 41 reliability gates and the 355-entry max-lines ratchet pass, and the existing 26 + unrelated warnings remain. Focused `oxfmt --check` and `git diff --check` pass. +- Oracle proved: the output must be absent below an existing physical parent before any GitHub + request. Each authenticated response is streamed once into an exclusive temporary file while the + existing size and SHA-256 pass runs; the file is synced and final names are published only after + every asset verifies. A later asset failure or mid-stream cancellation removes the entire output. + Success returns only the bounded validated assets with exact physical paths. The verification-only + API remains byte-for-byte compatible at its return boundary and its 10-test suite stays green. +- Consumer-disconnection oracle: repository search finds the new entrypoint only in its purpose-named + test; both native artifact job families perform syntax and test execution only. There is no CLI, + workflow caller, archive execution, release API write, publication, desktop consumer, tuple + enablement, or production/default behavior change. The protected Node/npm resolver files and + existing desktop required-asset gate retain zero diff. +- Does not prove: Node 24/native-runner behavior, real authenticated GitHub bytes, detached-signature + encoding, downloaded archive execution, real release write/publication, native signing/trust, + oldest OS floors, desktop embedding, SSH transfer/install, performance against legacy, or any + enabled tuple. +- Follow-up: commit and push the exact package, then require syntax and 4-test materialization + execution plus the full contract/runtime construction on all six exact-head native Node 24 jobs. + Keep every production/default consumer disconnected. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -11548,14 +11635,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Checkpoint and push E-M4-RELEASE-ASSETS-CI-001. Then add a purpose-named RED for exclusive local -materialization during the existing authenticated draft read-back: create only an absent output -directory, persist the exact already-streamed bytes without a second download, publish no partial -file before size/SHA-256 success, clean up on failure/cancellation, and return bounded asset paths -for the later archive-execution slice. Do not add archive execution, a workflow caller, or a real -release write in this package. Keep the existing desktop `verify-release-required-assets.mjs` -behavior and detached-signature byte encoding unchanged. The protected environment, accepted -production keys, and secret remain unprovisioned, so live signing evidence stays BLOCKED. +Commit and push E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001 and +E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001, then require exact-head syntax and focused execution +on all six native Node 24 build jobs plus the existing full artifact construction, PR Checks, and +Golden E2E. Do not add archive execution, a workflow caller, or a real release write in this +package. Keep the existing desktop `verify-release-required-assets.mjs` behavior and +detached-signature byte encoding unchanged. The protected environment, accepted production keys, +and secret remain unprovisioned, so live signing evidence stays BLOCKED. Keep Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. Do not merge to `main`; retain every production/default gate. From 94d9ee9cb90e4bffd2dafc5b7e8e532d197f70d0 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 13:33:52 -0400 Subject: [PATCH 142/267] docs: record relay read-back materialization proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 24 ++--- ...github-release-implementation-checklist.md | 88 +++++++++++++++---- 2 files changed, 85 insertions(+), 27 deletions(-) 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 5141d7f6af0..fb9029f1b3f 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 @@ -9,17 +9,16 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 4 / Work Package 3 disconnected authenticated draft read-back -materialization — +Active checkpoint: **Milestone 4 / Work Package 3 disconnected downloaded-archive execution — in progress, 2026-07-15, Codex implementation owner.** Exact-head run -[29433808457](https://github.com/stablyai/orca/actions/runs/29433808457) at `5460563ff` passes all six -target-native artifact jobs and the required-asset gate's 13-test suite on every Node 24 runner under -`E-M4-RELEASE-ASSETS-CI-001`. PR Checks and Golden E2E are green; the artifact workflow is red only -for the retained Windows ARM build-26200-versus-26100 floor mismatch after successful runtime smoke. -Exclusive one-pass local materialization is now locally green under -`E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001`; exact-head proof on all six native Node 24 jobs is -next. Production keys/environment/seed, publication, desktop/default consumers, tuple enablement, -and merge to `main` remain disconnected. Current pushed branch head is `16edb7e75`; the +[29435837082](https://github.com/stablyai/orca/actions/runs/29435837082) at `0fbbac118` passes all six +target-native artifact jobs and the 4-test materialization suite on every Node 24 runner under +`E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001`. PR Checks and Golden E2E are green; the artifact +workflow is red only for the retained Windows ARM build-26200-versus-26100 floor mismatch after +successful runtime smoke. Next, bind exact materialized paths to the existing exclusive extraction +and bundled Node/native PTY/watcher execution boundary. Production keys/environment/seed, +publication, desktop/default consumers, SSH transfer/install, tuple enablement, and merge to `main` +remain disconnected. Current pushed branch head is `0fbbac118`; the implementation owner is authorized to drive commits, pushes, CI, and PR-contained rehearsals end to end without merging. @@ -277,8 +276,9 @@ end without merging. so this item remains open. The purpose-named transactional materialization RED is recorded under `E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001`; one-pass persistence, exclusive temporary/final naming, cancellation/failure cleanup, and exact returned paths pass locally - under `E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001`. Exact-head all-six Node 24 proof and - downloaded archive execution are still required. + under `E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001`. Exact-head all-six Node 24 proof is + closed under `E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001`; downloaded archive execution and a + real release write/read-back are still required. - [ ] Test timeouts, retries, approval denial, signing failure, partial output, and draft recovery. ### WP4 — Desktop resolver and verified cache 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 5e4c79c877d..1fa8f2d3a9b 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md @@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    -Current phase: Milestone 4 / Work Package 3 disconnected authenticated draft read-back materialization — **In progress — 2026-07-15, Codex implementation owner**. The relay-specific required-asset capability is closed locally and on all six exact-head native Node 24 jobs under E-M4-RELEASE-ASSETS-LOCAL-RED-001, E-M4-RELEASE-ASSETS-LOCAL-001, and E-M4-RELEASE-ASSETS-CI-001. The exclusive local materialization boundary is now locally green under E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001 and E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001; exact-head all-six native Node 24 proof remains required before this slice closes. It persists the existing authenticated response stream without a second download, exposes final names only after verification, and removes the complete output on failure or cancellation. Release writes, archive execution, publication, desktop consumers, and every tuple remain disconnected.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — current pushed branch head `16edb7e755d35eeec561adbfff07587f5fc0bdf2` records E-M4-RELEASE-ASSETS-CI-001; its exact code head `5460563ff48846cbf54218860fdca1cc3c7b8614` passes the new 13-test release-asset gate and full runtime construction on all six native jobs in run 29433808457. PR Checks 29433808934 and both Golden E2E jobs in 29433808372 are green. Linux supplements and the Windows x64 floor pass. Windows arm64 runtime smoke passes in 7,936.874ms with 49,553,408-byte RSS; its separate floor job then retains the expected fail-closed hosted-build 26200 versus required 26100 mismatch. The intended materialization module/test, native test wiring, and living-checklist updates are locally green and awaiting commit/push plus exact-head CI; `git diff --check` passes and the protected resolver/desktop gate files have zero diff. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no command-line entrypoint, release API write, upload/publication caller, archive execution, desktop consumer, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, release write, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
    +Current phase: Milestone 4 / Work Package 3 disconnected downloaded-archive execution — **In progress — 2026-07-15, Codex implementation owner**. Exclusive authenticated draft read-back materialization is closed locally and on all six exact-head native Node 24 jobs under E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001, E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001, and E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001. The next slice must prove that only exact materialized archive paths bound to their verified tuple identity can enter the existing exclusive extraction and bundled Node/native PTY/watcher execution boundary. Begin with a purpose-named disconnected RED; do not add a real release write, production workflow caller, publication, desktop consumer, SSH transfer, tuple enablement, or default behavior.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `0fbbac118e08a17f8263ad48f116a8d61362fe92` passes the materialization suite and full runtime construction on all six native jobs in run 29435837082 under E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001. PR Checks 29435836210 and both Golden E2E jobs in 29435835950 are green. Linux supplements and the Windows x64 floor pass. Windows arm64 runtime smoke passes in 6,121.6774ms with 48,623,616-byte RSS; its separate floor job then retains the expected fail-closed hosted-build 26200 versus required 26100 mismatch. The worktree is clean; `git diff --check` passed before commit and the protected resolver/desktop gate files have zero diff. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, upload/publication caller, downloaded-archive execution consumer, desktop consumer, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -116,12 +116,15 @@ same change as the work it records. identity, and consumer disconnection pass locally and on all six exact-head jobs under E-M4-RELEASE-ASSETS-LOCAL-RED-001, E-M4-RELEASE-ASSETS-LOCAL-001, and E-M4-RELEASE-ASSETS-CI-001. Release workflow composition remains open. -- Active package: Work Package 3 disconnected authenticated draft read-back materialization. The - purpose-named RED and local GREEN are recorded under - E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001 and - E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001. Exact-head all-six native Node 24 execution is the - next gate. Do not add archive execution, release publication, or a production caller in this - slice. +- Completed Work Package 3 gate: disconnected authenticated draft read-back materialization. The + purpose-named RED, local GREEN, and exact-head all-six native Node 24 execution are recorded under + E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001, + E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001, and + E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001. +- Active package: Work Package 3 disconnected downloaded-archive execution. Begin with a + purpose-named RED binding exact materialized paths to the verified tuple identity, existing + exclusive extraction, and bundled Node/native PTY/watcher smoke. Do not add a real release write, + publication, desktop consumer, SSH transfer/install, or production caller in this slice. - Completed Work Package 2 gate: target-native Windows source-signature reports from exact-head artifact jobs 87267322867 and 87267322870 were independently downloaded and matched to their identities and signing-stage reports under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001. PR Checks @@ -11577,6 +11580,59 @@ pnpm run typecheck` passes in 2.83s wall with 1,257,357,312-byte maximum RSS and execution plus the full contract/runtime construction on all six exact-head native Node 24 jobs. Keep every production/default consumer disconnected. +### E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001 — Verified materialization passes all six native Node 24 jobs + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `0fbbac118e08a17f8263ad48f116a8d61362fe92`; draft PR #8741. +- Workflow: [SSH Relay Runtime Artifacts run 29435837082](https://github.com/stablyai/orca/actions/runs/29435837082), + 17:16:47Z–17:30:29Z. The aggregate conclusion is the expected failure only because the separate + Windows arm64 floor job rejects hosted build 26200 after successful exact-byte runtime execution. +- Native build jobs, runner 2.335.1 throughout: + - Linux x64 job 87422172377, `ubuntu-24.04` image `20260714.240.1`, PASS in 4m21s. + - Darwin arm64 job 87422172403, `macos-15-arm64` image `20260706.0213.1`, PASS in 3m41s. + - Windows x64 job 87422172406, `windows-2022` image `20260714.244.1`, PASS in 6m13s. + - Linux arm64 job 87422172462, `ubuntu-24.04-arm` image `20260706.52.2`, PASS in 4m28s. + - Windows arm64 job 87422172494, `windows-11-arm64` image `20260714.109.1`, PASS in 9m27s. + - Darwin x64 job 87422172522, `macos-15` image `20260629.0276.1`, PASS in 4m48s. +- Exact materialization suite: + `config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs` passes 4/4 under Node + 24.18.0 on every native job: Linux x64 328ms, Darwin arm64 168ms, Windows x64 264ms, Linux arm64 + 181ms, Windows arm64 260ms, and Darwin x64 237ms. +- Broad contract counts: every POSIX job passes 53 files / 283 tests; durations are Linux x64 7.17s, + Darwin arm64 10.73s, Linux arm64 5.79s, and Darwin x64 17.35s. Each Windows job passes 54 files / + 277 tests plus 10 platform skips of 287; x64 takes 16.25s and arm64 16.55s. +- Downstream evidence: Linux x64 supplement job 87423276409 passes in 32s; Linux arm64 supplement job + 87423276419 passes in 1m10s. Windows x64 floor job 87424346586 passes on Server 2022 build 20348 + in 2m17s; bundled runtime smoke takes 5,368.7141ms with 49,999,872-byte RSS. Windows arm64 floor + job 87424346592 uses `windows-11-arm64` image `20260714.109.1`, executes bundled Node 24.18.0 and + native PTY/watcher smoke in 6,121.6774ms with 48,623,616-byte RSS, and then fails exactly because + observed OS/kernel build 26200 is not the required floor 26100. +- Adjacent exact-head workflows: PR Checks run + [29435836210](https://github.com/stablyai/orca/actions/runs/29435836210), job 87422140840, passes + 17:16:50Z–17:27:52Z including lint, typecheck, Git compatibility, tests, unpacked app build, and + packaged CLI smoke. Golden E2E run + [29435835950](https://github.com/stablyai/orca/actions/runs/29435835950) passes macOS job + 87422139557 and Linux job 87422139620. +- Oracle proved: the purpose-named materialization suite and POSIX/PowerShell syntax/test wiring parse + and execute consistently on all six native Node 24 runner families. Every family proves one-pass + streamed persistence, exclusive physical output, temporary-only partial bytes, final-name exposure + only after complete verification, whole-output cleanup after later failure, mid-stream cancellation + settlement, exact physical returned paths, and unchanged verification-only behavior. Full + target-native runtime construction and adjacent product regressions remain green. +- Consumer-disconnection oracle: both native workflow families execute only the contract test. There + is no CLI, release write, production workflow caller, publication, archive execution, desktop + consumer, SSH transfer/install, tuple enablement, or default behavior change. +- Does not prove: a real authenticated GitHub response or full-size network transfer, detached- + signature encoding, downloaded archive execution, real release write/publication, native signing/ + trust, oldest macOS/Linux-kernel/Windows-arm64 floors, desktop embedding, SSH transfer/install, + performance against legacy, or any enabled tuple. +- Checklist items satisfied: exact-head all-six CI gate for disconnected authenticated read-back + materialization. The broader real draft upload/read-back/execution and release composition boxes + remain open. +- Follow-up: checkpoint this evidence, then begin the purpose-named disconnected downloaded-archive + execution RED. Retain all production/default disconnections. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -11635,13 +11691,15 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001 and -E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001, then require exact-head syntax and focused execution -on all six native Node 24 build jobs plus the existing full artifact construction, PR Checks, and -Golden E2E. Do not add archive execution, a workflow caller, or a real release write in this -package. Keep the existing desktop `verify-release-required-assets.mjs` behavior and -detached-signature byte encoding unchanged. The protected environment, accepted production keys, -and secret remain unprovisioned, so live signing evidence stays BLOCKED. +Begin the purpose-named disconnected downloaded-archive execution RED. It must accept only exact +materialized asset paths bound to the verified tuple identity, route the archive through the existing +exclusive extraction/full-tree verification boundary, and execute bundled Node plus native PTY/ +watcher smoke before reporting success. It must fail closed and clean exclusive output on path, +identity, extraction, execution, cancellation, or mutation failure. Do not add a real release write, +publication, desktop consumer, SSH transfer/install, tuple enablement, or production caller in this +package. Keep the existing desktop `verify-release-required-assets.mjs` behavior and detached- +signature byte encoding unchanged. The protected environment, accepted production keys, and secret +remain unprovisioned, so live signing evidence stays BLOCKED. Keep Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. Do not merge to `main`; retain every production/default gate. From b8c42f92abeb0bda2dac94b7257913a749322c25 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 13:51:33 -0400 Subject: [PATCH 143/267] build(ssh): execute verified relay runtime archives Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 51 ++++ .../ssh-relay-runtime-archive-extraction.mjs | 2 +- ...lay-runtime-readback-archive-execution.mjs | 192 +++++++++++++ ...untime-readback-archive-execution.test.mjs | 254 ++++++++++++++++++ config/scripts/verify-ssh-relay-runtime.mjs | 47 +++- ...elease-implementation-checklist-summary.md | 12 +- ...github-release-implementation-checklist.md | 96 ++++++- 7 files changed, 633 insertions(+), 21 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-readback-archive-execution.mjs create mode 100644 config/scripts/ssh-relay-runtime-readback-archive-execution.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 7dbaa410423..e2e68e97cd6 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -219,6 +219,8 @@ jobs: node --check config/scripts/ssh-relay-runtime-draft-readback.mjs node --check config/scripts/ssh-relay-runtime-draft-readback.test.mjs node --check config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs + node --check config/scripts/ssh-relay-runtime-readback-archive-execution.mjs + node --check config/scripts/ssh-relay-runtime-readback-archive-execution.test.mjs node --check config/scripts/ssh-relay-runtime-draft-upload.mjs node --check config/scripts/ssh-relay-runtime-draft-upload.test.mjs node --check config/scripts/ssh-relay-runtime-release-assets.mjs @@ -279,6 +281,7 @@ jobs: config/scripts/ssh-relay-runtime-aggregate-input.test.mjs \ config/scripts/ssh-relay-runtime-draft-readback.test.mjs \ config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs \ + config/scripts/ssh-relay-runtime-readback-archive-execution.test.mjs \ config/scripts/ssh-relay-runtime-draft-upload.test.mjs \ config/scripts/ssh-relay-runtime-release-assets.test.mjs \ config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs \ @@ -376,6 +379,24 @@ jobs: --git-commit "$source_commit" first_output="$output_root/first" identity="$first_output/orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json" + archive=$(find "$first_output" -maxdepth 1 -name '*.tar.xz' -print -quit) + test -n "$archive" + executed_runtime="$output_root/readback-executed" + # Why: exercise only local verified bytes; release writes and remote consumers stay disconnected. + docker run --rm --network none --read-only --cap-drop all \ + --user "$(id -u):$(id -g)" \ + --security-opt no-new-privileges --pids-limit 512 --memory 6g --cpus 4 \ + --tmpfs /tmp:rw,nosuid,size=1g,mode=1777 \ + --mount "type=bind,src=$GITHUB_WORKSPACE,dst=$GITHUB_WORKSPACE" \ + --mount "type=bind,src=$RUNNER_TEMP,dst=$RUNNER_TEMP" \ + --workdir "$GITHUB_WORKSPACE" \ + "$ORCA_LINUX_BUILDER" \ + /usr/bin/node config/scripts/ssh-relay-runtime-readback-archive-execution.mjs \ + --identity "$identity" \ + --archive "$archive" \ + --output-directory "$executed_runtime" + rm -rf -- "$executed_runtime" + test ! -e "$executed_runtime" signing_stage="$output_root/signing-stage" signing_report="$output_root/orca-ssh-relay-runtime-${{ matrix.tuple }}.signing-stage.json" node config/scripts/ssh-relay-runtime-native-signing-stage.mjs \ @@ -436,6 +457,17 @@ jobs: --tuple '${{ matrix.tuple }}' \ --first-output-directory "$first_output" \ --second-output-directory "$second_output" + identity="$first_output/orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json" + archive=$(find "$first_output" -maxdepth 1 -name '*.tar.xz' -print -quit) + test -n "$archive" + executed_runtime="$output_root/readback-executed" + # Why: exercise only local verified bytes; release writes and remote consumers stay disconnected. + node config/scripts/ssh-relay-runtime-readback-archive-execution.mjs \ + --identity "$identity" \ + --archive "$archive" \ + --output-directory "$executed_runtime" + rm -rf -- "$executed_runtime" + test ! -e "$executed_runtime" signing_stage="$output_root/signing-stage" signing_report="$output_root/orca-ssh-relay-runtime-${{ matrix.tuple }}.signing-stage.json" node config/scripts/ssh-relay-runtime-native-signing-stage.mjs \ @@ -644,6 +676,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime draft read-back test syntax check failed' } node --check config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime draft read-back materialization test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-readback-archive-execution.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime read-back archive execution syntax check failed' } + node --check config/scripts/ssh-relay-runtime-readback-archive-execution.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime read-back archive execution test syntax check failed' } node --check config/scripts/ssh-relay-runtime-draft-upload.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime draft upload syntax check failed' } node --check config/scripts/ssh-relay-runtime-draft-upload.test.mjs @@ -737,6 +773,7 @@ jobs: config/scripts/ssh-relay-runtime-aggregate-input.test.mjs ` config/scripts/ssh-relay-runtime-draft-readback.test.mjs ` config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs ` + config/scripts/ssh-relay-runtime-readback-archive-execution.test.mjs ` config/scripts/ssh-relay-runtime-draft-upload.test.mjs ` config/scripts/ssh-relay-runtime-release-assets.test.mjs ` config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs ` @@ -863,6 +900,20 @@ jobs: } throw 'runtime reproducibility verification failed' } + $identity = Join-Path $firstOutput 'orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json' + $archive = Get-ChildItem -LiteralPath $firstOutput -Filter '*.zip' -File | Select-Object -First 1 + if (-not $archive) { throw 'runtime read-back archive is missing' } + $executedRuntime = Join-Path $outputRoot 'readback-executed' + # Why: exercise only local verified bytes; release writes and remote consumers stay disconnected. + node config/scripts/ssh-relay-runtime-readback-archive-execution.mjs ` + --identity $identity ` + --archive $archive.FullName ` + --output-directory $executedRuntime + if ($LASTEXITCODE -ne 0) { throw 'runtime read-back archive execution failed' } + Remove-Item -LiteralPath $executedRuntime -Recurse -Force + if (Test-Path -LiteralPath $executedRuntime) { + throw 'runtime read-back archive execution cleanup failed' + } $signingStage = Join-Path $outputRoot 'signing-stage' $signingReport = Join-Path $outputRoot 'orca-ssh-relay-runtime-${{ matrix.tuple }}.signing-stage.json' $reportLines = & node config/scripts/ssh-relay-runtime-native-signing-stage.mjs ` diff --git a/config/scripts/ssh-relay-runtime-archive-extraction.mjs b/config/scripts/ssh-relay-runtime-archive-extraction.mjs index cd07a2b3a0a..b0491aec047 100644 --- a/config/scripts/ssh-relay-runtime-archive-extraction.mjs +++ b/config/scripts/ssh-relay-runtime-archive-extraction.mjs @@ -170,7 +170,7 @@ export async function extractSshRelayRuntimeArchive({ signal: effectiveSignal }) : extractTarXz(absoluteArchive, physicalOutput, effectiveSignal)) - const tree = await verifyRuntimeTree(physicalOutput, finalIdentity) + const tree = await verifyRuntimeTree(physicalOutput, finalIdentity, { signal: effectiveSignal }) const after = await describeArchive(absoluteArchive, identity.archive.size, effectiveSignal) if (!sameFileState(before.state, after.state) || before.sha256 !== after.sha256) { throw new Error('Runtime archive extraction input changed during extraction') diff --git a/config/scripts/ssh-relay-runtime-readback-archive-execution.mjs b/config/scripts/ssh-relay-runtime-readback-archive-execution.mjs new file mode 100644 index 00000000000..1a21d898dd3 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-readback-archive-execution.mjs @@ -0,0 +1,192 @@ +import { readFile, realpath, rm } from 'node:fs/promises' +import { basename, dirname, isAbsolute, resolve } from 'node:path' +import { pathToFileURL } from 'node:url' + +import { extractSshRelayRuntimeArchive } from './ssh-relay-runtime-archive-extraction.mjs' +import { verifyExtractedSshRelayRuntime } from './verify-ssh-relay-runtime.mjs' + +const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u +const MATERIALIZED_ARCHIVE_FIELDS = ['name', 'path', 'sha256', 'size'] +const ARGUMENT_FIELDS = new Map([ + ['--identity', 'identityPath'], + ['--archive', 'archivePath'], + ['--output-directory', 'outputDirectory'] +]) + +function assertObject(value, label) { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`SSH relay runtime read-back archive execution ${label} must be an object`) + } +} + +async function exactMaterializedArchive(materializedArchive, identity) { + assertObject(identity, 'identity') + assertObject(identity.archive, 'identity archive') + assertObject(materializedArchive, 'materialized archive') + if ( + JSON.stringify(Object.keys(materializedArchive).sort()) !== + JSON.stringify([...MATERIALIZED_ARCHIVE_FIELDS].sort()) + ) { + throw new Error( + 'SSH relay runtime read-back archive execution materialized archive fields are invalid' + ) + } + if ( + typeof materializedArchive.name !== 'string' || + typeof materializedArchive.path !== 'string' || + !isAbsolute(materializedArchive.path) || + resolve(materializedArchive.path) !== materializedArchive.path || + typeof materializedArchive.sha256 !== 'string' || + !DIGEST_PATTERN.test(materializedArchive.sha256) || + !Number.isSafeInteger(materializedArchive.size) || + materializedArchive.size <= 0 || + materializedArchive.name !== identity.archive.name || + materializedArchive.sha256 !== identity.archive.sha256 || + materializedArchive.size !== identity.archive.size || + basename(materializedArchive.path) !== materializedArchive.name + ) { + throw new Error( + 'SSH relay runtime read-back archive execution materialized archive identity drifted' + ) + } + const physicalParent = resolve(await realpath(dirname(materializedArchive.path))) + const physicalPath = resolve(physicalParent, basename(materializedArchive.path)) + if (physicalPath !== materializedArchive.path) { + throw new Error( + 'SSH relay runtime read-back archive execution requires a physical materialized path' + ) + } + return { ...materializedArchive } +} + +async function physicalOutputDirectory(outputDirectory) { + if (typeof outputDirectory !== 'string' || outputDirectory.length === 0) { + throw new Error('SSH relay runtime read-back archive execution output directory is required') + } + const absolute = resolve(outputDirectory) + const physicalParent = resolve(await realpath(dirname(absolute))) + return resolve(physicalParent, basename(absolute)) +} + +export function parseSshRelayRuntimeReadbackArchiveExecutionArguments(argv) { + const result = {} + for (let index = 0; index < argv.length; index += 2) { + const flag = argv[index] + const field = ARGUMENT_FIELDS.get(flag) + const value = argv[index + 1] + if (!field || !value || value.startsWith('--') || result[field]) { + throw new Error(`Invalid runtime read-back archive execution argument: ${flag}`) + } + result[field] = resolve(value) + } + if (Object.keys(result).length !== ARGUMENT_FIELDS.size) { + throw new Error('Runtime read-back archive execution requires identity, archive, and output') + } + return result +} + +export async function executeSshRelayRuntimeReadbackArchive({ + identity, + materializedArchive, + outputDirectory, + signal, + extractImpl = extractSshRelayRuntimeArchive, + verifyImpl = verifyExtractedSshRelayRuntime +}) { + signal?.throwIfAborted() + const archive = await exactMaterializedArchive(materializedArchive, identity) + const expectedRuntimeRoot = await physicalOutputDirectory(outputDirectory) + let extractedRuntime + try { + const extracted = await extractImpl({ + archivePath: archive.path, + outputDirectory: expectedRuntimeRoot, + identity, + signal + }) + // Why: a successful extractor owns this exclusive path, so later failures may safely remove it. + extractedRuntime = expectedRuntimeRoot + if ( + extracted?.tupleId !== identity.tupleId || + extracted?.runtimeRoot !== expectedRuntimeRoot || + extracted?.tree?.contentId !== identity.contentId + ) { + throw new Error('SSH relay runtime read-back archive extraction identity drifted') + } + signal?.throwIfAborted() + const verified = await verifyImpl({ + runtimeDirectory: extracted.runtimeRoot, + identity, + archivePath: archive.path, + signal + }) + signal?.throwIfAborted() + if ( + verified?.tuple !== identity.tupleId || + verified?.tree?.contentId !== identity.contentId || + verified?.smoke === null || + typeof verified?.smoke !== 'object' + ) { + throw new Error('SSH relay runtime read-back archive execution identity drifted') + } + return { + tupleId: identity.tupleId, + contentId: identity.contentId, + archive, + runtimeRoot: extracted.runtimeRoot, + tree: verified.tree, + smoke: verified.smoke, + durationMs: verified.durationMs + } + } catch (error) { + if (extractedRuntime) { + // Why: a downloaded archive may be retained only after complete bundled-native execution. + await rm(extractedRuntime, { recursive: true, force: true }) + } + throw error + } +} + +export async function executeSshRelayRuntimeReadbackArchiveFromPaths({ + identityPath, + archivePath, + outputDirectory, + signal, + extractImpl, + verifyImpl +}) { + const [identityBytes, physicalArchivePath] = await Promise.all([ + readFile(identityPath, { encoding: 'utf8', signal }), + realpath(archivePath) + ]) + const identity = JSON.parse(identityBytes) + // Why: runner temp roots may be aliases; the execution boundary accepts only physical paths. + return executeSshRelayRuntimeReadbackArchive({ + identity, + materializedArchive: { + name: identity.archive?.name, + path: physicalArchivePath, + sha256: identity.archive?.sha256, + size: identity.archive?.size + }, + outputDirectory, + signal, + extractImpl, + verifyImpl + }) +} + +async function main() { + const options = parseSshRelayRuntimeReadbackArchiveExecutionArguments(process.argv.slice(2)) + const result = await executeSshRelayRuntimeReadbackArchiveFromPaths(options) + 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 read-back archive execution failed: ${error.stack ?? error}\n` + ) + process.exitCode = 1 + }) +} diff --git a/config/scripts/ssh-relay-runtime-readback-archive-execution.test.mjs b/config/scripts/ssh-relay-runtime-readback-archive-execution.test.mjs new file mode 100644 index 00000000000..db7c6589601 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-readback-archive-execution.test.mjs @@ -0,0 +1,254 @@ +import { createHash } from 'node:crypto' +import { + mkdir, + mkdtemp, + readFile, + readdir, + realpath, + rm, + symlink, + writeFile +} from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { + executeSshRelayRuntimeReadbackArchive, + executeSshRelayRuntimeReadbackArchiveFromPaths, + parseSshRelayRuntimeReadbackArchiveExecutionArguments +} from './ssh-relay-runtime-readback-archive-execution.mjs' + +const ARCHIVE_NAME = 'orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.xz' +const ARCHIVE_BYTES = Buffer.from('verified materialized archive bytes') +const ARCHIVE_SHA256 = `sha256:${createHash('sha256').update(ARCHIVE_BYTES).digest('hex')}` + +let root +let archivePath + +function identity() { + return { + tupleId: 'linux-x64-glibc', + contentId: `sha256:${'a'.repeat(64)}`, + archive: { + name: ARCHIVE_NAME, + sha256: ARCHIVE_SHA256, + size: ARCHIVE_BYTES.length + } + } +} + +function materializedArchive(overrides = {}) { + return { + name: ARCHIVE_NAME, + path: archivePath, + sha256: ARCHIVE_SHA256, + size: ARCHIVE_BYTES.length, + ...overrides + } +} + +function input(overrides = {}) { + return { + identity: identity(), + materializedArchive: materializedArchive(), + outputDirectory: join(root, 'executed-runtime'), + ...overrides + } +} + +beforeEach(async () => { + root = await realpath(await mkdtemp(join(tmpdir(), 'orca-relay-readback-execution-'))) + archivePath = join(root, ARCHIVE_NAME) + await writeFile(archivePath, ARCHIVE_BYTES) +}) + +afterEach(async () => { + await rm(root, { recursive: true, force: true }) + vi.restoreAllMocks() +}) + +describe('SSH relay runtime read-back archive execution', () => { + it('runs real archives in all three native job families without release or consumer wiring', async () => { + const workflow = await readFile( + resolve(import.meta.dirname, '../../.github/workflows/ssh-relay-runtime-artifacts.yml'), + 'utf8' + ) + + expect(workflow.match(/exercise only local verified bytes/g)).toHaveLength(3) + expect(workflow.match(/--output-directory "\$executed_runtime"/g)).toHaveLength(2) + expect(workflow.match(/--output-directory \$executedRuntime/g)).toHaveLength(1) + expect(workflow).toContain('docker run --rm --network none --read-only --cap-drop all') + expect(workflow).toContain('rm -rf -- "$executed_runtime"') + expect(workflow).toContain('Remove-Item -LiteralPath $executedRuntime -Recurse -Force') + for (const tuple of [ + 'linux-x64-glibc', + 'linux-arm64-glibc', + 'darwin-x64', + 'darwin-arm64', + 'win32-x64', + 'win32-arm64' + ]) { + expect(workflow).toContain(`tuple: ${tuple}`) + } + expect(workflow).not.toMatch(/ssh-relay-runtime-readback-archive-execution[^\n]*publish/) + }) + + it('requires exact resolved inputs for the disconnected native-runner rehearsal', () => { + expect( + parseSshRelayRuntimeReadbackArchiveExecutionArguments([ + '--identity', + 'runtime.identity.json', + '--archive', + ARCHIVE_NAME, + '--output-directory', + 'reconstructed-runtime' + ]) + ).toEqual({ + identityPath: resolve('runtime.identity.json'), + archivePath: resolve(ARCHIVE_NAME), + outputDirectory: resolve('reconstructed-runtime') + }) + expect(() => + parseSshRelayRuntimeReadbackArchiveExecutionArguments([ + '--identity', + 'runtime.identity.json', + '--archive', + ARCHIVE_NAME + ]) + ).toThrow(/requires identity, archive, and output/i) + }) + + it('canonicalizes a runner temp alias before entering the physical-path boundary', async () => { + const alias = join(root, 'runner-temp-alias') + await symlink(root, alias, process.platform === 'win32' ? 'junction' : 'dir') + const identityPath = join(root, 'runtime.identity.json') + await writeFile(identityPath, JSON.stringify(identity())) + const outputDirectory = join(root, 'alias-executed-runtime') + const extractImpl = vi.fn(async ({ archivePath: candidate }) => { + expect(candidate).toBe(archivePath) + await mkdir(outputDirectory) + return { + tupleId: identity().tupleId, + runtimeRoot: outputDirectory, + tree: { contentId: identity().contentId } + } + }) + const verifyImpl = vi.fn(async () => ({ + tuple: identity().tupleId, + tree: { contentId: identity().contentId }, + smoke: { nodeVersion: 'v24.18.0' }, + durationMs: 8 + })) + + await expect( + executeSshRelayRuntimeReadbackArchiveFromPaths({ + identityPath, + archivePath: join(alias, ARCHIVE_NAME), + outputDirectory, + extractImpl, + verifyImpl + }) + ).resolves.toMatchObject({ + archive: { path: archivePath }, + runtimeRoot: outputDirectory + }) + }) + + it('extracts the exact materialized archive before bundled runtime smoke', async () => { + const order = [] + const outputDirectory = join(root, 'executed-runtime') + const extractImpl = vi.fn(async (options) => { + order.push('extract') + expect(options).toMatchObject({ archivePath, outputDirectory, identity: identity() }) + await mkdir(outputDirectory) + return { + tupleId: identity().tupleId, + runtimeRoot: outputDirectory, + tree: { contentId: identity().contentId } + } + }) + const verifyImpl = vi.fn(async (options) => { + order.push('verify-and-smoke') + expect(options).toMatchObject({ + archivePath, + runtimeDirectory: outputDirectory, + identity: identity() + }) + return { + tuple: identity().tupleId, + tree: { contentId: identity().contentId }, + smoke: { nodeVersion: 'v24.18.0' }, + durationMs: 12 + } + }) + + await expect( + executeSshRelayRuntimeReadbackArchive(input({ extractImpl, verifyImpl, outputDirectory })) + ).resolves.toEqual({ + tupleId: identity().tupleId, + contentId: identity().contentId, + archive: materializedArchive(), + runtimeRoot: outputDirectory, + tree: { contentId: identity().contentId }, + smoke: { nodeVersion: 'v24.18.0' }, + durationMs: 12 + }) + expect(order).toEqual(['extract', 'verify-and-smoke']) + }) + + it.each([ + ['unexpected fields', { extra: true }], + ['archive name', { name: `${ARCHIVE_NAME}.changed` }], + ['archive digest', { sha256: `sha256:${'b'.repeat(64)}` }], + ['archive size', { size: ARCHIVE_BYTES.length + 1 }], + ['physical absolute path', { path: ARCHIVE_NAME }] + ])('rejects materialized %s drift before extraction', async (_label, changed) => { + const extractImpl = vi.fn() + + await expect( + executeSshRelayRuntimeReadbackArchive( + input({ materializedArchive: materializedArchive(changed), extractImpl }) + ) + ).rejects.toThrow(/materialized|archive|path|field|identity/i) + expect(extractImpl).not.toHaveBeenCalled() + }) + + it('removes the extracted runtime after smoke, cancellation, or result-identity failure', async () => { + for (const failure of ['smoke', 'cancel', 'identity']) { + const outputDirectory = join(root, `failed-${failure}`) + const controller = new AbortController() + const extractImpl = vi.fn(async () => { + await mkdir(outputDirectory) + return { + tupleId: identity().tupleId, + runtimeRoot: outputDirectory, + tree: { contentId: identity().contentId } + } + }) + const verifyImpl = vi.fn(async () => { + if (failure === 'cancel') { + controller.abort(new Error('cancel archive execution')) + controller.signal.throwIfAborted() + } + if (failure === 'smoke') { + throw new Error('bundled watcher smoke failed') + } + return { + tuple: 'darwin-arm64', + tree: { contentId: identity().contentId }, + smoke: {}, + durationMs: 1 + } + }) + + await expect( + executeSshRelayRuntimeReadbackArchive( + input({ outputDirectory, extractImpl, verifyImpl, signal: controller.signal }) + ) + ).rejects.toThrow(/smoke|cancel|identity/i) + await expect(readdir(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' }) + } + }) +}) diff --git a/config/scripts/verify-ssh-relay-runtime.mjs b/config/scripts/verify-ssh-relay-runtime.mjs index f849367cbba..4e655814f2b 100644 --- a/config/scripts/verify-ssh-relay-runtime.mjs +++ b/config/scripts/verify-ssh-relay-runtime.mjs @@ -60,11 +60,13 @@ function parseArguments(argv) { return result } -async function actualTree(runtimeDirectory) { +async function actualTree(runtimeDirectory, signal) { const results = new Map() async function visit(directory) { + signal?.throwIfAborted() const children = await readdir(directory, { withFileTypes: true }) for (const child of children) { + signal?.throwIfAborted() const absolutePath = join(directory, child.name) const path = relative(runtimeDirectory, absolutePath).split(sep).join('/') if (results.has(path.toLowerCase())) { @@ -79,7 +81,10 @@ async function actualTree(runtimeDirectory) { }) await visit(absolutePath) } else if (child.isFile()) { - const [metadata, bytes] = await Promise.all([stat(absolutePath), readFile(absolutePath)]) + const [metadata, bytes] = await Promise.all([ + stat(absolutePath), + readFile(absolutePath, { signal }) + ]) results.set(path.toLowerCase(), { path, type: 'file', @@ -96,9 +101,10 @@ async function actualTree(runtimeDirectory) { return results } -export async function verifyRuntimeTree(runtimeDirectory, identity) { - const actual = await actualTree(runtimeDirectory) +export async function verifyRuntimeTree(runtimeDirectory, identity, { signal } = {}) { + const actual = await actualTree(runtimeDirectory, signal) for (const expected of identity.entries) { + signal?.throwIfAborted() const entry = actual.get(expected.path.toLowerCase()) if ( !entry || @@ -132,7 +138,7 @@ export async function verifyRuntimeTree(runtimeDirectory, identity) { } } -async function runBundledSmoke(runtimeDirectory, identity) { +async function runBundledSmoke(runtimeDirectory, identity, signal) { const nodePath = join(runtimeDirectory, 'bin', identity.os === 'win32' ? 'node.exe' : 'node') const childPath = resolve(scriptDirectory, 'ssh-relay-runtime-smoke-child.cjs') let result @@ -142,7 +148,8 @@ async function runBundledSmoke(runtimeDirectory, identity) { encoding: 'utf8', maxBuffer: MAX_SMOKE_OUTPUT_BYTES, timeout: SMOKE_TIMEOUT_MS, - windowsHide: true + windowsHide: true, + signal }) } catch (error) { // The child classifies PTY/watcher failures; retain its bounded tail to avoid blind CI retries. @@ -155,13 +162,21 @@ async function runBundledSmoke(runtimeDirectory, identity) { return smoke } -export async function verifySshRelayRuntime({ runtimeDirectory, identityPath, archivePath }) { - const identity = JSON.parse(await readFile(identityPath, 'utf8')) +export async function verifyExtractedSshRelayRuntime({ + runtimeDirectory, + identity, + archivePath, + signal +}) { + signal?.throwIfAborted() const started = process.hrtime.bigint() - const archive = archivePath ? await inspectSshRelayRuntimeArchive(archivePath, identity) : null - const tree = await verifyRuntimeTree(runtimeDirectory, identity) + const archive = archivePath + ? await inspectSshRelayRuntimeArchive(archivePath, identity, { signal }) + : null + const tree = await verifyRuntimeTree(runtimeDirectory, identity, { signal }) // Why: native code runs only after the complete extracted tree matches its content identity. - const smoke = await runBundledSmoke(runtimeDirectory, identity) + const smoke = await runBundledSmoke(runtimeDirectory, identity, signal) + signal?.throwIfAborted() return { tuple: identity.tupleId, archive, @@ -171,6 +186,16 @@ export async function verifySshRelayRuntime({ runtimeDirectory, identityPath, ar } } +export async function verifySshRelayRuntime({ + runtimeDirectory, + identityPath, + archivePath, + signal +}) { + const identity = JSON.parse(await readFile(identityPath, { encoding: 'utf8', signal })) + return verifyExtractedSshRelayRuntime({ runtimeDirectory, identity, archivePath, signal }) +} + async function main() { const result = await verifySshRelayRuntime(parseArguments(process.argv.slice(2))) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`) 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 fb9029f1b3f..e39e0021974 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md @@ -15,8 +15,9 @@ in progress, 2026-07-15, Codex implementation owner.** Exact-head run target-native artifact jobs and the 4-test materialization suite on every Node 24 runner under `E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001`. PR Checks and Golden E2E are green; the artifact workflow is red only for the retained Windows ARM build-26200-versus-26100 floor mismatch after -successful runtime smoke. Next, bind exact materialized paths to the existing exclusive extraction -and bundled Node/native PTY/watcher execution boundary. Production keys/environment/seed, +successful runtime smoke. The disconnected execution boundary and a real 24,747,276-byte Darwin +arm64 Actions artifact pass locally under `E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001`; exact-head +all-six proof is pending. Production keys/environment/seed, publication, desktop/default consumers, SSH transfer/install, tuple enablement, and merge to `main` remain disconnected. Current pushed branch head is `0fbbac118`; the implementation owner is authorized to drive commits, pushes, CI, and PR-contained rehearsals end to @@ -278,7 +279,12 @@ end without merging. temporary/final naming, cancellation/failure cleanup, and exact returned paths pass locally under `E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001`. Exact-head all-six Node 24 proof is closed under `E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001`; downloaded archive execution and a - real release write/read-back are still required. + real release write/read-back are still required. The purpose-named missing execution-boundary + RED is recorded under `E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-RED-001`. Exact descriptor/path + binding, exclusive extraction, full-tree verification, bundled Node/native smoke, cleanup, + and real Darwin arm64 Actions-artifact execution pass locally under + `E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001`; all-six exact-head CI and a real authenticated + release write/read-back remain open. - [ ] Test timeouts, retries, approval denial, signing failure, partial output, and draft recovery. ### WP4 — Desktop resolver and verified cache 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 1fa8f2d3a9b..e4d1eccce74 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md @@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    -Current phase: Milestone 4 / Work Package 3 disconnected downloaded-archive execution — **In progress — 2026-07-15, Codex implementation owner**. Exclusive authenticated draft read-back materialization is closed locally and on all six exact-head native Node 24 jobs under E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001, E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001, and E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001. The next slice must prove that only exact materialized archive paths bound to their verified tuple identity can enter the existing exclusive extraction and bundled Node/native PTY/watcher execution boundary. Begin with a purpose-named disconnected RED; do not add a real release write, production workflow caller, publication, desktop consumer, SSH transfer, tuple enablement, or default behavior.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `0fbbac118e08a17f8263ad48f116a8d61362fe92` passes the materialization suite and full runtime construction on all six native jobs in run 29435837082 under E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001. PR Checks 29435836210 and both Golden E2E jobs in 29435835950 are green. Linux supplements and the Windows x64 floor pass. Windows arm64 runtime smoke passes in 6,121.6774ms with 48,623,616-byte RSS; its separate floor job then retains the expected fail-closed hosted-build 26200 versus required 26100 mismatch. The worktree is clean; `git diff --check` passed before commit and the protected resolver/desktop gate files have zero diff. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, upload/publication caller, downloaded-archive execution consumer, desktop consumer, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, desktop consumer, tuple enablement, or production/default change is connected. Legacy remains the production default.
    +Current phase: Milestone 4 / Work Package 3 disconnected downloaded-archive execution — **In progress — 2026-07-15, Codex implementation owner**. Exclusive authenticated draft read-back materialization is closed locally and on all six exact-head native Node 24 jobs under E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001, E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001, and E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001. Exact materialized descriptor/path binding, exclusive extraction, signal propagation, complete tree verification, bundled Node/native PTY/watcher smoke, and cleanup now pass locally under E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001, including one real full-size Darwin arm64 Actions artifact. Exact-head all-six native proof remains the active gate; do not add a real release write, production workflow caller, publication, desktop consumer, SSH transfer, tuple enablement, or default behavior.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — local implementation atop `94d9ee9cb90e4bffd2dafc5b7e8e532d197f70d0` passes 55 SSH-relay files / 297 tests, typecheck, full lint, formatting, diff scope, and a 24,747,276-byte Darwin arm64 artifact execution under E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001. The first full-size attempt correctly failed closed on a lexical `/var` temp alias before extraction; the CLI now canonicalizes to the physical path while the core boundary remains strict. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. Exact-head all-six native CI is next. There is no real release write, publication caller, desktop consumer, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -121,10 +121,12 @@ same change as the work it records. E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001, E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001, and E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001. -- Active package: Work Package 3 disconnected downloaded-archive execution. Begin with a - purpose-named RED binding exact materialized paths to the verified tuple identity, existing - exclusive extraction, and bundled Node/native PTY/watcher smoke. Do not add a real release write, - publication, desktop consumer, SSH transfer/install, or production caller in this slice. +- Active package: Work Package 3 disconnected downloaded-archive execution. The purpose-named RED, + local contracts, and a real full-size Darwin arm64 Actions-artifact execution are green under + E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-RED-001 and + E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001. Exact-head all-six native execution is next. Do not add + a real release write, publication, desktop consumer, SSH transfer/install, or production caller + in this slice. - Completed Work Package 2 gate: target-native Windows source-signature reports from exact-head artifact jobs 87267322867 and 87267322870 were independently downloaded and matched to their identities and signing-stage reports under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001. PR Checks @@ -11633,6 +11635,88 @@ pnpm run typecheck` passes in 2.83s wall with 1,257,357,312-byte maximum RSS and - Follow-up: checkpoint this evidence, then begin the purpose-named disconnected downloaded-archive execution RED. Retain all production/default disconnections. +### E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-RED-001 — Materialized archive execution boundary is absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Worktree head before implementation: local evidence commit + `94d9ee9cb90e4bffd2dafc5b7e8e532d197f70d0` atop pushed code head + `0fbbac118e08a17f8263ad48f116a8d61362fe92`; draft PR #8741. The worktree contains only the + purpose-named RED and POSIX/Windows native-job syntax/test wiring. +- Command: + `pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-readback-archive-execution.test.mjs` +- Result: expected FAIL in 0.749s; one suite / zero collected tests because + `ssh-relay-runtime-readback-archive-execution.mjs` does not exist. +- Contract pinned by the RED: accept only an exact four-field materialized archive descriptor whose + physical absolute path, name, size, and SHA-256 match the verified tuple identity; invoke existing + exclusive extraction before bundled Node/native PTY/watcher verification; reject returned tuple/ + content identity drift; remove the extracted runtime after smoke failure, cancellation, or result + mismatch. +- Does not prove: implementation, local GREEN, real archive extraction or native execution, Node 24/ + native-runner portability, a real authenticated GitHub response, full-size network transfer, real + release write/publication, desktop/SSH behavior, or any enabled tuple. +- Follow-up: implement only the disconnected composition boundary and signal propagation into the + existing bundled runtime verifier. Preserve all production/default disconnections. + +### E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001 — Exact archive composition and native execution pass locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted implementation atop local evidence head + `94d9ee9cb90e4bffd2dafc5b7e8e532d197f70d0`; draft PR #8741. Local runner is macOS 26.2 build + 25C56 arm64, Node v26.0.0, and pnpm 10.24.0. +- First full-size correction signal: `gh run download 29435837082 --repo stablyai/orca --name +ssh-relay-runtime-darwin-arm64` followed by `/usr/bin/time -l node +config/scripts/ssh-relay-runtime-readback-archive-execution.mjs --identity --archive + --output-directory ` correctly failed before extraction in 0.08s with + `requires a physical materialized path`. macOS exposed the Actions artifact through lexical + `/var` while its physical parent is `/private/var`. The CLI adapter now calls `realpath` before + constructing the exact materialized descriptor; the core physical-path check was not weakened. + A cross-platform directory-alias/junction test pins the correction. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +config/scripts/ssh-relay-runtime-readback-archive-execution.test.mjs +config/scripts/ssh-relay-runtime-archive-extraction.test.mjs +config/scripts/ssh-relay-runtime-pty-smoke.test.mjs +config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 4 files / 21 tests in 426ms Vitest and + 1.15s wall; maximum RSS 131,776,512 bytes and zero swaps. +- Adjacent command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1` with the execution, extraction, materialization, verification-only read-back, + upload, recovery, release-stage, workflow, release-assets, and protected required-assets suites — + PASS, 10 files / 80 tests in 1.83s Vitest and 2.65s wall; maximum RSS 131,760,128 bytes and zero + swaps. +- Broad command: `/usr/bin/time -l sh -c "rg --files config/scripts | rg +'/ssh-relay.*[.]test[.]mjs$' | xargs pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1"` — PASS, 55 files / 297 tests in 14.19s Vitest and 15.20s wall; maximum RSS + 191,299,584 bytes and zero swaps. +- Real archive command: download unpublished artifact `ssh-relay-runtime-darwin-arm64` from exact- + head artifact run 29435837082, then execute the purpose-named CLI with its identity, archive, and + an exclusive temporary output. PASS: the 24,747,276-byte archive reconstructs 122,027,869 bytes, + 50 entries, and 35 files at content ID + `sha256:40ff5d2036784b794e7b09f78596409f63f3145280c530bece5280d40897f6cb`; bundled Node + v24.18.0, modules ABI 137, node-pty input/resize/exit 23, and watcher create/update/delete/rename + smoke all pass. Verification/smoke takes 2,975.938625ms; the full command takes 5.16s wall with + 364,691,456-byte maximum RSS, and the reconstructed output is removed afterward. +- Static gates: production/test `node --check`, focused `oxfmt --check`, and `git diff --check` pass. + `/usr/bin/time -l pnpm run typecheck` passes in 3.39s wall with 1,228,537,856-byte maximum RSS. + `/usr/bin/time -l pnpm run lint` passes in 13.20s wall with 2,024,636,416-byte maximum RSS; all 41 + reliability gates and the 355-entry max-lines ratchet pass, with the existing 26 unrelated + warnings. The protected Node/npm resolver and required-asset files retain zero diff. +- Oracle proved: only an exact four-field descriptor whose physical path, name, size, and SHA-256 + match the tuple identity can enter the existing exclusive extractor. The actual archive is + bounded and hashed before and after extraction; the complete extracted tree is hashed before the + bundled Node launches native PTY/watcher smoke. Abort signals reach inspection, extraction, each + staged-file read, and the smoke child. Extraction/result/smoke/cancellation failures remove the + reconstructed tree. All three native job families invoke this boundary against the real first- + build archive and remove the successful output; Linux does so inside the digest-pinned, + network-disabled Rocky 8 builder. +- Does not prove: exact-head Node 24 behavior on all six runners, a real authenticated GitHub release + response, upload/read-back composition, full-size release-asset network transfer, native signing/ + trust, oldest macOS/Linux-kernel/Windows-arm64 floors, desktop resolver/cache, SSH transfer/ + install, performance against legacy, or any enabled tuple. +- Follow-up: commit and push the exact package, then require the purpose-named suite and real archive + execution to pass on all six exact-head native jobs. Keep release writes and every product/default + consumer disconnected. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 3779838f66a57e3688367256510e096d17d78011 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 13:52:11 -0400 Subject: [PATCH 144/267] docs: checkpoint relay archive execution proof Co-authored-by: Orca --- ...h-relay-github-release-implementation-checklist-summary.md | 4 ++-- ...07-14-ssh-relay-github-release-implementation-checklist.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 e39e0021974..de8ff9a199c 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 @@ -20,8 +20,8 @@ arm64 Actions artifact pass locally under `E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL all-six proof is pending. Production keys/environment/seed, publication, desktop/default consumers, SSH transfer/install, tuple enablement, and merge to `main` remain disconnected. Current pushed branch head is `0fbbac118`; the -implementation owner is authorized to drive commits, pushes, CI, and PR-contained rehearsals end to -end without merging. +local implementation commit is `b8c42f92a`. The implementation owner is authorized to drive +commits, pushes, CI, and PR-contained rehearsals end to end without merging. ## Safety status 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 e4d1eccce74..6e19f0948ea 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 4 / Work Package 3 disconnected downloaded-archive execution — **In progress — 2026-07-15, Codex implementation owner**. Exclusive authenticated draft read-back materialization is closed locally and on all six exact-head native Node 24 jobs under E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001, E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001, and E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001. Exact materialized descriptor/path binding, exclusive extraction, signal propagation, complete tree verification, bundled Node/native PTY/watcher smoke, and cleanup now pass locally under E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001, including one real full-size Darwin arm64 Actions artifact. Exact-head all-six native proof remains the active gate; do not add a real release write, production workflow caller, publication, desktop consumer, SSH transfer, tuple enablement, or default behavior.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — local implementation atop `94d9ee9cb90e4bffd2dafc5b7e8e532d197f70d0` passes 55 SSH-relay files / 297 tests, typecheck, full lint, formatting, diff scope, and a 24,747,276-byte Darwin arm64 artifact execution under E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001. The first full-size attempt correctly failed closed on a lexical `/var` temp alias before extraction; the CLI now canonicalizes to the physical path while the core boundary remains strict. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. Exact-head all-six native CI is next. There is no real release write, publication caller, desktop consumer, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact local implementation commit `b8c42f92a` passes 55 SSH-relay files / 297 tests, typecheck, full lint, formatting, diff scope, and a 24,747,276-byte Darwin arm64 artifact execution under E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001. The first full-size attempt correctly failed closed on a lexical `/var` temp alias before extraction; the CLI now canonicalizes to the physical path while the core boundary remains strict. The local branch also contains evidence checkpoint `94d9ee9cb` and is two commits ahead of pushed head `0fbbac118`. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. Exact-head all-six native CI is next. There is no real release write, publication caller, desktop consumer, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) From f801d3785d59ca6f8a13f275aa73c5cab45dec69 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 14:14:06 -0400 Subject: [PATCH 145/267] docs: record relay archive execution CI proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 24 ++--- ...github-release-implementation-checklist.md | 89 ++++++++++++++++--- 2 files changed, 87 insertions(+), 26 deletions(-) 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 de8ff9a199c..4a9fd228923 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 @@ -9,18 +9,17 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 4 / Work Package 3 disconnected downloaded-archive execution — +Active checkpoint: **Milestone 4 / Work Package 3 disconnected draft-release failure composition — in progress, 2026-07-15, Codex implementation owner.** Exact-head run -[29435837082](https://github.com/stablyai/orca/actions/runs/29435837082) at `0fbbac118` passes all six -target-native artifact jobs and the 4-test materialization suite on every Node 24 runner under -`E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001`. PR Checks and Golden E2E are green; the artifact -workflow is red only for the retained Windows ARM build-26200-versus-26100 floor mismatch after -successful runtime smoke. The disconnected execution boundary and a real 24,747,276-byte Darwin -arm64 Actions artifact pass locally under `E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001`; exact-head -all-six proof is pending. Production keys/environment/seed, +[29438285824](https://github.com/stablyai/orca/actions/runs/29438285824) at `3779838f6` passes the +10-test execution contract and real first-build archive reconstruction/native smoke on all six +target-native Node 24 jobs under `E-M4-READBACK-ARCHIVE-EXECUTION-CI-001`. PR Checks and Golden E2E +are green; the artifact workflow is red only for the retained Windows ARM +build-26200-versus-26100 floor mismatch after successful runtime smoke. Next, compose the existing +disconnected upload, recovery, materialization, and execution capabilities under purpose-named +failure-state tests without a release write or workflow caller. Production keys/environment/seed, publication, desktop/default consumers, SSH transfer/install, tuple enablement, and merge to `main` -remain disconnected. Current pushed branch head is `0fbbac118`; the -local implementation commit is `b8c42f92a`. The implementation owner is authorized to drive +remain disconnected. Current pushed branch head is `3779838f6`; the implementation owner is authorized to drive commits, pushes, CI, and PR-contained rehearsals end to end without merging. ## Safety status @@ -283,8 +282,9 @@ commits, pushes, CI, and PR-contained rehearsals end to end without merging. RED is recorded under `E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-RED-001`. Exact descriptor/path binding, exclusive extraction, full-tree verification, bundled Node/native smoke, cleanup, and real Darwin arm64 Actions-artifact execution pass locally under - `E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001`; all-six exact-head CI and a real authenticated - release write/read-back remain open. + `E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001`. All-six exact-head Node 24 archive execution is + closed under `E-M4-READBACK-ARCHIVE-EXECUTION-CI-001`; a real authenticated release write/ + read-back remains open. - [ ] Test timeouts, retries, approval denial, signing failure, partial output, and draft recovery. ### WP4 — Desktop resolver and verified cache 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 6e19f0948ea..f75963cc359 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md @@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    -Current phase: Milestone 4 / Work Package 3 disconnected downloaded-archive execution — **In progress — 2026-07-15, Codex implementation owner**. Exclusive authenticated draft read-back materialization is closed locally and on all six exact-head native Node 24 jobs under E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001, E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001, and E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001. Exact materialized descriptor/path binding, exclusive extraction, signal propagation, complete tree verification, bundled Node/native PTY/watcher smoke, and cleanup now pass locally under E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001, including one real full-size Darwin arm64 Actions artifact. Exact-head all-six native proof remains the active gate; do not add a real release write, production workflow caller, publication, desktop consumer, SSH transfer, tuple enablement, or default behavior.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact local implementation commit `b8c42f92a` passes 55 SSH-relay files / 297 tests, typecheck, full lint, formatting, diff scope, and a 24,747,276-byte Darwin arm64 artifact execution under E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001. The first full-size attempt correctly failed closed on a lexical `/var` temp alias before extraction; the CLI now canonicalizes to the physical path while the core boundary remains strict. The local branch also contains evidence checkpoint `94d9ee9cb` and is two commits ahead of pushed head `0fbbac118`. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. Exact-head all-six native CI is next. There is no real release write, publication caller, desktop consumer, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    +Current phase: Milestone 4 / Work Package 3 disconnected draft-release failure composition — **In progress — 2026-07-15, Codex implementation owner**. Exclusive authenticated draft read-back materialization and exact materialized archive execution are closed locally and on all six exact-head native Node 24 jobs under E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001, E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001, E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001, E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-RED-001, E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001, and E-M4-READBACK-ARCHIVE-EXECUTION-CI-001. Next, compose the existing disconnected upload, recovery, materialization, and execution capabilities under purpose-named timeout/retry/partial-output/failure tests. Do not add a real release write, production workflow caller, publication, desktop consumer, SSH transfer, tuple enablement, or default behavior.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `3779838f66a57e3688367256510e096d17d78011` passes real first-build archive reconstruction, full-tree verification, bundled Node/native PTY/watcher smoke, and cleanup on all six target-native jobs in run 29438285824 under E-M4-READBACK-ARCHIVE-EXECUTION-CI-001. PR Checks 29438280473 and both Golden E2E jobs in 29438284781 are green. Both Linux supplements and Windows x64 floor pass. Windows arm64 floor smoke passes in 7,454.2658ms with 49,430,528-byte RSS; the floor gate then correctly rejects hosted build 26200 versus required 26100. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -121,12 +121,16 @@ same change as the work it records. E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001, E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001, and E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001. -- Active package: Work Package 3 disconnected downloaded-archive execution. The purpose-named RED, - local contracts, and a real full-size Darwin arm64 Actions-artifact execution are green under - E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-RED-001 and - E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001. Exact-head all-six native execution is next. Do not add - a real release write, publication, desktop consumer, SSH transfer/install, or production caller - in this slice. +- Completed package: Work Package 3 disconnected downloaded-archive execution. The purpose-named + RED, local contracts, real full-size Darwin arm64 Actions-artifact execution, and exact-head + all-six native execution are green under E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-RED-001, + E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001, and + E-M4-READBACK-ARCHIVE-EXECUTION-CI-001. +- Active package: Work Package 3 disconnected draft-release failure composition. Begin with a + purpose-named RED that composes only the existing upload/recovery/materialization/archive- + execution capabilities under explicit timeout, retry exhaustion, partial-output, cancellation, + and fail-closed sequencing contracts. Do not add a real release write, publication, desktop + consumer, SSH transfer/install, or production caller in this slice. - Completed Work Package 2 gate: target-native Windows source-signature reports from exact-head artifact jobs 87267322867 and 87267322870 were independently downloaded and matched to their identities and signing-stage reports under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001. PR Checks @@ -11717,6 +11721,63 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 4 files / 21 tests execution to pass on all six exact-head native jobs. Keep release writes and every product/default consumer disconnected. +### E-M4-READBACK-ARCHIVE-EXECUTION-CI-001 — Real archives execute on all six native Node 24 jobs + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `3779838f66a57e3688367256510e096d17d78011`; implementation commit + `b8c42f92a`; draft PR #8741. +- Workflow: [SSH Relay Runtime Artifacts run 29438285824](https://github.com/stablyai/orca/actions/runs/29438285824), + 17:52:40Z–18:07:34Z. The aggregate conclusion is the expected failure only because the separate + Windows arm64 floor job rejects hosted build 26200 after successful exact-byte runtime execution. +- Native job evidence, all at source `3779838f66a57e3688367256510e096d17d78011` under Node 24.18.0: + - Linux x64 job 87430377762, `ubuntu-24.04`, image `ubuntu24` `20260705.232.1`, PASS in 4m32s. + The 10-test execution suite takes 37ms. The real 29,271,396-byte archive reconstructs 49 entries/ + 34 files and 124,846,430 bytes; composition takes 2,421.587534ms and smoke takes 189.607788ms + with 55,930,880-byte RSS. + - Linux arm64 job 87430377675, `ubuntu-24.04-arm`, image `ubuntu24-arm64` `20260714.61.1`, PASS in + 4m49s. The suite takes 32ms. The 28,194,028-byte archive reconstructs 49 entries/34 files and + 122,865,324 bytes; composition takes 1,657.784674ms and smoke takes 161.281354ms with + 51,896,320-byte RSS. + - Darwin x64 job 87430377758, `macos-15-intel`, image `macos15` `20260629.0276.1`, PASS in 9m14s. + The suite takes 78ms. The 26,381,668-byte archive reconstructs 50 entries/35 files and + 124,316,655 bytes; composition takes 3,433.335342ms and smoke takes 475.018668ms with + 40,570,880-byte RSS. + - Darwin arm64 job 87430377724, `macos-15`, image `macos15` `20260706.0213.1`, PASS in 2m56s. The + suite takes 21ms. The 24,715,844-byte archive reconstructs 50 entries/35 files and 122,027,869 + bytes; composition takes 2,008.11875ms and smoke takes 557.32775ms with 51,134,464-byte RSS. + - Windows x64 job 87430377708, `windows-2022`, image `win22` `20260714.244.1`, PASS in 5m29s. The + suite takes 81ms. The 37,168,778-byte ZIP reconstructs 60 entries/42 files and 96,527,161 bytes; + composition takes 6,312.6928ms and smoke takes 5,328.335ms with 53,751,808-byte RSS. + - Windows arm64 job 87430377658, `windows-11-arm`, image `win11-arm64` `20260706.102.1`, PASS in + 9m34s. The suite takes 63ms. The 33,204,738-byte ZIP reconstructs 60 entries/42 files and + 85,213,511 bytes; composition takes 8,369.1415ms and smoke takes 5,492.5873ms with + 52,654,080-byte RSS. +- Broad contract counts: each POSIX job passes 54 files / 293 tests; durations are Linux x64 8.16s, + Linux arm64 6.69s, Darwin x64 37.47s, and Darwin arm64 7.12s. Each Windows job passes 55 files / + 287 tests plus 10 platform skips of 297; x64 takes 13.48s and arm64 15.48s. +- Downstream evidence: Linux x64 supplement job 87432492978 and Linux arm64 supplement job + 87432492999 pass. Windows x64 floor job 87432565846 passes. Windows arm64 floor job 87432565824 + uses image `win11-arm64` `20260714.109.1`; exact archive/tree verification, bundled Node v24.18.0, + PTY/watcher smoke, and two-second resource settlement pass in 7,454.2658ms with 49,430,528-byte + RSS. It then fails exactly because observed OS/kernel build 26200 is not required build 26100. +- Adjacent exact-head workflows: [PR Checks run 29438280473](https://github.com/stablyai/orca/actions/runs/29438280473), + job 87430357941, passes in 11m35s. [Golden E2E run 29438284781](https://github.com/stablyai/orca/actions/runs/29438284781) + passes macOS job 87430373077 in 4m56s and Linux job 87430373141 in 4m41s. +- Oracle proved: all six target-native runners parse and run the 10-test exact descriptor/path, + alias/junction, sequencing, cancellation, identity-drift, and cleanup suite, then pass their real + first-build archive through exclusive extraction, complete archive/tree hashing, bundled Node, + native PTY/watcher smoke, and success cleanup. Linux execution occurs inside the same digest- + pinned, network-disabled Rocky 8 builder. Both tar.xz and ZIP families pass on x64 and arm64. +- Does not prove: a real authenticated GitHub release response, real upload/read-back composition, + native signing/trust, oldest macOS/Linux-kernel/Windows-arm64 floors, desktop resolver/cache, SSH + transfer/install, performance against legacy, or any enabled tuple. +- Checklist items satisfied: exact-head all-six CI gate for disconnected downloaded-archive + execution. The broader real draft release write/read-back/execution and release composition box + remains open. +- Follow-up: checkpoint this evidence, then begin a purpose-named disconnected draft-release + failure-composition RED. Retain all production/default disconnections. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -11775,12 +11836,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Begin the purpose-named disconnected downloaded-archive execution RED. It must accept only exact -materialized asset paths bound to the verified tuple identity, route the archive through the existing -exclusive extraction/full-tree verification boundary, and execute bundled Node plus native PTY/ -watcher smoke before reporting success. It must fail closed and clean exclusive output on path, -identity, extraction, execution, cancellation, or mutation failure. Do not add a real release write, -publication, desktop consumer, SSH transfer/install, tuple enablement, or production caller in this +Begin a purpose-named disconnected draft-release failure-composition RED. It must orchestrate only +the existing bounded upload/recovery, authenticated materialization, and exact archive-execution +capabilities through injected credential-free boundaries. Pin phase ordering and cleanup for timeout, +retry exhaustion, partial upload/output, cancellation, release-identity drift, materialization +failure, and archive-execution failure. Do not add a real release write, publication, desktop +consumer, SSH transfer/install, tuple enablement, production caller, or workflow connection in this package. Keep the existing desktop `verify-release-required-assets.mjs` behavior and detached- signature byte encoding unchanged. The protected environment, accepted production keys, and secret remain unprovisioned, so live signing evidence stays BLOCKED. From b6bb3dd9a55859dcb03d2f3da88f5798759055c6 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 14:29:19 -0400 Subject: [PATCH 146/267] build(ssh): compose relay draft release verification Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 8 + ...lay-runtime-draft-release-verification.mjs | 368 ++++++++++++++++++ ...untime-draft-release-verification.test.mjs | 349 +++++++++++++++++ ...elease-implementation-checklist-summary.md | 9 +- ...github-release-implementation-checklist.md | 89 ++++- 5 files changed, 816 insertions(+), 7 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-draft-release-verification.mjs create mode 100644 config/scripts/ssh-relay-runtime-draft-release-verification.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index e2e68e97cd6..018014e4ee3 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -219,6 +219,8 @@ jobs: node --check config/scripts/ssh-relay-runtime-draft-readback.mjs node --check config/scripts/ssh-relay-runtime-draft-readback.test.mjs node --check config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs + node --check config/scripts/ssh-relay-runtime-draft-release-verification.mjs + node --check config/scripts/ssh-relay-runtime-draft-release-verification.test.mjs node --check config/scripts/ssh-relay-runtime-readback-archive-execution.mjs node --check config/scripts/ssh-relay-runtime-readback-archive-execution.test.mjs node --check config/scripts/ssh-relay-runtime-draft-upload.mjs @@ -281,6 +283,7 @@ jobs: config/scripts/ssh-relay-runtime-aggregate-input.test.mjs \ config/scripts/ssh-relay-runtime-draft-readback.test.mjs \ config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs \ + config/scripts/ssh-relay-runtime-draft-release-verification.test.mjs \ config/scripts/ssh-relay-runtime-readback-archive-execution.test.mjs \ config/scripts/ssh-relay-runtime-draft-upload.test.mjs \ config/scripts/ssh-relay-runtime-release-assets.test.mjs \ @@ -676,6 +679,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime draft read-back test syntax check failed' } node --check config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime draft read-back materialization test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-draft-release-verification.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime draft release verification syntax check failed' } + node --check config/scripts/ssh-relay-runtime-draft-release-verification.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime draft release verification test syntax check failed' } node --check config/scripts/ssh-relay-runtime-readback-archive-execution.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime read-back archive execution syntax check failed' } node --check config/scripts/ssh-relay-runtime-readback-archive-execution.test.mjs @@ -773,6 +780,7 @@ jobs: config/scripts/ssh-relay-runtime-aggregate-input.test.mjs ` config/scripts/ssh-relay-runtime-draft-readback.test.mjs ` config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs ` + config/scripts/ssh-relay-runtime-draft-release-verification.test.mjs ` config/scripts/ssh-relay-runtime-readback-archive-execution.test.mjs ` config/scripts/ssh-relay-runtime-draft-upload.test.mjs ` config/scripts/ssh-relay-runtime-release-assets.test.mjs ` diff --git a/config/scripts/ssh-relay-runtime-draft-release-verification.mjs b/config/scripts/ssh-relay-runtime-draft-release-verification.mjs new file mode 100644 index 00000000000..aa60f962ee2 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-draft-release-verification.mjs @@ -0,0 +1,368 @@ +import { lstat, mkdir, realpath, rm } from 'node:fs/promises' +import { basename, dirname, join, relative, resolve, sep } from 'node:path' + +import { materializeSshRelayRuntimeDraftReadback } from './ssh-relay-runtime-draft-readback.mjs' +import { uploadSshRelayRuntimeDraftAssets } from './ssh-relay-runtime-draft-upload.mjs' +import { executeSshRelayRuntimeReadbackArchive } from './ssh-relay-runtime-readback-archive-execution.mjs' + +const COMPOSITION_TIMEOUT_MS = 45 * 60_000 +const COMMIT_PATTERN = /^[0-9a-f]{40}$/u +const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u +const REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u +const TAG_PATTERN = /^v\d+\.\d+\.\d+(?:-rc\.\d+(?:\.[0-9A-Za-z]+)?)?$/u +const TUPLE_PATTERN = /^(?:linux-(?:x64|arm64)-glibc|darwin-(?:x64|arm64)|win32-(?:x64|arm64))$/u +const RUNTIME_ARCHIVE_PATTERN = /^orca-ssh-relay-runtime-v1-.+\.(?:tar\.xz|zip)$/u +const MANAGED_ASSET_PATTERN = /^orca-ssh-relay-runtime-[A-Za-z0-9._-]+$/u +const MAX_ASSET_BYTES = 100 * 1024 * 1024 +const MAX_ASSETS = 26 +const MAX_TOTAL_BYTES = 1024 * 1024 * 1024 + +function assertObject(value, label) { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`SSH relay runtime draft release verification ${label} must be an object`) + } +} + +function assertExactFields(value, fields, label) { + assertObject(value, label) + if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify([...fields].sort())) { + throw new Error(`SSH relay runtime draft release verification ${label} fields drifted`) + } +} + +function sameAsset(left, right) { + return left.name === right.name && left.sha256 === right.sha256 && left.size === right.size +} + +function assetIdentity(asset, label, fields) { + assertObject(asset, label) + if (fields) { + assertExactFields(asset, fields, label) + } + if ( + typeof asset.name !== 'string' || + asset.name.length === 0 || + typeof asset.sha256 !== 'string' || + !DIGEST_PATTERN.test(asset.sha256) || + !Number.isSafeInteger(asset.size) || + asset.size <= 0 || + asset.size > MAX_ASSET_BYTES + ) { + throw new Error(`SSH relay runtime draft release verification ${label} is invalid`) + } + return { name: asset.name, sha256: asset.sha256, size: asset.size } +} + +function validateReleaseInput({ repo, releaseId, tag, sourceCommit, token }) { + if ( + typeof repo !== 'string' || + !REPOSITORY_PATTERN.test(repo) || + !Number.isSafeInteger(releaseId) || + releaseId <= 0 || + typeof tag !== 'string' || + !TAG_PATTERN.test(tag) || + typeof sourceCommit !== 'string' || + !COMMIT_PATTERN.test(sourceCommit) || + typeof token !== 'string' || + token.length === 0 + ) { + throw new Error('SSH relay runtime draft release verification release identity is invalid') + } +} + +function expectedAssets(assets) { + if (!Array.isArray(assets) || assets.length === 0 || assets.length > MAX_ASSETS) { + throw new Error('SSH relay runtime draft release verification assets are required') + } + const byName = new Map() + let totalBytes = 0 + for (const [index, asset] of assets.entries()) { + const normalized = assetIdentity(asset, `asset ${index}`) + if ( + !MANAGED_ASSET_PATTERN.test(normalized.name) || + typeof asset.path !== 'string' || + asset.path.length === 0 || + byName.has(normalized.name) + ) { + throw new Error('SSH relay runtime draft release verification asset path or name is invalid') + } + totalBytes += normalized.size + byName.set(normalized.name, normalized) + } + if (!Number.isSafeInteger(totalBytes) || totalBytes > MAX_TOTAL_BYTES) { + throw new Error('SSH relay runtime draft release verification assets exceed the total bound') + } + return byName +} + +function expectedArchiveIdentities(archiveIdentities, assetsByName) { + if (!Array.isArray(archiveIdentities) || archiveIdentities.length === 0) { + throw new Error('SSH relay runtime draft release verification archive identities are required') + } + const tuples = new Set() + const archives = new Set() + const normalized = archiveIdentities.map((identity, index) => { + assertObject(identity, `archive identity ${index}`) + assertObject(identity.archive, `archive identity ${index} archive`) + const archive = assetIdentity(identity.archive, `archive identity ${index} archive`) + const expected = assetsByName.get(archive.name) + if ( + typeof identity.tupleId !== 'string' || + !TUPLE_PATTERN.test(identity.tupleId) || + typeof identity.contentId !== 'string' || + !DIGEST_PATTERN.test(identity.contentId) || + !expected || + !sameAsset(archive, expected) || + tuples.has(identity.tupleId) || + archives.has(archive.name) + ) { + throw new Error('SSH relay runtime draft release verification archive identity drifted') + } + tuples.add(identity.tupleId) + archives.add(archive.name) + return { identity, archive } + }) + const expectedArchiveNames = [...assetsByName.keys()].filter((name) => + RUNTIME_ARCHIVE_PATTERN.test(name) + ) + if ( + expectedArchiveNames.length !== archives.size || + expectedArchiveNames.some((name) => !archives.has(name)) + ) { + throw new Error('SSH relay runtime draft release verification archive coverage drifted') + } + return normalized +} + +async function exclusivePhysicalDirectory(path, label) { + if (typeof path !== 'string' || path.length === 0) { + throw new Error(`SSH relay runtime draft release verification ${label} is required`) + } + const absolute = resolve(path) + const physicalParent = resolve(await realpath(dirname(absolute))) + const physical = resolve(physicalParent, basename(absolute)) + try { + await lstat(physical) + } catch (error) { + if (error.code === 'ENOENT') { + return physical + } + throw error + } + throw new Error( + `SSH relay runtime draft release verification ${label} must be an exclusive absent directory` + ) +} + +function containsPath(parent, candidate) { + const path = relative(parent, candidate) + return path === '' || (path !== '..' && !path.startsWith(`..${sep}`)) +} + +function assertDisjointOutputs(readbackDirectory, executionDirectory) { + if ( + containsPath(readbackDirectory, executionDirectory) || + containsPath(executionDirectory, readbackDirectory) + ) { + throw new Error( + 'SSH relay runtime draft release verification output directories must be disjoint' + ) + } +} + +function validateUploadResult(result, expected, { releaseId, tag, sourceCommit }) { + assertExactFields( + result, + ['releaseId', 'reusedAssets', 'sourceCommit', 'tag', 'uploadedAssets'], + 'upload result' + ) + if ( + result.releaseId !== releaseId || + result.tag !== tag || + result.sourceCommit !== sourceCommit || + !Array.isArray(result.reusedAssets) || + !Array.isArray(result.uploadedAssets) + ) { + throw new Error('SSH relay runtime draft release verification upload identity drifted') + } + const returned = [...result.reusedAssets, ...result.uploadedAssets] + const names = new Set() + for (const [index, asset] of returned.entries()) { + const normalized = assetIdentity(asset, `upload result asset ${index}`, [ + 'name', + 'sha256', + 'size' + ]) + const expectedAsset = expected.get(normalized.name) + if (!expectedAsset || !sameAsset(normalized, expectedAsset) || names.has(normalized.name)) { + throw new Error('SSH relay runtime draft release verification upload assets drifted') + } + names.add(normalized.name) + } + if (names.size !== expected.size) { + throw new Error('SSH relay runtime draft release verification upload asset coverage drifted') + } +} + +function validateMaterializationResult(result, expected, { releaseId, tag, readbackDirectory }) { + assertExactFields(result, ['materializedAssets', 'releaseId', 'tag'], 'read-back result') + if ( + result.releaseId !== releaseId || + result.tag !== tag || + !Array.isArray(result.materializedAssets) + ) { + throw new Error('SSH relay runtime draft release verification read-back identity drifted') + } + const returned = new Map() + for (const [index, asset] of result.materializedAssets.entries()) { + const normalized = assetIdentity(asset, `materialized asset ${index}`, [ + 'name', + 'path', + 'sha256', + 'size' + ]) + const expectedAsset = expected.get(normalized.name) + if ( + !expectedAsset || + !sameAsset(normalized, expectedAsset) || + returned.has(normalized.name) || + asset.path !== join(readbackDirectory, normalized.name) + ) { + throw new Error('SSH relay runtime draft release verification materialized assets drifted') + } + returned.set(normalized.name, { ...normalized, path: asset.path }) + } + if (returned.size !== expected.size) { + throw new Error('SSH relay runtime draft release verification materialized coverage drifted') + } + return returned +} + +function validateExecutionResult(result, identity, outputDirectory) { + assertObject(result, 'archive execution result') + if ( + result.tupleId !== identity.tupleId || + result.contentId !== identity.contentId || + result.runtimeRoot !== outputDirectory || + result.smoke === null || + typeof result.smoke !== 'object' + ) { + throw new Error('SSH relay runtime draft release verification execution identity drifted') + } + return result +} + +async function removeFailedOutputs(error, directories) { + const cleanup = await Promise.allSettled( + directories.map((directory) => rm(directory, { recursive: true, force: true })) + ) + const cleanupFailures = cleanup + .filter((result) => result.status === 'rejected') + .map((result) => result.reason) + if (cleanupFailures.length > 0) { + throw new AggregateError( + [error, ...cleanupFailures], + 'SSH relay runtime draft release verification failed and cleanup was incomplete' + ) + } + throw error +} + +export async function verifySshRelayRuntimeDraftReleaseTransaction({ + repo, + releaseId, + tag, + sourceCommit, + token, + assets, + archiveIdentities, + readbackDirectory, + executionDirectory, + signal, + uploadImpl = uploadSshRelayRuntimeDraftAssets, + materializeImpl = materializeSshRelayRuntimeDraftReadback, + executeImpl = executeSshRelayRuntimeReadbackArchive +}) { + const effectiveSignal = signal + ? AbortSignal.any([signal, AbortSignal.timeout(COMPOSITION_TIMEOUT_MS)]) + : AbortSignal.timeout(COMPOSITION_TIMEOUT_MS) + effectiveSignal.throwIfAborted() + validateReleaseInput({ repo, releaseId, tag, sourceCommit, token }) + const assetsByName = expectedAssets(assets) + const archives = expectedArchiveIdentities(archiveIdentities, assetsByName) + const [physicalReadback, physicalExecution] = await Promise.all([ + exclusivePhysicalDirectory(readbackDirectory, 'read-back directory'), + exclusivePhysicalDirectory(executionDirectory, 'execution directory') + ]) + assertDisjointOutputs(physicalReadback, physicalExecution) + + let readbackOwned = false + let executionOwned = false + try { + const upload = await uploadImpl({ + repo, + releaseId, + tag, + sourceCommit, + token, + assets, + signal: effectiveSignal + }) + effectiveSignal.throwIfAborted() + validateUploadResult(upload, assetsByName, { releaseId, tag, sourceCommit }) + + const materialization = await materializeImpl({ + repo, + releaseId, + tag, + token, + expectedAssets: [...assetsByName.values()], + outputDirectory: physicalReadback, + signal: effectiveSignal + }) + readbackOwned = true + effectiveSignal.throwIfAborted() + const materialized = validateMaterializationResult(materialization, assetsByName, { + releaseId, + tag, + readbackDirectory: physicalReadback + }) + + await mkdir(physicalExecution, { mode: 0o700 }) + executionOwned = true + const verifiedRuntimes = [] + for (const { identity, archive } of archives) { + effectiveSignal.throwIfAborted() + const outputDirectory = join(physicalExecution, identity.tupleId) + const result = await executeImpl({ + identity, + materializedArchive: materialized.get(archive.name), + outputDirectory, + signal: effectiveSignal + }) + effectiveSignal.throwIfAborted() + verifiedRuntimes.push(validateExecutionResult(result, identity, outputDirectory)) + } + return { + releaseId, + tag, + sourceCommit, + readbackDirectory: physicalReadback, + executionDirectory: physicalExecution, + verifiedRuntimes + } + } catch (error) { + // Why: no later release phase may observe bytes from a partially verified transaction. + return removeFailedOutputs(error, [ + ...(readbackOwned ? [physicalReadback] : []), + ...(executionOwned ? [physicalExecution] : []) + ]) + } +} + +export const SSH_RELAY_RUNTIME_DRAFT_RELEASE_VERIFICATION_LIMITS = Object.freeze({ + maximumAssetBytes: MAX_ASSET_BYTES, + maximumAssets: MAX_ASSETS, + maximumTotalBytes: MAX_TOTAL_BYTES, + timeoutMs: COMPOSITION_TIMEOUT_MS +}) diff --git a/config/scripts/ssh-relay-runtime-draft-release-verification.test.mjs b/config/scripts/ssh-relay-runtime-draft-release-verification.test.mjs new file mode 100644 index 00000000000..e6b2e71cf82 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-draft-release-verification.test.mjs @@ -0,0 +1,349 @@ +import { createHash } from 'node:crypto' +import { mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { verifySshRelayRuntimeDraftReleaseTransaction } from './ssh-relay-runtime-draft-release-verification.mjs' + +const REPO = 'stablyai/orca' +const RELEASE_ID = 42 +const TAG = 'v1.5.0-rc.1' +const SOURCE_COMMIT = 'a'.repeat(40) +const TOKEN = 'test-token' +const ARCHIVE_NAME = 'orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.xz' +const ARCHIVE_BYTES = Buffer.from('verified runtime archive') +const SECOND_ARCHIVE_NAME = 'orca-ssh-relay-runtime-v1-win32-x64-c.zip' +const SECOND_ARCHIVE_BYTES = Buffer.from('verified Windows runtime archive') +const MANIFEST_NAME = 'orca-ssh-relay-runtime-manifest.json' +const MANIFEST_BYTES = Buffer.from('signed immutable manifest') + +let root +let readbackDirectory +let executionDirectory + +function digest(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +function identity(overrides = {}) { + return { + tupleId: 'linux-x64-glibc', + contentId: `sha256:${'b'.repeat(64)}`, + archive: { + name: ARCHIVE_NAME, + sha256: digest(ARCHIVE_BYTES), + size: ARCHIVE_BYTES.length + }, + ...overrides + } +} + +function assets() { + return [ + { + name: ARCHIVE_NAME, + path: join(root, ARCHIVE_NAME), + sha256: digest(ARCHIVE_BYTES), + size: ARCHIVE_BYTES.length + }, + { + name: MANIFEST_NAME, + path: join(root, MANIFEST_NAME), + sha256: digest(MANIFEST_BYTES), + size: MANIFEST_BYTES.length + } + ] +} + +function assetIdentities() { + return assets().map(({ path: _path, ...asset }) => asset) +} + +function input(overrides = {}) { + return { + repo: REPO, + releaseId: RELEASE_ID, + tag: TAG, + sourceCommit: SOURCE_COMMIT, + token: TOKEN, + assets: assets(), + archiveIdentities: [identity()], + readbackDirectory, + executionDirectory, + ...overrides + } +} + +function uploadResult(overrides = {}) { + return { + releaseId: RELEASE_ID, + tag: TAG, + sourceCommit: SOURCE_COMMIT, + reusedAssets: [], + uploadedAssets: assetIdentities(), + ...overrides + } +} + +async function materializeResult(overrides = {}) { + await mkdir(readbackDirectory, { recursive: true }) + return { + releaseId: RELEASE_ID, + tag: TAG, + materializedAssets: assetIdentities().map((asset) => ({ + ...asset, + path: join(readbackDirectory, asset.name) + })), + ...overrides + } +} + +beforeEach(async () => { + root = await realpath(await mkdtemp(join(tmpdir(), 'orca-relay-draft-verification-'))) + readbackDirectory = join(root, 'readback') + executionDirectory = join(root, 'execution') + await Promise.all([ + writeFile(join(root, ARCHIVE_NAME), ARCHIVE_BYTES), + writeFile(join(root, MANIFEST_NAME), MANIFEST_BYTES) + ]) +}) + +afterEach(async () => { + await rm(root, { recursive: true, force: true }) + vi.restoreAllMocks() +}) + +describe('SSH relay runtime draft release verification transaction', () => { + it('runs only the contract suite in both native workflow families', async () => { + const workflow = await readFile( + join(import.meta.dirname, '../../.github/workflows/ssh-relay-runtime-artifacts.yml'), + 'utf8' + ) + + expect(workflow.match(/ssh-relay-runtime-draft-release-verification\.test\.mjs/g)).toHaveLength( + 4 + ) + expect(workflow.match(/ssh-relay-runtime-draft-release-verification\.mjs/g)).toHaveLength(2) + expect(workflow).not.toMatch( + /node config\/scripts\/ssh-relay-runtime-draft-release-verification\.mjs/ + ) + }) + + it('orders upload, authenticated materialization, and exact archive execution', async () => { + const order = [] + const uploadImpl = vi.fn(async () => { + order.push('upload') + return uploadResult() + }) + const materializeImpl = vi.fn(async () => { + order.push('materialize') + return materializeResult() + }) + const executeImpl = vi.fn(async ({ materializedArchive, outputDirectory }) => { + order.push('execute') + expect(materializedArchive).toEqual({ + ...assetIdentities()[0], + path: join(readbackDirectory, ARCHIVE_NAME) + }) + expect(outputDirectory).toBe(join(executionDirectory, identity().tupleId)) + await mkdir(outputDirectory) + return { + tupleId: identity().tupleId, + contentId: identity().contentId, + runtimeRoot: outputDirectory, + smoke: { nodeVersion: 'v24.18.0' } + } + }) + + await expect( + verifySshRelayRuntimeDraftReleaseTransaction( + input({ uploadImpl, materializeImpl, executeImpl }) + ) + ).resolves.toMatchObject({ + releaseId: RELEASE_ID, + tag: TAG, + sourceCommit: SOURCE_COMMIT, + verifiedRuntimes: [ + { + tupleId: identity().tupleId, + contentId: identity().contentId, + runtimeRoot: join(executionDirectory, identity().tupleId) + } + ] + }) + expect(order).toEqual(['upload', 'materialize', 'execute']) + }) + + it('rejects archive/asset drift and unsafe output paths before upload', async () => { + const uploadImpl = vi.fn() + await expect( + verifySshRelayRuntimeDraftReleaseTransaction( + input({ archiveIdentities: [identity({ tupleId: '../escape' })], uploadImpl }) + ) + ).rejects.toThrow(/tuple|archive|identity/i) + await expect( + verifySshRelayRuntimeDraftReleaseTransaction( + input({ + archiveIdentities: [identity({ archive: { ...identity().archive, size: 1 } })], + uploadImpl + }) + ) + ).rejects.toThrow(/archive|asset|identity/i) + await mkdir(readbackDirectory) + await expect( + verifySshRelayRuntimeDraftReleaseTransaction(input({ uploadImpl })) + ).rejects.toThrow(/exclusive|output|directory/i) + expect(uploadImpl).not.toHaveBeenCalled() + }) + + it.each(['timeout', 'retry exhaustion', 'partial upload'])( + 'does not materialize after %s', + async (failure) => { + const uploadImpl = vi.fn(async () => { + throw new Error(failure) + }) + const materializeImpl = vi.fn() + + await expect( + verifySshRelayRuntimeDraftReleaseTransaction( + input({ uploadImpl, materializeImpl, executeImpl: vi.fn() }) + ) + ).rejects.toThrow(failure) + expect(materializeImpl).not.toHaveBeenCalled() + } + ) + + it('does not remove an output path it never owned after upload failure', async () => { + const marker = join(readbackDirectory, 'other-process.txt') + const uploadImpl = vi.fn(async () => { + await mkdir(readbackDirectory) + await writeFile(marker, 'not owned by the transaction') + throw new Error('upload failed before materialization') + }) + + await expect( + verifySshRelayRuntimeDraftReleaseTransaction( + input({ uploadImpl, materializeImpl: vi.fn(), executeImpl: vi.fn() }) + ) + ).rejects.toThrow(/upload failed/i) + await expect(readdir(readbackDirectory)).resolves.toEqual(['other-process.txt']) + }) + + it('rejects upload/read-back identity drift before the next phase', async () => { + const materializeImpl = vi.fn() + await expect( + verifySshRelayRuntimeDraftReleaseTransaction( + input({ + uploadImpl: vi.fn(async () => uploadResult({ releaseId: RELEASE_ID + 1 })), + materializeImpl, + executeImpl: vi.fn() + }) + ) + ).rejects.toThrow(/upload|release|identity|drift/i) + expect(materializeImpl).not.toHaveBeenCalled() + + const executeImpl = vi.fn() + await expect( + verifySshRelayRuntimeDraftReleaseTransaction( + input({ + uploadImpl: vi.fn(async () => uploadResult()), + materializeImpl: vi.fn(async () => materializeResult({ materializedAssets: [] })), + executeImpl + }) + ) + ).rejects.toThrow(/read-back|materialized|asset|drift/i) + expect(executeImpl).not.toHaveBeenCalled() + }) + + it('removes every owned output after materialization, execution, or cancellation failure', async () => { + for (const failure of ['materialization', 'execution', 'cancellation']) { + const controller = new AbortController() + const materializeImpl = vi.fn(async () => { + await mkdir(readbackDirectory) + if (failure === 'materialization') { + // The materializer owns and removes its partial transaction before rejecting. + await rm(readbackDirectory, { recursive: true, force: true }) + throw new Error('partial materialization') + } + return materializeResult() + }) + const executeImpl = vi.fn(async ({ outputDirectory }) => { + await mkdir(outputDirectory) + if (failure === 'cancellation') { + controller.abort(new Error('cancel transaction')) + controller.signal.throwIfAborted() + } + throw new Error('native execution failed') + }) + + await expect( + verifySshRelayRuntimeDraftReleaseTransaction( + input({ + uploadImpl: vi.fn(async () => uploadResult()), + materializeImpl, + executeImpl, + signal: controller.signal + }) + ) + ).rejects.toThrow(/materialization|execution|cancel|native/i) + await expect(readdir(readbackDirectory)).rejects.toMatchObject({ code: 'ENOENT' }) + await expect(readdir(executionDirectory)).rejects.toMatchObject({ code: 'ENOENT' }) + } + }) + + it('removes an earlier verified runtime when a later archive fails', async () => { + const secondAsset = { + name: SECOND_ARCHIVE_NAME, + path: join(root, SECOND_ARCHIVE_NAME), + sha256: digest(SECOND_ARCHIVE_BYTES), + size: SECOND_ARCHIVE_BYTES.length + } + await writeFile(secondAsset.path, SECOND_ARCHIVE_BYTES) + const allAssets = [...assets(), secondAsset] + const returnedAssets = allAssets.map(({ path: _path, ...asset }) => asset) + const secondIdentity = identity({ + tupleId: 'win32-x64', + contentId: `sha256:${'c'.repeat(64)}`, + archive: { ...returnedAssets.at(-1) } + }) + const executeImpl = vi.fn(async ({ identity: candidate, outputDirectory }) => { + await mkdir(outputDirectory) + if (candidate.tupleId === secondIdentity.tupleId) { + throw new Error('second archive native smoke failed') + } + return { + tupleId: candidate.tupleId, + contentId: candidate.contentId, + runtimeRoot: outputDirectory, + smoke: { nodeVersion: 'v24.18.0' } + } + }) + + await expect( + verifySshRelayRuntimeDraftReleaseTransaction( + input({ + assets: allAssets, + archiveIdentities: [identity(), secondIdentity], + uploadImpl: vi.fn(async () => uploadResult({ uploadedAssets: returnedAssets })), + materializeImpl: vi.fn(async () => { + await mkdir(readbackDirectory) + return { + releaseId: RELEASE_ID, + tag: TAG, + materializedAssets: returnedAssets.map((asset) => ({ + ...asset, + path: join(readbackDirectory, asset.name) + })) + } + }), + executeImpl + }) + ) + ).rejects.toThrow(/second archive native smoke failed/i) + expect(executeImpl).toHaveBeenCalledTimes(2) + await expect(readdir(readbackDirectory)).rejects.toMatchObject({ code: 'ENOENT' }) + await expect(readdir(executionDirectory)).rejects.toMatchObject({ code: 'ENOENT' }) + }) +}) 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 4a9fd228923..27890f1e118 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 @@ -17,7 +17,10 @@ target-native Node 24 jobs under `E-M4-READBACK-ARCHIVE-EXECUTION-CI-001`. PR Ch are green; the artifact workflow is red only for the retained Windows ARM build-26200-versus-26100 floor mismatch after successful runtime smoke. Next, compose the existing disconnected upload, recovery, materialization, and execution capabilities under purpose-named -failure-state tests without a release write or workflow caller. Production keys/environment/seed, +failure-state tests without a release write or workflow caller. The missing composition boundary is +recorded under `E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-RED-001`; the 10-test implementation and full +local regression set pass under `E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001`. Exact-head all-six +Node 24 proof is pending. Production keys/environment/seed, publication, desktop/default consumers, SSH transfer/install, tuple enablement, and merge to `main` remain disconnected. Current pushed branch head is `3779838f6`; the implementation owner is authorized to drive commits, pushes, CI, and PR-contained rehearsals end to end without merging. @@ -286,6 +289,10 @@ commits, pushes, CI, and PR-contained rehearsals end to end without merging. closed under `E-M4-READBACK-ARCHIVE-EXECUTION-CI-001`; a real authenticated release write/ read-back remains open. - [ ] Test timeouts, retries, approval denial, signing failure, partial output, and draft recovery. + Disconnected upload/materialization/execution ordering, timeout/retry/partial-output stops, + cancellation, identity drift, ownership-safe cleanup, and later-archive failure pass locally + under `E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001`. Protected approval and real signing failure + evidence remain blocked/open, so this item is not complete. ### WP4 — Desktop resolver and verified cache 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 f75963cc359..d308ad5c64e 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 4 / Work Package 3 disconnected draft-release failure composition — **In progress — 2026-07-15, Codex implementation owner**. Exclusive authenticated draft read-back materialization and exact materialized archive execution are closed locally and on all six exact-head native Node 24 jobs under E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001, E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001, E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001, E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-RED-001, E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001, and E-M4-READBACK-ARCHIVE-EXECUTION-CI-001. Next, compose the existing disconnected upload, recovery, materialization, and execution capabilities under purpose-named timeout/retry/partial-output/failure tests. Do not add a real release write, production workflow caller, publication, desktop consumer, SSH transfer, tuple enablement, or default behavior.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `3779838f66a57e3688367256510e096d17d78011` passes real first-build archive reconstruction, full-tree verification, bundled Node/native PTY/watcher smoke, and cleanup on all six target-native jobs in run 29438285824 under E-M4-READBACK-ARCHIVE-EXECUTION-CI-001. PR Checks 29438280473 and both Golden E2E jobs in 29438284781 are green. Both Linux supplements and Windows x64 floor pass. Windows arm64 floor smoke passes in 7,454.2658ms with 49,430,528-byte RSS; the floor gate then correctly rejects hosted build 26200 versus required 26100. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `3779838f66a57e3688367256510e096d17d78011` passes real first-build archive reconstruction, full-tree verification, bundled Node/native PTY/watcher smoke, and cleanup on all six target-native jobs in run 29438285824 under E-M4-READBACK-ARCHIVE-EXECUTION-CI-001. PR Checks 29438280473 and both Golden E2E jobs in 29438284781 are green. Both Linux supplements and Windows x64 floor pass. Windows arm64 floor smoke passes in 7,454.2658ms with 49,430,528-byte RSS; the floor gate then correctly rejects hosted build 26200 versus required 26100. The disconnected draft-release failure-composition RED and 10-test local implementation are recorded under E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-RED-001 and E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001; exact-head all-six Node 24 proof is next. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -126,11 +126,12 @@ same change as the work it records. all-six native execution are green under E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-RED-001, E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001, and E-M4-READBACK-ARCHIVE-EXECUTION-CI-001. -- Active package: Work Package 3 disconnected draft-release failure composition. Begin with a - purpose-named RED that composes only the existing upload/recovery/materialization/archive- - execution capabilities under explicit timeout, retry exhaustion, partial-output, cancellation, - and fail-closed sequencing contracts. Do not add a real release write, publication, desktop - consumer, SSH transfer/install, or production caller in this slice. +- Active package: Work Package 3 disconnected draft-release failure composition. The purpose-named + RED and 10-test local implementation are green under + E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-RED-001 and + E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001; exact-head all-six Node 24 proof is next. Do not add a + real release write, publication, desktop consumer, SSH transfer/install, or production caller in + this slice. - Completed Work Package 2 gate: target-native Windows source-signature reports from exact-head artifact jobs 87267322867 and 87267322870 were independently downloaded and matched to their identities and signing-stage reports under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001. PR Checks @@ -11778,6 +11779,82 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 4 files / 21 tests - Follow-up: checkpoint this evidence, then begin a purpose-named disconnected draft-release failure-composition RED. Retain all production/default disconnections. +### E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-RED-001 — Draft release failure composition is absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: local evidence commit `f801d3785` atop exact pushed head + `3779838f66a57e3688367256510e096d17d78011`; draft PR #8741. The only new uncommitted file is the + purpose-named RED. +- Command: `pnpm exec vitest run --config config/vitest.config.ts +config/scripts/ssh-relay-runtime-draft-release-verification.test.mjs`. +- Result: expected FAIL in 569ms; one suite / zero collected tests because + `ssh-relay-runtime-draft-release-verification.mjs` does not exist. +- Contract pinned by the RED: preflight exact archive/asset identities and exclusive output roots + before upload; order upload before authenticated materialization before archive execution; reject + upload/read-back/result identity drift between phases; never enter a later phase after timeout, + retry exhaustion, partial upload, or cancellation; remove all owned read-back and execution output + after materialization, native execution, or cancellation failure. +- Does not prove: implementation, GREEN, real GitHub requests, retry behavior inside the already- + proven upload primitive, authenticated network bytes, real release write/read-back, native signing/ + trust, desktop/SSH behavior, or any enabled tuple. +- Follow-up: implement only the disconnected dependency-injected composition boundary. Retain every + workflow, release-write, publication, product, tuple, and default disconnection. + +### E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001 — Failure composition passes locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted implementation atop local evidence commit `f801d3785`; exact pushed base + `3779838f66a57e3688367256510e096d17d78011`; draft PR #8741. Local runner is macOS 26.2 build 25C56 + arm64, Node v26.0.0, and pnpm 10.24.0. All release/upload/read-back/execution boundaries are test + doubles or local temporary files; no GitHub request or external write occurs. +- First GREEN command: production/test `node --check` plus `pnpm exec vitest run --config +config/vitest.config.ts config/scripts/ssh-relay-runtime-draft-release-verification.test.mjs` — + PASS, 7/7. Safety review then exposed an ownership race in the initial catch path: a failure before + materialization must not remove a path merely observed absent before a long upload. Cleanup now + removes only directories whose successful lower phase transferred ownership; the already-proven + materializer remains responsible for its own rejected partial transaction. A dedicated outsider- + path test pins the rule. +- Workflow-fixture correction signal: the first combined contract/workflow run had 1 failed / 16 + passed because the test expected six production-module filename occurrences instead of the actual + two production syntax checks plus four test-file occurrences. The assertion was corrected; no + production code or workflow behavior changed. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 config/scripts/ssh-relay-runtime-draft-release-verification.test.mjs` — PASS, 1 file + / 10 tests in 612ms Vitest and 2.75s wall; maximum RSS 132,890,624 bytes and zero swaps. +- Adjacent command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1` with draft release verification, upload, recovery, materialization, read-back, + archive execution/extraction, release stage, workflow, release assets, and protected required- + assets suites — PASS, 11 files / 90 tests in 10.41s Vitest and 13.30s wall; maximum RSS 134,496,256 + bytes and zero swaps. +- Broad command: `/usr/bin/time -l sh -c "rg --files config/scripts | rg +'/ssh-relay.*[.]test[.]mjs$' | xargs pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1"` — PASS, 56 files / 307 tests in 70.53s Vitest and 78.97s wall; maximum RSS + 189,743,104 bytes and zero swaps. +- Static gates: production/test `node --check`, focused `oxfmt --check`, and `git diff --check` pass. + `/usr/bin/time -l pnpm run typecheck` passes in 9.91s wall with 1,230,274,560-byte maximum RSS. + `/usr/bin/time -l pnpm run lint` passes in 24.32s wall with 2,019,295,232-byte maximum RSS; all 41 + reliability gates and the 355-entry max-lines ratchet pass, with the existing 26 unrelated + warnings. The protected Node/npm resolver and desktop required-asset files retain zero diff. +- Oracle proved: exact bounded release identity, managed asset set, runtime archive coverage, tuple/ + content/archive identity, and disjoint exclusive output roots are validated before upload. Upload + must return one exact reusable/uploaded partition before authenticated materialization starts; + materialization must return every exact physical asset before execution starts. Timeout, retry + exhaustion, partial upload, cancellation, and phase identity drift prevent later phases. A later + archive failure removes earlier verified runtimes and all owned read-back bytes. An upload failure + cannot remove a concurrently created path the transaction never owned. The overall timeout is 45 + minutes and the existing 26-asset, 100 MiB-per-asset, and 1 GiB-total bounds are retained. +- Consumer-disconnection oracle: both native artifact job families perform only syntax and contract- + test execution. There is no CLI, workflow composition caller, token/credential, real release write, + publication, desktop consumer, SSH transfer/install, tuple enablement, or default behavior change. +- Does not prove: Node 24/native-runner portability, real GitHub timeout/retry/partial-release state, + authenticated network bytes, real release write/read-back, protected approval/signing failures, + native signing/trust, desktop/SSH behavior, performance against legacy, or any enabled tuple. +- Follow-up: commit and push the isolated package with evidence checkpoint `f801d3785`, then require + the 10-test suite to pass on all six exact-head native Node 24 jobs. Preserve every production/ + default disconnection. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 8c925c24990ac93f6d2dd8de2081a50a74f30d97 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 14:29:59 -0400 Subject: [PATCH 147/267] docs: checkpoint relay draft release verification Co-authored-by: Orca --- ...ssh-relay-github-release-implementation-checklist-summary.md | 2 +- ...6-07-14-ssh-relay-github-release-implementation-checklist.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 27890f1e118..cc41443427a 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 @@ -20,7 +20,7 @@ disconnected upload, recovery, materialization, and execution capabilities under failure-state tests without a release write or workflow caller. The missing composition boundary is recorded under `E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-RED-001`; the 10-test implementation and full local regression set pass under `E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001`. Exact-head all-six -Node 24 proof is pending. Production keys/environment/seed, +Node 24 proof is pending at local implementation commit `b6bb3dd9a`. Production keys/environment/seed, publication, desktop/default consumers, SSH transfer/install, tuple enablement, and merge to `main` remain disconnected. Current pushed branch head is `3779838f6`; the implementation owner is authorized to drive commits, pushes, CI, and PR-contained rehearsals end to end without merging. 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 d308ad5c64e..cefc24cd9de 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 4 / Work Package 3 disconnected draft-release failure composition — **In progress — 2026-07-15, Codex implementation owner**. Exclusive authenticated draft read-back materialization and exact materialized archive execution are closed locally and on all six exact-head native Node 24 jobs under E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001, E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001, E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001, E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-RED-001, E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001, and E-M4-READBACK-ARCHIVE-EXECUTION-CI-001. Next, compose the existing disconnected upload, recovery, materialization, and execution capabilities under purpose-named timeout/retry/partial-output/failure tests. Do not add a real release write, production workflow caller, publication, desktop consumer, SSH transfer, tuple enablement, or default behavior.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `3779838f66a57e3688367256510e096d17d78011` passes real first-build archive reconstruction, full-tree verification, bundled Node/native PTY/watcher smoke, and cleanup on all six target-native jobs in run 29438285824 under E-M4-READBACK-ARCHIVE-EXECUTION-CI-001. PR Checks 29438280473 and both Golden E2E jobs in 29438284781 are green. Both Linux supplements and Windows x64 floor pass. Windows arm64 floor smoke passes in 7,454.2658ms with 49,430,528-byte RSS; the floor gate then correctly rejects hosted build 26200 versus required 26100. The disconnected draft-release failure-composition RED and 10-test local implementation are recorded under E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-RED-001 and E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001; exact-head all-six Node 24 proof is next. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `3779838f66a57e3688367256510e096d17d78011` passes real first-build archive reconstruction, full-tree verification, bundled Node/native PTY/watcher smoke, and cleanup on all six target-native jobs in run 29438285824 under E-M4-READBACK-ARCHIVE-EXECUTION-CI-001. PR Checks 29438280473 and both Golden E2E jobs in 29438284781 are green. Both Linux supplements and Windows x64 floor pass. Windows arm64 floor smoke passes in 7,454.2658ms with 49,430,528-byte RSS; the floor gate then correctly rejects hosted build 26200 versus required 26100. The disconnected draft-release failure-composition RED and 10-test local implementation at `b6bb3dd9a` are recorded under E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-RED-001 and E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001; exact-head all-six Node 24 proof is next. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) From 2097a40dd8bc27ee5dc1f231229b443c12a1540a Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 14:51:09 -0400 Subject: [PATCH 148/267] docs: record relay draft release composition CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 27 +++--- ...github-release-implementation-checklist.md | 83 +++++++++++++++---- 2 files changed, 79 insertions(+), 31 deletions(-) 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 cc41443427a..2dc16010375 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 @@ -9,20 +9,18 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 4 / Work Package 3 disconnected draft-release failure composition — +Active checkpoint: **Milestone 5 / Work Package 4 offline artifact selection — in progress, 2026-07-15, Codex implementation owner.** Exact-head run -[29438285824](https://github.com/stablyai/orca/actions/runs/29438285824) at `3779838f6` passes the -10-test execution contract and real first-build archive reconstruction/native smoke on all six -target-native Node 24 jobs under `E-M4-READBACK-ARCHIVE-EXECUTION-CI-001`. PR Checks and Golden E2E -are green; the artifact workflow is red only for the retained Windows ARM -build-26200-versus-26100 floor mismatch after successful runtime smoke. Next, compose the existing -disconnected upload, recovery, materialization, and execution capabilities under purpose-named -failure-state tests without a release write or workflow caller. The missing composition boundary is -recorded under `E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-RED-001`; the 10-test implementation and full -local regression set pass under `E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001`. Exact-head all-six -Node 24 proof is pending at local implementation commit `b6bb3dd9a`. Production keys/environment/seed, +[29440806947](https://github.com/stablyai/orca/actions/runs/29440806947) at `8c925c249` passes the +10-test draft-release composition contract and full runtime construction/execution on all six +target-native Node 24 jobs under `E-M4-DRAFT-RELEASE-COMPOSITION-CI-001`. PR Checks and Golden E2E +are green; the artifact workflow is red only for the retained Windows ARM build-26200-versus-26100 +floor mismatch after successful runtime smoke. A real draft release write/read-back remains blocked +because it is not PR-contained and the production signer/environment are unprovisioned; the safe +default is no write. Next, begin the purpose-named offline tuple/content/immutable-URL selector RED +without download, cache, desktop consumer, or mode wiring. Production keys/environment/seed, publication, desktop/default consumers, SSH transfer/install, tuple enablement, and merge to `main` -remain disconnected. Current pushed branch head is `3779838f6`; the implementation owner is authorized to drive +remain disconnected. Current pushed branch head is `8c925c249`; the implementation owner is authorized to drive commits, pushes, CI, and PR-contained rehearsals end to end without merging. ## Safety status @@ -291,8 +289,9 @@ commits, pushes, CI, and PR-contained rehearsals end to end without merging. - [ ] Test timeouts, retries, approval denial, signing failure, partial output, and draft recovery. Disconnected upload/materialization/execution ordering, timeout/retry/partial-output stops, cancellation, identity drift, ownership-safe cleanup, and later-archive failure pass locally - under `E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001`. Protected approval and real signing failure - evidence remain blocked/open, so this item is not complete. + under `E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001` and on all six native Node 24 jobs under + `E-M4-DRAFT-RELEASE-COMPOSITION-CI-001`. Protected approval and real signing failure evidence + remain blocked/open, so this item is not complete. ### WP4 — Desktop resolver and verified cache 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 cefc24cd9de..de8661d346b 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md @@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    -Current phase: Milestone 4 / Work Package 3 disconnected draft-release failure composition — **In progress — 2026-07-15, Codex implementation owner**. Exclusive authenticated draft read-back materialization and exact materialized archive execution are closed locally and on all six exact-head native Node 24 jobs under E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-RED-001, E-M4-DRAFT-READBACK-MATERIALIZATION-LOCAL-001, E-M4-DRAFT-READBACK-MATERIALIZATION-CI-001, E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-RED-001, E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001, and E-M4-READBACK-ARCHIVE-EXECUTION-CI-001. Next, compose the existing disconnected upload, recovery, materialization, and execution capabilities under purpose-named timeout/retry/partial-output/failure tests. Do not add a real release write, production workflow caller, publication, desktop consumer, SSH transfer, tuple enablement, or default behavior.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `3779838f66a57e3688367256510e096d17d78011` passes real first-build archive reconstruction, full-tree verification, bundled Node/native PTY/watcher smoke, and cleanup on all six target-native jobs in run 29438285824 under E-M4-READBACK-ARCHIVE-EXECUTION-CI-001. PR Checks 29438280473 and both Golden E2E jobs in 29438284781 are green. Both Linux supplements and Windows x64 floor pass. Windows arm64 floor smoke passes in 7,454.2658ms with 49,430,528-byte RSS; the floor gate then correctly rejects hosted build 26200 versus required 26100. The disconnected draft-release failure-composition RED and 10-test local implementation at `b6bb3dd9a` are recorded under E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-RED-001 and E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001; exact-head all-six Node 24 proof is next. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 offline artifact selection — **In progress — 2026-07-15, Codex implementation owner**. Milestone 4 disconnected upload/recovery/materialization/execution composition is closed locally and on all six exact-head native Node 24 jobs under E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-RED-001, E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001, and E-M4-DRAFT-RELEASE-COMPOSITION-CI-001. A real draft release write/read-back is not PR-contained and remains blocked by the unprovisioned production signer/environment; safe default is no write. Begin only offline tuple/content/immutable-URL selection contracts; do not add download, cache, desktop consumer, SSH transfer, tuple enablement, mode wiring, or default behavior.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `8c925c24990ac93f6d2dd8de2081a50a74f30d97` passes the 10-test draft-release composition suite and full runtime construction/execution on all six target-native jobs in run 29440806947 under E-M4-DRAFT-RELEASE-COMPOSITION-CI-001. PR Checks 29440804969 and both Golden E2E jobs in 29440804978 are green. Both Linux supplements and Windows x64 floor pass. Windows arm64 floor smoke passes in 6,178.8338ms with 49,881,088-byte RSS; the floor gate then correctly rejects hosted build 26200 versus required 26100. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -126,12 +126,15 @@ same change as the work it records. all-six native execution are green under E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-RED-001, E-M4-READBACK-ARCHIVE-EXECUTION-LOCAL-001, and E-M4-READBACK-ARCHIVE-EXECUTION-CI-001. -- Active package: Work Package 3 disconnected draft-release failure composition. The purpose-named - RED and 10-test local implementation are green under - E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-RED-001 and - E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001; exact-head all-six Node 24 proof is next. Do not add a - real release write, publication, desktop consumer, SSH transfer/install, or production caller in - this slice. +- Completed package: Work Package 3 disconnected draft-release failure composition. The purpose- + named RED, 10-test local implementation, and exact-head all-six Node 24 proof are green under + E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-RED-001, + E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001, and + E-M4-DRAFT-RELEASE-COMPOSITION-CI-001. +- Active package: Work Package 4 offline artifact selection. Begin with a purpose-named RED for + exact offline tuple/content selection and immutable GitHub release URL construction from an + already-verified embedded manifest. Do not add signature key policy, networking, download/cache, + desktop call sites, SSH behavior, mode wiring, tuple enablement, or default behavior in this slice. - Completed Work Package 2 gate: target-native Windows source-signature reports from exact-head artifact jobs 87267322867 and 87267322870 were independently downloaded and matched to their identities and signing-stage reports under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001. PR Checks @@ -11855,6 +11858,52 @@ config/vitest.config.ts config/scripts/ssh-relay-runtime-draft-release-verificat the 10-test suite to pass on all six exact-head native Node 24 jobs. Preserve every production/ default disconnection. +### E-M4-DRAFT-RELEASE-COMPOSITION-CI-001 — Failure composition passes all six native Node 24 jobs + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `8c925c24990ac93f6d2dd8de2081a50a74f30d97`; implementation commit + `b6bb3dd9a`; draft PR #8741. +- Workflow: [SSH Relay Runtime Artifacts run 29440806947](https://github.com/stablyai/orca/actions/runs/29440806947), + 18:30:21Z–18:47:19Z. The aggregate conclusion is the expected failure only because the separate + Windows arm64 floor job rejects hosted build 26200 after successful runtime execution. +- Native jobs and 10-test suite timings: + - Linux x64 job 87438955106, `ubuntu-24.04`, image `ubuntu24` `20260714.240.1`, PASS in 4m33s; + suite 57ms. + - Linux arm64 job 87438955082, `ubuntu-24.04-arm`, image `ubuntu24-arm64` `20260706.52.2`, PASS in + 4m56s; suite 48ms. + - Darwin x64 job 87438955142, `macos-15-intel`, image `macos15` `20260629.0276.1`, PASS in 7m10s; + suite 103ms. + - Darwin arm64 job 87438955086, `macos-15`, image `macos15` `20260706.0213.1`, PASS in 4m24s; + suite 83ms. + - Windows x64 job 87438955074, `windows-2022`, image `win22` `20260706.237.1`, PASS in 5m44s; + suite 151ms. + - Windows arm64 job 87438955084, `windows-11-arm`, image `win11-arm64` `20260714.109.1`, PASS in + 11m17s; suite 165ms. +- Broad contract counts: each POSIX job passes 55 files / 303 tests; durations are Linux x64 7.81s, + Linux arm64 6.43s, Darwin x64 23.67s, and Darwin arm64 12.01s. Each Windows job passes 56 files / + 297 tests plus 10 platform skips of 307; x64 takes 13.76s and arm64 17.76s. Every job also rebuilds, + compares, reconstructs, and executes its real target-native archive successfully. +- Downstream/adjacent evidence: both Linux supplement jobs pass; Windows x64 floor passes. Windows + arm64 floor job 87441657346 uses image `20260706.102.1`; bundled Node/PTY/watcher smoke passes in + 6,178.8338ms with 49,881,088-byte RSS and the complete verifier takes 8,186.4482ms before the gate + rejects observed OS/kernel build 26200 versus required build 26100. PR Checks run + [29440804969](https://github.com/stablyai/orca/actions/runs/29440804969), job 87438947689, passes in + 13m57s. Golden E2E run [29440804978](https://github.com/stablyai/orca/actions/runs/29440804978) + passes macOS job 87438947704 in 3m12s and Linux job 87438947780 in 4m25s. +- Oracle proved: both native workflow families parse and execute the exact 10-test bounded asset/ + archive coverage, phase ordering, timeout/retry/partial-output stop, cancellation, identity drift, + ownership-safe cleanup, and later-archive failure contract consistently on x64 and arm64. Full + artifact construction/execution and adjacent product regressions remain green. +- Consumer-disconnection oracle: the workflow invokes only syntax and contract tests. There is no + CLI, release/workflow caller, credential, GitHub write, publication, desktop/SSH consumer, tuple + enablement, or default behavior change. +- Does not prove: real GitHub timeout/retry/partial-release behavior, a real draft write/read-back, + protected approval/signing failures, native trust, desktop resolver/cache, SSH transfer/install, + performance against legacy, or any enabled tuple. +- Follow-up: checkpoint this evidence. A real release write is not PR-contained and remains blocked; + retain no-write as the safe default and begin the independent Milestone 5 offline selector RED. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -11913,15 +11962,15 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Begin a purpose-named disconnected draft-release failure-composition RED. It must orchestrate only -the existing bounded upload/recovery, authenticated materialization, and exact archive-execution -capabilities through injected credential-free boundaries. Pin phase ordering and cleanup for timeout, -retry exhaustion, partial upload/output, cancellation, release-identity drift, materialization -failure, and archive-execution failure. Do not add a real release write, publication, desktop -consumer, SSH transfer/install, tuple enablement, production caller, or workflow connection in this -package. Keep the existing desktop `verify-release-required-assets.mjs` behavior and detached- -signature byte encoding unchanged. The protected environment, accepted production keys, and secret -remain unprovisioned, so live signing evidence stays BLOCKED. +Begin a purpose-named Milestone 5 offline artifact-selection RED. It must accept only an already- +verified immutable embedded manifest, map the existing detected relay OS/architecture plus explicit +libc classification to one exact tuple/content ID, and construct the exact immutable +`/releases/download//` URL without an API/latest/freshness lookup. Unknown, ambiguous, +unsupported, musl, or inconsistent inputs must return a classified unavailable result suitable for +the still-disconnected legacy path; manifest/identity drift must fail closed. Do not add signature +key policy, networking, redirect handling, download/cache, desktop call sites, SSH transfer/install, +mode wiring, tuple enablement, or default behavior in this package. Keep `ORCA_RELAY_PATH`, existing +desktop required-assets behavior, and detached-signature byte encoding unchanged. Keep Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. Do not merge to `main`; retain every production/default gate. From 7b8c7669aced2ac2b54b9f7da64df5c1f9caff33 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 15:02:26 -0400 Subject: [PATCH 149/267] feat(ssh): bind relay selection to verified manifest Co-authored-by: Orca --- .../ssh/ssh-relay-artifact-selector.test.ts | 101 ++++++++------ src/main/ssh/ssh-relay-artifact-selector.ts | 66 ++++++--- .../ssh/ssh-relay-artifact-test-manifest.ts | 129 ++++++++++++++++++ .../ssh/ssh-relay-manifest-signature.test.ts | 10 ++ src/main/ssh/ssh-relay-manifest-signature.ts | 27 +++- 5 files changed, 276 insertions(+), 57 deletions(-) diff --git a/src/main/ssh/ssh-relay-artifact-selector.test.ts b/src/main/ssh/ssh-relay-artifact-selector.test.ts index 522a0e791e6..f702a612a60 100644 --- a/src/main/ssh/ssh-relay-artifact-selector.test.ts +++ b/src/main/ssh/ssh-relay-artifact-selector.test.ts @@ -1,8 +1,18 @@ -import { describe, expect, it } from 'vitest' - -import { createSshRelayArtifactTestManifest } from './ssh-relay-artifact-test-manifest' +import nacl from 'tweetnacl' +import { describe, expect, expectTypeOf, it } from 'vitest' + +import { + createSshRelayArtifactTestManifest, + createSshRelayDarwinArtifactTestManifest, + createSshRelayWindowsArtifactTestManifest +} from './ssh-relay-artifact-test-manifest' import { parseSshRelayArtifactManifest } from './ssh-relay-artifact-schema' import { selectSshRelayArtifact } from './ssh-relay-artifact-selector' +import { + signSshRelayArtifactManifest, + sshRelayManifestKeyId, + verifySshRelayArtifactManifest +} from './ssh-relay-manifest-signature' import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity' import { sshRelayRuntimeArchiveName } from './ssh-relay-release-asset' @@ -16,6 +26,20 @@ const compatibleLinuxHost = { glibcxxVersion: '3.4.33' } +const manifestKeyPair = nacl.sign.keyPair.fromSeed( + Uint8Array.from({ length: 32 }, (_, index) => index) +) + +function verifiedManifest(manifest = createSshRelayArtifactTestManifest()) { + manifest.signatures = [signSshRelayArtifactManifest(manifest, manifestKeyPair.secretKey)] + return verifySshRelayArtifactManifest(manifest, [ + { + keyId: sshRelayManifestKeyId(manifestKeyPair.publicKey), + publicKey: manifestKeyPair.publicKey + } + ]) +} + function windowsManifest({ architecture = 'x64', minimumBuild = architecture === 'x64' ? 19045 : 26100 @@ -23,22 +47,7 @@ function windowsManifest({ architecture?: 'x64' | 'arm64' minimumBuild?: number } = {}) { - const manifest = createSshRelayArtifactTestManifest() - const tuple = structuredClone(manifest.tuples[0]) as unknown as Record - Object.assign(tuple, { - tupleId: `win32-${architecture}`, - os: 'win32', - architecture, - compatibility: { - kind: 'windows', - minimumBuild, - minimumOpenSshVersion: '8.1p1', - minimumPowerShellVersion: '5.1', - minimumDotNetFrameworkRelease: 528040 - } - }) - manifest.tuples = [tuple as never] - return manifest + return createSshRelayWindowsArtifactTestManifest({ architecture, minimumBuild }) } const compatibleWindowsHost = { @@ -52,15 +61,38 @@ const compatibleWindowsHost = { } describe('SSH relay artifact selector', () => { + it('requires a signature-verified manifest type', () => { + expectTypeOf(createSshRelayArtifactTestManifest()).not.toMatchTypeOf< + Parameters[0] + >() + }) + it('selects the single compatible glibc tuple', () => { - const result = selectSshRelayArtifact(createSshRelayArtifactTestManifest(), compatibleLinuxHost) + const manifest = verifiedManifest() + const result = selectSshRelayArtifact(manifest, compatibleLinuxHost) - expect(result).toMatchObject({ kind: 'selected', tupleId: 'linux-x64-glibc' }) + expect(result).toMatchObject({ + kind: 'selected', + tupleId: 'linux-x64-glibc', + contentId: manifest.tuples[0].contentId, + releaseTag: 'v1.4.140-rc.1', + archive: { + name: manifest.tuples[0].archive.name, + sha256: manifest.tuples[0].archive.sha256, + downloadUrl: `https://github.com/stablyai/orca/releases/download/v1.4.140-rc.1/${manifest.tuples[0].archive.name}` + } + }) + expect(JSON.stringify(result)).not.toContain('latest') + expect(() => { + if (result.kind === 'selected') { + Object.defineProperty(result.archive, 'downloadUrl', { value: 'https://example.com' }) + } + }).toThrow(TypeError) }) it('accepts every Linux compatibility value at its exact minimum', () => { expect( - selectSshRelayArtifact(createSshRelayArtifactTestManifest(), { + selectSshRelayArtifact(verifiedManifest(), { ...compatibleLinuxHost, kernelVersion: '4.18', libc: { family: 'glibc', version: '2.28' }, @@ -97,13 +129,14 @@ describe('SSH relay artifact selector', () => { musl.archive.name = sshRelayRuntimeArchiveName(musl.tupleId, musl.contentId) manifest.tuples.push(musl) const parsed = parseSshRelayArtifactManifest(manifest) + const verified = verifiedManifest(parsed) - expect(selectSshRelayArtifact(parsed, compatibleLinuxHost)).toMatchObject({ + expect(selectSshRelayArtifact(verified, compatibleLinuxHost)).toMatchObject({ kind: 'selected', tupleId: 'linux-x64-glibc' }) expect( - selectSshRelayArtifact(parsed, { + selectSshRelayArtifact(verified, { ...compatibleLinuxHost, libc: { family: 'musl', version: '1.2.5' } }) @@ -127,14 +160,14 @@ describe('SSH relay artifact selector', () => { [{ ...compatibleLinuxHost, processTranslated: true }, 'translated-process'], [{ ...compatibleLinuxHost, architecture: 'arm64' as const }, 'tuple-unavailable'] ])('selects legacy for incompatible or unknown host evidence', (host, reason) => { - expect(selectSshRelayArtifact(createSshRelayArtifactTestManifest(), host)).toEqual({ + expect(selectSshRelayArtifact(verifiedManifest(), host)).toEqual({ kind: 'legacy', reason }) }) it('checks Windows bootstrap versions before selection', () => { - const manifest = windowsManifest() + const manifest = verifiedManifest(windowsManifest()) expect(selectSshRelayArtifact(manifest, compatibleWindowsHost).kind).toBe('selected') expect(selectSshRelayArtifact(manifest, { ...compatibleWindowsHost, build: 19044 })).toEqual({ kind: 'legacy', @@ -166,7 +199,7 @@ describe('SSH relay artifact selector', () => { ] as const)( 'enforces the reviewed Windows %s build boundary', (architecture, rejectedBuild, acceptedBuild) => { - const manifest = windowsManifest({ architecture }) + const manifest = verifiedManifest(windowsManifest({ architecture })) const host = { ...compatibleWindowsHost, architecture } expect(selectSshRelayArtifact(manifest, { ...host, build: rejectedBuild })).toEqual({ @@ -180,7 +213,7 @@ describe('SSH relay artifact selector', () => { ) it('selects legacy when Windows bootstrap evidence is absent or malformed', () => { - const manifest = windowsManifest() + const manifest = verifiedManifest(windowsManifest()) expect( selectSshRelayArtifact(manifest, { ...compatibleWindowsHost, openSshVersion: '8.1.0' }) ).toEqual({ kind: 'legacy', reason: 'unknown-openssh' }) @@ -196,18 +229,10 @@ describe('SSH relay artifact selector', () => { }) it('checks the minimum macOS version before selection', () => { - const manifest = createSshRelayArtifactTestManifest() - const tuple = structuredClone(manifest.tuples[0]) as unknown as Record - Object.assign(tuple, { - tupleId: 'darwin-arm64', - os: 'darwin', - architecture: 'arm64', - compatibility: { kind: 'darwin', minimumVersion: '13.5' } - }) - manifest.tuples = [tuple as never] + const manifest = createSshRelayDarwinArtifactTestManifest() expect( - selectSshRelayArtifact(manifest, { + selectSshRelayArtifact(verifiedManifest(manifest), { os: 'darwin', architecture: 'arm64', processTranslated: false, diff --git a/src/main/ssh/ssh-relay-artifact-selector.ts b/src/main/ssh/ssh-relay-artifact-selector.ts index 32a118eca75..d3cf74818cd 100644 --- a/src/main/ssh/ssh-relay-artifact-selector.ts +++ b/src/main/ssh/ssh-relay-artifact-selector.ts @@ -1,4 +1,7 @@ -import type { SshRelayArtifactManifest, SshRelayRuntimeTuple } from './ssh-relay-artifact-schema' +import type { VerifiedSshRelayArtifactManifest } from './ssh-relay-manifest-signature' +import { sshRelayRuntimeDownloadUrl } from './ssh-relay-release-asset' + +type VerifiedSshRelayRuntimeTuple = VerifiedSshRelayArtifactManifest['tuples'][number] type SshRelayHostBase = { architecture: 'x64' | 'arm64' @@ -52,9 +55,37 @@ export type SshRelayArtifactLegacyReason = | 'dotnet-too-old' export type SshRelayArtifactSelection = - | { kind: 'selected'; tupleId: SshRelayRuntimeTuple['tupleId']; tuple: SshRelayRuntimeTuple } + | { + readonly kind: 'selected' + readonly tupleId: VerifiedSshRelayRuntimeTuple['tupleId'] + readonly contentId: VerifiedSshRelayRuntimeTuple['contentId'] + readonly releaseTag: string + readonly archive: VerifiedSshRelayRuntimeTuple['archive'] & { + readonly downloadUrl: string + } + readonly tuple: VerifiedSshRelayRuntimeTuple + } | { kind: 'legacy'; reason: SshRelayArtifactLegacyReason } +function selectedArtifact( + tuple: VerifiedSshRelayRuntimeTuple, + releaseTag: string +): SshRelayArtifactSelection { + const archive = Object.freeze({ + ...tuple.archive, + downloadUrl: sshRelayRuntimeDownloadUrl(releaseTag, tuple.archive.name) + }) + // Why: later download code must not be able to drift the authenticated identity selected here. + return Object.freeze({ + kind: 'selected', + tupleId: tuple.tupleId, + contentId: tuple.contentId, + releaseTag, + archive, + tuple + }) +} + function parseNumericVersion(value: string | undefined): number[] | null { if (!value) { return null @@ -104,8 +135,9 @@ function meetsVersion( } function selectLinux( - tuple: SshRelayRuntimeTuple, - host: SshRelayLinuxHostEvidence + tuple: VerifiedSshRelayRuntimeTuple, + host: SshRelayLinuxHostEvidence, + releaseTag: string ): SshRelayArtifactSelection { if (tuple.compatibility.kind !== 'linux') { return { kind: 'legacy', reason: 'tuple-inconsistent' } @@ -147,12 +179,13 @@ function selectLinux( return { kind: 'legacy', reason: 'libstdcxx-too-old' } } } - return { kind: 'selected', tupleId: tuple.tupleId, tuple } + return selectedArtifact(tuple, releaseTag) } function selectDarwin( - tuple: SshRelayRuntimeTuple, - host: SshRelayDarwinHostEvidence + tuple: VerifiedSshRelayRuntimeTuple, + host: SshRelayDarwinHostEvidence, + releaseTag: string ): SshRelayArtifactSelection { if (tuple.compatibility.kind !== 'darwin') { return { kind: 'legacy', reason: 'tuple-inconsistent' } @@ -161,14 +194,13 @@ function selectDarwin( if (compatible === null) { return { kind: 'legacy', reason: 'unknown-os-version' } } - return compatible - ? { kind: 'selected', tupleId: tuple.tupleId, tuple } - : { kind: 'legacy', reason: 'os-too-old' } + return compatible ? selectedArtifact(tuple, releaseTag) : { kind: 'legacy', reason: 'os-too-old' } } function selectWindows( - tuple: SshRelayRuntimeTuple, - host: SshRelayWindowsHostEvidence + tuple: VerifiedSshRelayRuntimeTuple, + host: SshRelayWindowsHostEvidence, + releaseTag: string ): SshRelayArtifactSelection { if (tuple.compatibility.kind !== 'windows') { return { kind: 'legacy', reason: 'tuple-inconsistent' } @@ -209,11 +241,11 @@ function selectWindows( if (host.dotNetFrameworkRelease < tuple.compatibility.minimumDotNetFrameworkRelease) { return { kind: 'legacy', reason: 'dotnet-too-old' } } - return { kind: 'selected', tupleId: tuple.tupleId, tuple } + return selectedArtifact(tuple, releaseTag) } export function selectSshRelayArtifact( - manifest: SshRelayArtifactManifest, + manifest: VerifiedSshRelayArtifactManifest, host: SshRelayHostEvidence ): SshRelayArtifactSelection { // Why: translated and ambiguous process boundaries stay on the proven legacy path until their @@ -249,10 +281,10 @@ export function selectSshRelayArtifact( const tuple = candidates[0] if (host.os === 'linux') { - return selectLinux(tuple, host) + return selectLinux(tuple, host, manifest.build.tag) } if (host.os === 'darwin') { - return selectDarwin(tuple, host) + return selectDarwin(tuple, host, manifest.build.tag) } - return selectWindows(tuple, host) + return selectWindows(tuple, host, manifest.build.tag) } diff --git a/src/main/ssh/ssh-relay-artifact-test-manifest.ts b/src/main/ssh/ssh-relay-artifact-test-manifest.ts index 72fc5e3befb..1266db222d7 100644 --- a/src/main/ssh/ssh-relay-artifact-test-manifest.ts +++ b/src/main/ssh/ssh-relay-artifact-test-manifest.ts @@ -166,3 +166,132 @@ export function createSshRelayArtifactTestManifest(): SshRelayArtifactManifest { ] } } + +function finalizeTestTuple(manifest: SshRelayArtifactManifest): SshRelayArtifactManifest { + const tuple = manifest.tuples[0] + const files = tuple.entries.filter((entry) => entry.type === 'file') + tuple.contentId = computeSshRelayRuntimeContentId(tuple) + tuple.archive.name = sshRelayRuntimeArchiveName(tuple.tupleId, tuple.contentId) + tuple.archive.fileCount = files.length + tuple.archive.expandedSize = files.reduce((total, entry) => total + entry.size, 0) + return manifest +} + +export function createSshRelayWindowsArtifactTestManifest({ + architecture = 'x64', + minimumBuild = architecture === 'x64' ? 19045 : 26100 +}: { + architecture?: 'x64' | 'arm64' + minimumBuild?: number +} = {}): SshRelayArtifactManifest { + const manifest = createSshRelayArtifactTestManifest() + const tuple = manifest.tuples[0] + tuple.tupleId = `win32-${architecture}` + tuple.os = 'win32' + tuple.architecture = architecture + tuple.compatibility = { + kind: 'windows', + minimumBuild, + minimumOpenSshVersion: '8.1p1', + minimumPowerShellVersion: '5.1', + minimumDotNetFrameworkRelease: 528040 + } + const watcherPackage = `watcher-win32-${architecture}` + for (const entry of tuple.entries) { + entry.path = entry.path + .replace('bin/node', 'bin/node.exe') + .replace('watcher-linux-x64-glibc', watcherPackage) + .replace( + 'node_modules/node-pty/build/Release/pty.node', + 'node_modules/node-pty/build/Release/conpty.node' + ) + } + tuple.entries.push( + { + path: 'node_modules/node-pty/build/Release/conpty', + type: 'directory', + mode: 0o755 + }, + { + path: 'node_modules/node-pty/build/Release/conpty_console_list.node', + type: 'file', + role: 'node-pty-native', + size: 31, + mode: 0o755, + sha256: digest('9') + }, + { + path: 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe', + type: 'file', + role: 'native-runtime', + size: 32, + mode: 0o755, + sha256: digest('a') + }, + { + path: 'node_modules/node-pty/build/Release/conpty/conpty.dll', + type: 'file', + role: 'native-runtime', + size: 33, + mode: 0o755, + sha256: digest('b') + } + ) + tuple.nativeVerification.policy = 'signpath-authenticode-v1' + for (const file of tuple.nativeVerification.files) { + file.path = file.path + .replace('bin/node', 'bin/node.exe') + .replace('watcher-linux-x64-glibc', watcherPackage) + .replace( + 'node_modules/node-pty/build/Release/pty.node', + 'node_modules/node-pty/build/Release/conpty.node' + ) + } + tuple.nativeVerification.files.push( + { + path: 'node_modules/node-pty/build/Release/conpty_console_list.node', + sha256: digest('9') + }, + { + path: 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe', + sha256: digest('a') + }, + { + path: 'node_modules/node-pty/build/Release/conpty/conpty.dll', + sha256: digest('b') + } + ) + return finalizeTestTuple(manifest) +} + +export function createSshRelayDarwinArtifactTestManifest( + architecture: 'x64' | 'arm64' = 'arm64' +): SshRelayArtifactManifest { + const manifest = createSshRelayArtifactTestManifest() + const tuple = manifest.tuples[0] + tuple.tupleId = `darwin-${architecture}` + tuple.os = 'darwin' + tuple.architecture = architecture + tuple.compatibility = { kind: 'darwin', minimumVersion: '13.5' } + const watcherPackage = `watcher-darwin-${architecture}` + for (const entry of tuple.entries) { + entry.path = entry.path.replace('watcher-linux-x64-glibc', watcherPackage) + } + tuple.entries.push({ + path: 'node_modules/node-pty/build/Release/spawn-helper', + type: 'file', + role: 'native-runtime', + size: 31, + mode: 0o755, + sha256: digest('9') + }) + tuple.nativeVerification.policy = 'apple-developer-id-v1' + for (const file of tuple.nativeVerification.files) { + file.path = file.path.replace('watcher-linux-x64-glibc', watcherPackage) + } + tuple.nativeVerification.files.push({ + path: 'node_modules/node-pty/build/Release/spawn-helper', + sha256: digest('9') + }) + return finalizeTestTuple(manifest) +} diff --git a/src/main/ssh/ssh-relay-manifest-signature.test.ts b/src/main/ssh/ssh-relay-manifest-signature.test.ts index 474888430dd..8516579e104 100644 --- a/src/main/ssh/ssh-relay-manifest-signature.test.ts +++ b/src/main/ssh/ssh-relay-manifest-signature.test.ts @@ -89,6 +89,16 @@ describe('SSH relay manifest signatures', () => { expect(verified.tuples[0].contentId).toBe(manifest.tuples[0].contentId) }) + it('makes verified manifest fields immutable after signature acceptance', () => { + const verified = verifySshRelayArtifactManifest(signedManifest(), [ + { keyId: sshRelayManifestKeyId(keyPair.publicKey), publicKey: keyPair.publicKey } + ]) + + expect(() => { + Object.defineProperty(verified.tuples[0].archive, 'name', { value: 'latest.tar.xz' }) + }).toThrow(TypeError) + }) + it('rejects signed content mutation', () => { const manifest = signedManifest() manifest.build.relayProtocolVersion += 1 diff --git a/src/main/ssh/ssh-relay-manifest-signature.ts b/src/main/ssh/ssh-relay-manifest-signature.ts index f6131d6167b..d65ec364a24 100644 --- a/src/main/ssh/ssh-relay-manifest-signature.ts +++ b/src/main/ssh/ssh-relay-manifest-signature.ts @@ -21,6 +21,27 @@ export type SshRelayManifestAcceptedKey = { publicKey: Uint8Array } +type DeepReadonly = T extends readonly (infer Item)[] + ? readonly DeepReadonly[] + : T extends object + ? { readonly [Key in keyof T]: DeepReadonly } + : T + +declare const verifiedSshRelayManifest: unique symbol + +export type VerifiedSshRelayArtifactManifest = DeepReadonly & { + readonly [verifiedSshRelayManifest]: true +} + +function deepFreezeManifest(value: T): DeepReadonly { + for (const nested of Object.values(value)) { + if (nested && typeof nested === 'object' && !Object.isFrozen(nested)) { + deepFreezeManifest(nested) + } + } + return Object.freeze(value) as DeepReadonly +} + function compareAscii(left: string, right: string): number { return left < right ? -1 : left > right ? 1 : 0 } @@ -173,7 +194,7 @@ function acceptedKeyMap(keys: readonly SshRelayManifestAcceptedKey[]): Map Date: Wed, 15 Jul 2026 15:02:36 -0400 Subject: [PATCH 150/267] docs: checkpoint relay offline artifact selection Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 26 +++-- ...github-release-implementation-checklist.md | 108 +++++++++++++++--- 2 files changed, 105 insertions(+), 29 deletions(-) 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 2dc16010375..f24739ef940 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 @@ -10,18 +10,17 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 5 / Work Package 4 offline artifact selection — -in progress, 2026-07-15, Codex implementation owner.** Exact-head run -[29440806947](https://github.com/stablyai/orca/actions/runs/29440806947) at `8c925c249` passes the -10-test draft-release composition contract and full runtime construction/execution on all six -target-native Node 24 jobs under `E-M4-DRAFT-RELEASE-COMPOSITION-CI-001`. PR Checks and Golden E2E -are green; the artifact workflow is red only for the retained Windows ARM build-26200-versus-26100 -floor mismatch after successful runtime smoke. A real draft release write/read-back remains blocked -because it is not PR-contained and the production signer/environment are unprovisioned; the safe -default is no write. Next, begin the purpose-named offline tuple/content/immutable-URL selector RED -without download, cache, desktop consumer, or mode wiring. Production keys/environment/seed, -publication, desktop/default consumers, SSH transfer/install, tuple enablement, and merge to `main` -remain disconnected. Current pushed branch head is `8c925c249`; the implementation owner is authorized to drive -commits, pushes, CI, and PR-contained rehearsals end to end without merging. +local GREEN; exact-head native CI pending, 2026-07-15, Codex implementation owner.** The selector +now accepts only a signature-verified recursively immutable manifest and returns its exact offline +tuple, content ID, archive metadata, and tag-qualified direct URL. Focused 27-test, adjacent 91-test, +broad 307-test, typecheck, lint, formatting, and scope gates pass under +`E-M5-OFFLINE-SELECTION-LOCAL-RED-001` and `E-M5-OFFLINE-SELECTION-LOCAL-001`. Next, commit and push +this isolated package with the pending Milestone 4 evidence checkpoint, then require all six native +Node 24 jobs, PR Checks, and Golden E2E at one exact head. Download, cache, desktop consumers, SSH +transfer/install, mode wiring, tuple enablement, publication, production keys/environment/seed, and +merge to `main` remain disconnected. Current pushed branch head is `8c925c249`; the implementation +owner is authorized to drive commits, pushes, CI, and PR-contained rehearsals end to end without +merging. ## Safety status @@ -296,6 +295,9 @@ commits, pushes, CI, and PR-contained rehearsals end to end without merging. ### WP4 — Desktop resolver and verified cache - [ ] Select tuples offline from the embedded manifest and resolve immutable direct asset URLs. + The verified immutable selection and URL boundary is locally green under + `E-M5-OFFLINE-SELECTION-LOCAL-001`; exact-head all-six native CI and the actual packaged + embedded-manifest loader remain open. - [ ] Stream bounded downloads; verify signature, size, archive hash, and extracted tree. - [ ] Add exclusive staging, atomic publication, quarantine, locking, and the 2 GiB cache policy. - [ ] Prove verified cached bytes can be transferred while the client is offline. 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 de8661d346b..ccfc4a9d7b9 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    -Current phase: Milestone 5 / Work Package 4 offline artifact selection — **In progress — 2026-07-15, Codex implementation owner**. Milestone 4 disconnected upload/recovery/materialization/execution composition is closed locally and on all six exact-head native Node 24 jobs under E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-RED-001, E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001, and E-M4-DRAFT-RELEASE-COMPOSITION-CI-001. A real draft release write/read-back is not PR-contained and remains blocked by the unprovisioned production signer/environment; safe default is no write. Begin only offline tuple/content/immutable-URL selection contracts; do not add download, cache, desktop consumer, SSH transfer, tuple enablement, mode wiring, or default behavior.
    +Current phase: Milestone 5 / Work Package 4 offline artifact selection — **Local GREEN; exact-head native CI pending — 2026-07-15, Codex implementation owner**. The signature-verified immutable-manifest boundary and exact offline tuple/content/archive/URL result pass local focused, adjacent, broad, static, and scope gates under E-M5-OFFLINE-SELECTION-LOCAL-RED-001 and E-M5-OFFLINE-SELECTION-LOCAL-001. No networking, cache, desktop consumer, SSH transfer, tuple enablement, mode wiring, or default behavior is connected. Commit and push the isolated package with the pending Milestone 4 evidence checkpoint, then require all-six exact-head native Node 24 CI before starting download/cache work.
    Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `8c925c24990ac93f6d2dd8de2081a50a74f30d97` passes the 10-test draft-release composition suite and full runtime construction/execution on all six target-native jobs in run 29440806947 under E-M4-DRAFT-RELEASE-COMPOSITION-CI-001. PR Checks 29440804969 and both Golden E2E jobs in 29440804978 are green. Both Linux supplements and Windows x64 floor pass. Windows arm64 floor smoke passes in 6,178.8338ms with 49,881,088-byte RSS; the floor gate then correctly rejects hosted build 26200 versus required 26100. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -1035,9 +1035,13 @@ Suggested concrete modules: - [ ] Prefer `getconf GNU_LIBC_VERSION` when present; parse only output following Orca’s marker. - [ ] Detect musl through marked `ldd --version` output and known loader paths without treating arbitrary startup text as evidence. -- [ ] Validate required libc/libstdc++/kernel/OS versions against manifest constraints. -- [ ] Treat absent, ambiguous, conflicting, or unparseable results as unknown and choose proven +- [x] Validate required libc/libstdc++/kernel/OS versions against manifest constraints. + E-M5-OFFLINE-SELECTION-LOCAL-001 validates Linux kernel/glibc/libstdc++/GLIBCXX, macOS, and + Windows build/OpenSSH/PowerShell/.NET exact minima from a signature-verified manifest. +- [x] Treat absent, ambiguous, conflicting, or unparseable results as unknown and choose proven legacy behavior in `auto` mode. + E-M5-OFFLINE-SELECTION-LOCAL-001 pins classified unavailable results without connecting a + consumer or changing the legacy default. - [ ] Cover GNU coreutils, BusyBox, Alpine, containers, old glibc, Rosetta, Windows, macOS, startup noise, missing commands, localization, cancellation, and `MaxSessions=1`. @@ -1046,8 +1050,12 @@ Suggested concrete modules: - [ ] Load the embedded manifest for official builds and verify its signature before trusting fields. - [ ] Verify the manifest-authenticated target-native attestation and exact byte hashes portably; do not assume a macOS client can run Authenticode tooling or a Windows client can run `codesign`. -- [ ] Resolve the expected tuple and content ID while fully offline. -- [ ] Construct the exact `/releases/download//` URL without an API lookup. +- [x] Resolve the expected tuple and content ID while fully offline. + E-M5-OFFLINE-SELECTION-LOCAL-001 accepts only the branded immutable result of accepted-key + signature verification and returns its exact tuple/content/archive identity. +- [x] Construct the exact `/releases/download//` URL without an API lookup. + E-M5-OFFLINE-SELECTION-LOCAL-001 derives the tag-qualified direct URL from authenticated + fields and proves that no `latest` lookup is present. - [ ] Reject redirects that violate the approved GitHub release-asset origin policy. - [ ] Ensure authorization, cookies, proxy credentials, and custom headers do not leak across redirect origins. @@ -11904,6 +11912,72 @@ config/vitest.config.ts config/scripts/ssh-relay-runtime-draft-release-verificat - Follow-up: checkpoint this evidence. A real release write is not PR-contained and remains blocked; retain no-write as the safe default and begin the independent Milestone 5 offline selector RED. +### E-M5-OFFLINE-SELECTION-LOCAL-RED-001 — Verified offline selection boundary was incomplete + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted RED atop local evidence commit `2097a40dd`; exact pushed base + `8c925c24990ac93f6d2dd8de2081a50a74f30d97`; draft PR #8741. Local runner is macOS 26.2 build + 25C56 arm64, Node v26.0.0, and pnpm 10.24.0. No network request or external write occurred. +- Command: `/usr/bin/time -l pnpm exec vitest run src/main/ssh/ssh-relay-artifact-selector.test.ts + src/main/ssh/ssh-relay-manifest-signature.test.ts` — expected FAIL, 2 files / 26 tests, 7 failed + and 19 passed in 430ms Vitest / 1.31s wall, maximum RSS 136,265,728 bytes, zero swaps. +- Contract failures: a compatible selection returned only tuple ID/tuple and omitted the exact + content ID, release tag, authenticated archive metadata, and direct release URL. A verified + manifest remained mutable after signature acceptance. Requiring verification also rejected five + old Windows/macOS selector shortcuts because they did not represent schema-consistent native + closures; these were test-fixture defects, not product compatibility failures. +- Contract pinned by the RED: accept only the signature verifier's branded result; make that parsed + result recursively immutable; select an exact tuple/content/archive from authenticated fields; + construct only the exact tag-qualified GitHub release URL; retain classified unavailable results + for unknown/incompatible evidence. Use complete internally consistent signed Windows and macOS + fixtures rather than bypassing the schema. +- Does not prove: implementation, GREEN, production keys, embedded desktop loading, networking, + redirect policy, download/cache/extraction, SSH behavior, live hosts, performance, or any enabled + tuple. + +### E-M5-OFFLINE-SELECTION-LOCAL-001 — Verified immutable offline selection passes locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted implementation atop local evidence commit `2097a40dd`; exact pushed base + `8c925c24990ac93f6d2dd8de2081a50a74f30d97`; draft PR #8741. Local runner is macOS 26.2 build + 25C56 arm64, Node v26.0.0, and pnpm 10.24.0. All accepted keys and manifests are deterministic + test fixtures; no GitHub request or external write occurs. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts + --maxWorkers=1 src/main/ssh/ssh-relay-artifact-selector.test.ts + src/main/ssh/ssh-relay-manifest-signature.test.ts` — PASS, 2 files / 27 tests in 1.42s Vitest / + 3.04s wall; maximum RSS 131,923,968 bytes and zero swaps. +- Adjacent command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts + --maxWorkers=1` with artifact schema, signature, release asset, selector, runtime identity, manifest + aggregate, aggregate command, and signing-handoff suites — PASS, 8 files / 91 tests in 3.60s + Vitest / 4.47s wall; maximum RSS 189,333,504 bytes and zero swaps. +- Broad command: `/usr/bin/time -l sh -c "rg --files config/scripts | rg + '/ssh-relay.*[.]test[.]mjs$' | xargs pnpm exec vitest run --config config/vitest.config.ts + --maxWorkers=1"` — PASS, 56 files / 307 tests in 16.83s Vitest / 17.93s wall; maximum RSS + 189,857,792 bytes and zero swaps. +- Static gates: `/usr/bin/time -l pnpm typecheck` passes in 2.46s wall with 1,252,245,504-byte + maximum RSS. `/usr/bin/time -l pnpm lint` passes in 26.98s wall with 2,021,146,624-byte maximum + RSS; all 41 reliability gates and the 355-entry max-lines ratchet pass with the existing 26 + unrelated warnings. Focused `oxfmt --check`, localization checks, `git diff --check`, and protected + resolver/required-asset zero-diff checks pass. +- Oracle proved: only `verifySshRelayArtifactManifest` can construct the branded selector input; + accepted-key signature verification returns a recursively frozen parsed manifest. Selection uses + authenticated build tag, exact tuple/content ID, archive size/hash/count identity, and constructs + `https://github.com/stablyai/orca/releases/download//` without `latest`, API, or + freshness input. Exact Linux, Windows, and macOS minima plus classified unknown/incompatible/ + translated/ambiguous behavior remain pinned. Complete schema-valid signed native fixtures replace + the old selector-only shortcuts. +- Consumer-disconnection oracle: no embedded-manifest loader, accepted production key, Electron + networking, redirect, download/cache/extraction, desktop consumer, SSH transfer/install, mode, + tuple enablement, or default behavior is added. Legacy remains the only production behavior. +- Does not prove: Node 24/native-runner portability, real production manifest keys or packaged + embedding, direct-URL network behavior, proxy/certificate/redirect safety, download/cache, live + SSH hosts, performance against legacy, or any enabled tuple. +- Follow-up: commit and push this isolated package together with evidence commit `2097a40dd`, then + require focused and adjacent contracts on all six exact-head native Node 24 jobs plus normal PR + checks before starting the disconnected download boundary. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -11962,18 +12036,18 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Begin a purpose-named Milestone 5 offline artifact-selection RED. It must accept only an already- -verified immutable embedded manifest, map the existing detected relay OS/architecture plus explicit -libc classification to one exact tuple/content ID, and construct the exact immutable -`/releases/download//` URL without an API/latest/freshness lookup. Unknown, ambiguous, -unsupported, musl, or inconsistent inputs must return a classified unavailable result suitable for -the still-disconnected legacy path; manifest/identity drift must fail closed. Do not add signature -key policy, networking, redirect handling, download/cache, desktop call sites, SSH transfer/install, -mode wiring, tuple enablement, or default behavior in this package. Keep `ORCA_RELAY_PATH`, existing -desktop required-assets behavior, and detached-signature byte encoding unchanged. -Keep Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, -publication, and every tuple separately gated. Do not merge to `main`; retain every -production/default gate. +Commit and push the isolated Milestone 5 offline-selection package together with local evidence +commit `2097a40dd`. Require the focused selector/signature contract and adjacent manifest consumers +to pass on Linux x64/arm64, macOS x64/arm64, and Windows x64/arm64 at the exact pushed head, plus +normal PR Checks and Golden E2E. Record job IDs, runner images, timings, results, and the separately +gated Windows arm64 floor outcome before closing the package. + +Only after that evidence is complete, begin a purpose-named disconnected download-boundary RED for +bounded streaming, cancellation, archive hash/size, approved GitHub redirect origins, and credential/ +header stripping. Do not add a desktop call site, cache publication, extraction, SSH transfer/install, +mode wiring, tuple enablement, or default behavior. Keep `ORCA_RELAY_PATH`, existing desktop required- +assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux +kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. 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 From d1dda17c87795ccd8b9c1450cd50972896f03353 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 15:06:41 -0400 Subject: [PATCH 151/267] ci(ssh): run relay selector tests natively Co-authored-by: Orca --- .github/workflows/ssh-relay-runtime-artifacts.yml | 10 ++++++++++ config/scripts/ssh-relay-runtime-workflow.test.mjs | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 018014e4ee3..e7f4e39eb40 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -257,6 +257,11 @@ jobs: node --check config/scripts/ssh-relay-runtime-native-signing-rehearsal-workflow.test.mjs node --check config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs pnpm exec vitest run --config config/vitest.config.ts \ + 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 \ + src/main/ssh/ssh-relay-artifact-selector.test.ts \ + src/main/ssh/ssh-relay-runtime-identity.test.ts \ config/scripts/ssh-relay-runtime-compatibility.test.mjs \ config/scripts/ssh-relay-node-release-verification.test.mjs \ config/scripts/ssh-relay-node-tar-inspection.test.mjs \ @@ -754,6 +759,11 @@ jobs: node --check config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime build prerequisite test syntax check failed' } pnpm exec vitest run --config config/vitest.config.ts ` + 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 ` + src/main/ssh/ssh-relay-artifact-selector.test.ts ` + src/main/ssh/ssh-relay-runtime-identity.test.ts ` config/scripts/ssh-relay-runtime-compatibility.test.mjs ` config/scripts/ssh-relay-node-release-verification.test.mjs ` config/scripts/ssh-relay-node-tar-inspection.test.mjs ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index fb28af32c32..8dff5cdeee6 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -99,6 +99,17 @@ describe('SSH relay runtime artifact workflow', () => { expect(uploadIndex).toBeGreaterThan(buildIndex) expect(source).toContain('verify-ssh-relay-runtime.mjs') expect(source).toContain('ssh-relay-runtime-workflow.test.mjs') + for (const testName of [ + 'ssh-relay-artifact-schema.test.ts', + 'ssh-relay-manifest-signature.test.ts', + 'ssh-relay-release-asset.test.ts', + 'ssh-relay-artifact-selector.test.ts', + 'ssh-relay-runtime-identity.test.ts' + ]) { + // Why: portable desktop selection contracts need proof on every native runner family, not + // only the local client architecture or the generic Linux PR job. + expect(source.split(`src/main/ssh/${testName}`)).toHaveLength(3) + } expect(source).toContain('pnpm install --frozen-lockfile --ignore-scripts') expect(source).toContain('--connect-timeout 20 --max-time 300 --retry 2') expect(source).toContain('mkdir -p "$output_root"') From 3be4e68679f6f235165c8784eca61576dc2fd828 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 15:06:45 -0400 Subject: [PATCH 152/267] docs: record relay selector CI wiring gate Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 28 +++++----- ...github-release-implementation-checklist.md | 55 +++++++++++++++++-- 2 files changed, 64 insertions(+), 19 deletions(-) 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 f24739ef940..691787705f1 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 @@ -10,17 +10,17 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 5 / Work Package 4 offline artifact selection — -local GREEN; exact-head native CI pending, 2026-07-15, Codex implementation owner.** The selector -now accepts only a signature-verified recursively immutable manifest and returns its exact offline -tuple, content ID, archive metadata, and tag-qualified direct URL. Focused 27-test, adjacent 91-test, -broad 307-test, typecheck, lint, formatting, and scope gates pass under -`E-M5-OFFLINE-SELECTION-LOCAL-RED-001` and `E-M5-OFFLINE-SELECTION-LOCAL-001`. Next, commit and push -this isolated package with the pending Milestone 4 evidence checkpoint, then require all six native -Node 24 jobs, PR Checks, and Golden E2E at one exact head. Download, cache, desktop consumers, SSH -transfer/install, mode wiring, tuple enablement, publication, production keys/environment/seed, and -merge to `main` remain disconnected. Current pushed branch head is `8c925c249`; the implementation -owner is authorized to drive commits, pushes, CI, and PR-contained rehearsals end to end without -merging. +native CI wiring correction locally GREEN; replacement exact-head CI pending, 2026-07-15, Codex +implementation owner.** The verified immutable selector is pushed at `5ae2a505c`, but a post-push +audit proved the native artifact jobs did not invoke its portable source suites. The workflow now +pins artifact schema, signature, release asset, selector, and runtime identity tests in both POSIX +and Windows job families under `E-M5-OFFLINE-SELECTION-CI-WIRING-RED-001` and +`E-M5-OFFLINE-SELECTION-CI-WIRING-LOCAL-001`. Runs started for `5ae2a505c` are intermediate only. +Next, push this test-wiring correction and require replacement all-six native Node 24 jobs, PR +Checks, and Golden E2E at one exact head. Download, cache, desktop consumers, SSH transfer/install, +mode wiring, tuple enablement, publication, production keys/environment/seed, and merge to `main` +remain disconnected. The implementation owner is authorized to drive commits, pushes, CI, and +PR-contained rehearsals end to end without merging. ## Safety status @@ -296,8 +296,10 @@ merging. - [ ] Select tuples offline from the embedded manifest and resolve immutable direct asset URLs. The verified immutable selection and URL boundary is locally green under - `E-M5-OFFLINE-SELECTION-LOCAL-001`; exact-head all-six native CI and the actual packaged - embedded-manifest loader remain open. + `E-M5-OFFLINE-SELECTION-LOCAL-001`. The first post-push audit found that native jobs omitted + these source suites; both job families are now locally pinned under + `E-M5-OFFLINE-SELECTION-CI-WIRING-LOCAL-001`. Replacement exact-head all-six native CI and the + actual packaged embedded-manifest loader remain open. - [ ] Stream bounded downloads; verify signature, size, archive hash, and extracted tree. - [ ] Add exclusive staging, atomic publication, quarantine, locking, and the 2 GiB cache policy. - [ ] Prove verified cached bytes can be transferred while the client is offline. 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 ccfc4a9d7b9..83795a4d3e2 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    -Current phase: Milestone 5 / Work Package 4 offline artifact selection — **Local GREEN; exact-head native CI pending — 2026-07-15, Codex implementation owner**. The signature-verified immutable-manifest boundary and exact offline tuple/content/archive/URL result pass local focused, adjacent, broad, static, and scope gates under E-M5-OFFLINE-SELECTION-LOCAL-RED-001 and E-M5-OFFLINE-SELECTION-LOCAL-001. No networking, cache, desktop consumer, SSH transfer, tuple enablement, mode wiring, or default behavior is connected. Commit and push the isolated package with the pending Milestone 4 evidence checkpoint, then require all-six exact-head native Node 24 CI before starting download/cache work.
    +Current phase: Milestone 5 / Work Package 4 offline artifact selection — **Native CI wiring correction locally GREEN; replacement exact-head CI pending — 2026-07-15, Codex implementation owner**. The selector implementation is pushed at `5ae2a505c`, but audit E-M5-OFFLINE-SELECTION-CI-WIRING-RED-001 proved the native workflow did not invoke its portable source suites. E-M5-OFFLINE-SELECTION-CI-WIRING-LOCAL-001 now pins all five artifact/manifest/selector suites in both POSIX and Windows job families. Commit and push the correction, then require replacement all-six exact-head Node 24 evidence; runs already started for `5ae2a505c` are intermediate only. No networking, cache, desktop consumer, SSH transfer, tuple enablement, mode wiring, or default behavior is connected.
    Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `8c925c24990ac93f6d2dd8de2081a50a74f30d97` passes the 10-test draft-release composition suite and full runtime construction/execution on all six target-native jobs in run 29440806947 under E-M4-DRAFT-RELEASE-COMPOSITION-CI-001. PR Checks 29440804969 and both Golden E2E jobs in 29440804978 are green. Both Linux supplements and Windows x64 floor pass. Windows arm64 floor smoke passes in 6,178.8338ms with 49,881,088-byte RSS; the floor gate then correctly rejects hosted build 26200 versus required 26100. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -11978,6 +11978,49 @@ config/vitest.config.ts config/scripts/ssh-relay-runtime-draft-release-verificat require focused and adjacent contracts on all six exact-head native Node 24 jobs plus normal PR checks before starting the disconnected download boundary. +### E-M5-OFFLINE-SELECTION-CI-WIRING-RED-001 — Native jobs omitted desktop selection contracts + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: workflow audit immediately after pushed selector head + `5ae2a505cbb0d9744ebd4232cfc131747fdead54`; draft PR #8741. Runs 29443022601, + 29443022528, and 29443022549 had started but cannot close the selector package because the native + artifact workflow lacked its purpose-named source suites. +- Audit signal: `rg -n "artifact-selector|manifest-signature|artifact-schema|release-asset|vitest.*src/main/ssh" + .github/workflows config/scripts` returned no selector/source-suite invocation from + `.github/workflows/ssh-relay-runtime-artifacts.yml`. +- Purpose-named RED command after adding only the workflow assertion: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 + config/scripts/ssh-relay-runtime-workflow.test.mjs` — expected FAIL, 1 failed / 6 passed of 7 in + 473ms Vitest / 2.07s wall, maximum RSS 131,694,592 bytes, zero swaps. The workflow contained zero + occurrences where two job-family invocations were required. +- Contract pinned by the RED: artifact schema, signature, release asset, selector, and runtime + identity source suites must be explicit inputs to both the POSIX and Windows native test steps. + Green workflow status without these invocations is not selector portability evidence. +- Does not prove: corrected wiring, execution on any native runner, product behavior, or any tuple. + +### E-M5-OFFLINE-SELECTION-CI-WIRING-LOCAL-001 — Both native job families invoke source suites + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted workflow correction atop pushed head + `5ae2a505cbb0d9744ebd4232cfc131747fdead54`; draft PR #8741. +- Command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1` + with the workflow test plus artifact schema, signature, release asset, selector, and runtime + identity suites — PASS, 6 files / 81 tests in 3.97s Vitest / 6.00s wall; maximum RSS 132,087,808 + bytes and zero swaps. Focused YAML/test `oxfmt --check` and `git diff --check` pass. +- Oracle proved: the workflow source names all five portable source suites exactly once in the + four-runner POSIX job family and exactly once in the two-runner Windows job family. The YAML parses, + runner labels remain unchanged, and the existing credential/publication prohibitions remain + covered by the same workflow suite. +- Consumer-disconnection oracle: this changes test execution only. It adds no credential, release + write, desktop consumer, network/download/cache path, SSH behavior, tuple enablement, or default + behavior. +- Does not prove: actual Linux/macOS/Windows x64/arm64 execution. The `5ae2a505c` runs are + intermediate only; replacement exact-head CI after this correction is mandatory. +- Follow-up: commit and push the workflow/test/evidence correction, then collect replacement all-six + native Node 24 jobs plus PR Checks and Golden E2E. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -12036,11 +12079,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the isolated Milestone 5 offline-selection package together with local evidence -commit `2097a40dd`. Require the focused selector/signature contract and adjacent manifest consumers -to pass on Linux x64/arm64, macOS x64/arm64, and Windows x64/arm64 at the exact pushed head, plus -normal PR Checks and Golden E2E. Record job IDs, runner images, timings, results, and the separately -gated Windows arm64 floor outcome before closing the package. +Commit and push the native CI wiring correction and its evidence checkpoint. Require the five +portable artifact/manifest/selector suites to pass inside the Linux x64/arm64, macOS x64/arm64, and +Windows x64/arm64 artifact jobs at the replacement exact head, plus normal PR Checks and Golden E2E. +Runs for `5ae2a505c` are intermediate only. Record replacement job IDs, runner images, timings, +results, and the separately gated Windows arm64 floor outcome before closing the package. Only after that evidence is complete, begin a purpose-named disconnected download-boundary RED for bounded streaming, cancellation, archive hash/size, approved GitHub redirect origins, and credential/ From 1e3356cc6fa4e4090935184879b5a77db94ad2e5 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 15:27:22 -0400 Subject: [PATCH 153/267] docs: record relay selector native CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 28 ++++----- ...github-release-implementation-checklist.md | 61 ++++++++++++++++--- 2 files changed, 66 insertions(+), 23 deletions(-) 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 691787705f1..83bc0cc61c4 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 @@ -9,18 +9,18 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 offline artifact selection — -native CI wiring correction locally GREEN; replacement exact-head CI pending, 2026-07-15, Codex -implementation owner.** The verified immutable selector is pushed at `5ae2a505c`, but a post-push -audit proved the native artifact jobs did not invoke its portable source suites. The workflow now -pins artifact schema, signature, release asset, selector, and runtime identity tests in both POSIX -and Windows job families under `E-M5-OFFLINE-SELECTION-CI-WIRING-RED-001` and -`E-M5-OFFLINE-SELECTION-CI-WIRING-LOCAL-001`. Runs started for `5ae2a505c` are intermediate only. -Next, push this test-wiring correction and require replacement all-six native Node 24 jobs, PR -Checks, and Golden E2E at one exact head. Download, cache, desktop consumers, SSH transfer/install, -mode wiring, tuple enablement, publication, production keys/environment/seed, and merge to `main` -remain disconnected. The implementation owner is authorized to drive commits, pushes, CI, and -PR-contained rehearsals end to end without merging. +Active checkpoint: **Milestone 5 / Work Package 4 desktop download boundary — offline selection +closed; begin disconnected bounded-download RED, 2026-07-15, Codex implementation owner.** Exact +head `3be4e6867` passes verified immutable tuple/content/archive/URL selection and full runtime +construction on all six native Node 24 jobs in +[run 29443291349](https://github.com/stablyai/orca/actions/runs/29443291349) under +`E-M5-OFFLINE-SELECTION-CI-001`. PR Checks and both Golden E2E jobs are green. The artifact run is +red only after Windows arm64 successfully smokes the runtime and rejects hosted build 26200 against +the retained 26100 floor. Next, implement only a relay-specific bounded Electron download contract; +cache publication/extraction, desktop consumers, SSH transfer/install, mode wiring, tuple enablement, +publication, production keys/environment/seed, and merge to `main` remain disconnected. The +implementation owner is authorized to drive commits, pushes, CI, and PR-contained rehearsals end to +end without merging. ## Safety status @@ -298,8 +298,8 @@ PR-contained rehearsals end to end without merging. The verified immutable selection and URL boundary is locally green under `E-M5-OFFLINE-SELECTION-LOCAL-001`. The first post-push audit found that native jobs omitted these source suites; both job families are now locally pinned under - `E-M5-OFFLINE-SELECTION-CI-WIRING-LOCAL-001`. Replacement exact-head all-six native CI and the - actual packaged embedded-manifest loader remain open. + `E-M5-OFFLINE-SELECTION-CI-WIRING-LOCAL-001`, and exact-head all-six native proof is closed by + `E-M5-OFFLINE-SELECTION-CI-001`. The actual packaged embedded-manifest loader remains open. - [ ] Stream bounded downloads; verify signature, size, archive hash, and extracted tree. - [ ] Add exclusive staging, atomic publication, quarantine, locking, and the 2 GiB cache policy. - [ ] Prove verified cached bytes can be transferred while the client is offline. 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 83795a4d3e2..bf2af1eefae 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 5 / Work Package 4 offline artifact selection — **Native CI wiring correction locally GREEN; replacement exact-head CI pending — 2026-07-15, Codex implementation owner**. The selector implementation is pushed at `5ae2a505c`, but audit E-M5-OFFLINE-SELECTION-CI-WIRING-RED-001 proved the native workflow did not invoke its portable source suites. E-M5-OFFLINE-SELECTION-CI-WIRING-LOCAL-001 now pins all five artifact/manifest/selector suites in both POSIX and Windows job families. Commit and push the correction, then require replacement all-six exact-head Node 24 evidence; runs already started for `5ae2a505c` are intermediate only. No networking, cache, desktop consumer, SSH transfer, tuple enablement, mode wiring, or default behavior is connected.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `8c925c24990ac93f6d2dd8de2081a50a74f30d97` passes the 10-test draft-release composition suite and full runtime construction/execution on all six target-native jobs in run 29440806947 under E-M4-DRAFT-RELEASE-COMPOSITION-CI-001. PR Checks 29440804969 and both Golden E2E jobs in 29440804978 are green. Both Linux supplements and Windows x64 floor pass. Windows arm64 floor smoke passes in 6,178.8338ms with 49,881,088-byte RSS; the floor gate then correctly rejects hosted build 26200 versus required 26100. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop download boundary — **Offline selection closed; begin disconnected bounded-download RED — 2026-07-15, Codex implementation owner**. The verified immutable offline tuple/content/archive/URL selector is closed locally and on all six exact-head native Node 24 jobs under E-M5-OFFLINE-SELECTION-LOCAL-001 and E-M5-OFFLINE-SELECTION-CI-001. Begin only relay-specific Electron download contracts for bounded streaming, cancellation, exact size/hash, approved GitHub redirects, and credential/header stripping. Do not add cache publication/extraction, desktop call sites, SSH transfer/install, tuple enablement, mode wiring, or default behavior.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `3be4e68679f6f235165c8784eca61576dc2fd828` passes the portable artifact/manifest/selector suites and full runtime construction/execution on all six target-native jobs in run 29443291349 under E-M5-OFFLINE-SELECTION-CI-001. PR Checks 29443290994 and both Golden E2E jobs in 29443291091 are green. Both Linux supplements and Windows x64 floor pass. Windows arm64 floor smoke passes in 5,982.393ms with 48,451,584-byte RSS; the floor gate then correctly rejects hosted build 26200 versus required 26100. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -12021,6 +12021,55 @@ config/vitest.config.ts config/scripts/ssh-relay-runtime-draft-release-verificat - Follow-up: commit and push the workflow/test/evidence correction, then collect replacement all-six native Node 24 jobs plus PR Checks and Golden E2E. +### E-M5-OFFLINE-SELECTION-CI-001 — Verified offline selection passes all six native Node 24 jobs + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `3be4e68679f6f235165c8784eca61576dc2fd828`; selector + implementation commit `7b8c7669a`, native test wiring commit `d1dda17c8`; draft PR #8741. +- Workflow: [SSH Relay Runtime Artifacts run 29443291349](https://github.com/stablyai/orca/actions/runs/29443291349), + 19:07:08Z–19:24:08Z. The aggregate conclusion is the expected failure only because the separate + Windows arm64 floor job rejects hosted build 26200 after successful runtime verification/smoke. +- Native jobs and combined portable/release contract suites: + - Linux x64 job 87447445800, runner label/image `ubuntu-24.04`, image version + `20260705.232.1`, PASS in 4m06s; 60 files / 377 tests in 9.63s. + - Linux arm64 job 87447445787, `ubuntu-24.04-arm`, image version `20260706.52.2`, PASS in + 4m19s; 60 files / 377 tests in 7.32s. + - Darwin x64 job 87447445754, runner label `macos-15-intel`, image `macos-15` version + `20260629.0276.1`, PASS in 7m16s; 60 files / 377 tests in 32.57s. + - Darwin arm64 job 87447445790, runner label `macos-15`, image `macos-15-arm64` version + `20260706.0213.1`, PASS in 3m32s; 60 files / 377 tests in 14.35s. + - Windows x64 job 87447445786, `windows-2022`, image version `20260714.244.1`, PASS in + 6m47s; 61 files / 371 passed + 10 platform skips of 381 in 22.26s. + - Windows arm64 job 87447445745, `windows-11-arm`, image version `20260714.109.1`, PASS in + 10m48s; 61 files / 371 passed + 10 platform skips of 381 in 18.67s. +- Downstream/adjacent evidence: Linux x64 supplement job 87449112159 and Linux arm64 supplement + job 87449112031 pass. Windows x64 floor job 87449773555 passes. Windows arm64 floor job + 87449773588 uses image `windows-11-arm64` `20260706.102.1`; the 60-entry/42-file/85,213,511-byte + runtime passes Node v24.18.0, PTY, watcher, resource-settlement, full-tree verification, and smoke + in 5,982.393ms with 48,451,584-byte RSS; the complete verifier takes 8,030.9838ms before the gate + rejects observed build 26200 versus required build 26100. +- PR Checks [run 29443290994](https://github.com/stablyai/orca/actions/runs/29443290994), job + 87447364904, passes in 10m10s. Golden E2E + [run 29443291091](https://github.com/stablyai/orca/actions/runs/29443291091) passes Linux job + 87447365115 in 4m27s and macOS job 87447365196 in 5m38s. +- Oracle proved: artifact schema, accepted-key signature verification/recursive immutability, exact + release-asset identity, offline compatibility selection, and runtime identity contracts execute + consistently under Node 24 on Linux, macOS, and Windows x64/arm64. Every job returns the exact + authenticated tuple/content/archive/tag-qualified URL or a classified unavailable result, without + API/latest/freshness input. Full artifact build, native smoke, equality, and adjacent product gates + remain green. +- Consumer-disconnection oracle: there is no production accepted key, embedded-manifest loader, + Electron network call, redirect/download/cache/extraction, desktop consumer, SSH transfer/install, + mode, tuple enablement, or default behavior. No runtime artifact is published. Legacy remains the + only production path. +- Does not prove: production manifest keys/packaged embedding, live direct-URL/redirect/proxy/ + certificate behavior, download/cache, oldest Windows arm64/macOS/Linux-kernel floors, native + signing/trust, live SSH hosts, performance against legacy, or any enabled tuple. +- Follow-up: checkpoint this evidence, update PR/worktree status, then begin only the disconnected + bounded relay-download RED. Preserve every cache, consumer, SSH, tuple, publication, and default + disconnection. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -12079,13 +12128,7 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the native CI wiring correction and its evidence checkpoint. Require the five -portable artifact/manifest/selector suites to pass inside the Linux x64/arm64, macOS x64/arm64, and -Windows x64/arm64 artifact jobs at the replacement exact head, plus normal PR Checks and Golden E2E. -Runs for `5ae2a505c` are intermediate only. Record replacement job IDs, runner images, timings, -results, and the separately gated Windows arm64 floor outcome before closing the package. - -Only after that evidence is complete, begin a purpose-named disconnected download-boundary RED for +Begin a purpose-named disconnected download-boundary RED for bounded streaming, cancellation, archive hash/size, approved GitHub redirect origins, and credential/ header stripping. Do not add a desktop call site, cache publication, extraction, SSH transfer/install, mode wiring, tuple enablement, or default behavior. Keep `ORCA_RELAY_PATH`, existing desktop required- From ef9681d3258d4902cb99845d084048e38ddeca81 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 15:40:00 -0400 Subject: [PATCH 154/267] feat(ssh): add verified relay artifact downloader Co-authored-by: Orca --- .../ssh/ssh-relay-artifact-download.test.ts | 240 ++++++++++++++++++ src/main/ssh/ssh-relay-artifact-download.ts | 187 ++++++++++++++ src/main/ssh/ssh-relay-artifact-selector.ts | 2 + 3 files changed, 429 insertions(+) create mode 100644 src/main/ssh/ssh-relay-artifact-download.test.ts create mode 100644 src/main/ssh/ssh-relay-artifact-download.ts diff --git a/src/main/ssh/ssh-relay-artifact-download.test.ts b/src/main/ssh/ssh-relay-artifact-download.test.ts new file mode 100644 index 00000000000..4c7b70567ec --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-download.test.ts @@ -0,0 +1,240 @@ +import { createHash } from 'node:crypto' +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import nacl from 'tweetnacl' +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { downloadSshRelayArtifact } from './ssh-relay-artifact-download' +import { createSshRelayArtifactTestManifest } from './ssh-relay-artifact-test-manifest' +import { + selectSshRelayArtifact, + type SshRelaySelectedArtifact +} from './ssh-relay-artifact-selector' +import { + signSshRelayArtifactManifest, + sshRelayManifestKeyId, + verifySshRelayArtifactManifest +} from './ssh-relay-manifest-signature' + +const { netFetchMock } = vi.hoisted(() => ({ netFetchMock: vi.fn() })) + +vi.mock('electron', () => ({ net: { fetch: netFetchMock } })) + +const keyPair = nacl.sign.keyPair.fromSeed(Uint8Array.from({ length: 32 }, (_, index) => index)) +const bytes = Buffer.from('verified relay runtime bytes') +const sha256 = `sha256:${createHash('sha256').update(bytes).digest('hex')}` as const +const temporaryDirectories: string[] = [] + +function selectedArtifact(): SshRelaySelectedArtifact { + const manifest = createSshRelayArtifactTestManifest() + manifest.tuples[0].archive.size = bytes.length + manifest.tuples[0].archive.sha256 = sha256 + manifest.signatures = [signSshRelayArtifactManifest(manifest, keyPair.secretKey)] + const verified = verifySshRelayArtifactManifest(manifest, [ + { keyId: sshRelayManifestKeyId(keyPair.publicKey), publicKey: keyPair.publicKey } + ]) + const selected = selectSshRelayArtifact(verified, { + os: 'linux', + architecture: 'x64', + processTranslated: false, + kernelVersion: '6.8', + libc: { family: 'glibc', version: '2.39' }, + libstdcxxVersion: '6.0.33', + glibcxxVersion: '3.4.33' + }) + if (selected.kind !== 'selected') { + throw new Error('Expected compatible test artifact') + } + return selected +} + +async function destination(name = 'runtime.partial'): Promise { + const directory = await mkdtemp(join(tmpdir(), 'orca-relay-download-')) + temporaryDirectories.push(directory) + return join(directory, name) +} + +function exactResponse(body: BodyInit = bytes): Response { + return new Response(body, { status: 200, headers: { 'content-length': String(bytes.length) } }) +} + +afterEach(async () => { + netFetchMock.mockReset() + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })) + ) +}) + +describe('SSH relay artifact download', () => { + it('streams exact signed bytes into an exclusive temporary file', async () => { + const artifact = selectedArtifact() + const destinationPath = await destination() + netFetchMock.mockResolvedValueOnce(exactResponse()) + + await expect(downloadSshRelayArtifact({ artifact, destinationPath })).resolves.toEqual({ + destinationPath, + finalUrl: artifact.archive.downloadUrl, + size: bytes.length, + sha256 + }) + expect(await readFile(destinationPath)).toEqual(bytes) + expect(netFetchMock).toHaveBeenCalledWith(artifact.archive.downloadUrl, { + credentials: 'omit', + headers: { Accept: 'application/octet-stream' }, + redirect: 'manual', + signal: expect.any(AbortSignal) + }) + }) + + it('follows one approved GitHub asset redirect without forwarding sensitive headers', async () => { + const artifact = selectedArtifact() + const destinationPath = await destination() + const redirectedUrl = 'https://release-assets.githubusercontent.com/example/runtime?sig=secret' + netFetchMock + .mockResolvedValueOnce( + new Response(null, { status: 302, headers: { location: redirectedUrl } }) + ) + .mockResolvedValueOnce(exactResponse()) + + await downloadSshRelayArtifact({ artifact, destinationPath }) + + expect(netFetchMock).toHaveBeenCalledTimes(2) + expect(netFetchMock.mock.calls[1]).toEqual([ + redirectedUrl, + { + credentials: 'omit', + headers: { Accept: 'application/octet-stream' }, + redirect: 'manual', + signal: expect.any(AbortSignal) + } + ]) + }) + + it.each([ + ['http://release-assets.githubusercontent.com/runtime', /https/i], + ['https://example.com/runtime', /origin/i], + ['https://user:secret@release-assets.githubusercontent.com/runtime', /credentials/i], + ['https://release-assets.githubusercontent.com:444/runtime', /port/i], + [null, /location/i] + ])( + 'rejects an unsafe redirect location %s and removes partial output', + async (location, error) => { + const destinationPath = await destination() + netFetchMock.mockResolvedValueOnce( + new Response(null, { status: 302, headers: location ? { location } : undefined }) + ) + + await expect( + downloadSshRelayArtifact({ artifact: selectedArtifact(), destinationPath }) + ).rejects.toThrow(error) + await expect(readFile(destinationPath)).rejects.toMatchObject({ code: 'ENOENT' }) + } + ) + + it('rejects redirect chains and non-200 responses', async () => { + for (const responses of [ + [new Response(null, { status: 404 })], + [new Response(null, { status: 429 })], + [new Response(null, { status: 503 })], + [ + new Response(null, { + status: 302, + headers: { location: 'https://release-assets.githubusercontent.com/first' } + }), + new Response(null, { + status: 302, + headers: { location: 'https://release-assets.githubusercontent.com/second' } + }) + ] + ]) { + const destinationPath = await destination() + netFetchMock.mockReset() + netFetchMock.mockResolvedValueOnce(responses[0]) + if (responses[1]) { + netFetchMock.mockResolvedValueOnce(responses[1]) + } + + await expect( + downloadSshRelayArtifact({ artifact: selectedArtifact(), destinationPath }) + ).rejects.toThrow(/status|redirect/i) + await expect(readFile(destinationPath)).rejects.toMatchObject({ code: 'ENOENT' }) + } + }) + + it.each([ + [new Response(null, { status: 200 }), /body/i], + [new Response(bytes.subarray(1)), /size/i], + [new Response(Buffer.concat([bytes, bytes])), /size/i], + [new Response(Buffer.alloc(bytes.length, 1)), /sha-?256/i] + ])('rejects malformed or unverified response bytes', async (response, error) => { + const destinationPath = await destination() + netFetchMock.mockResolvedValueOnce(response) + + await expect( + downloadSshRelayArtifact({ artifact: selectedArtifact(), destinationPath }) + ).rejects.toThrow(error) + await expect(readFile(destinationPath)).rejects.toMatchObject({ code: 'ENOENT' }) + }) + + it('cancels a response whose declared length disagrees with the signed manifest', async () => { + const destinationPath = await destination() + const cancelled = vi.fn() + const body = new ReadableStream({ + start(stream) { + stream.enqueue(bytes) + }, + cancel: cancelled + }) + netFetchMock.mockResolvedValueOnce( + new Response(body, { headers: { 'content-length': String(bytes.length + 1) } }) + ) + + await expect( + downloadSshRelayArtifact({ artifact: selectedArtifact(), destinationPath }) + ).rejects.toThrow(/length/i) + expect(cancelled).toHaveBeenCalledOnce() + await expect(readFile(destinationPath)).rejects.toMatchObject({ code: 'ENOENT' }) + }) + + it('cancels a stalled body and removes partial output', async () => { + const destinationPath = await destination() + const controller = new AbortController() + const cancelled = vi.fn() + const body = new ReadableStream({ + start(stream) { + stream.enqueue(bytes.subarray(0, 3)) + }, + cancel: cancelled + }) + netFetchMock.mockResolvedValueOnce( + new Response(body, { headers: { 'content-length': String(bytes.length) } }) + ) + + const download = downloadSshRelayArtifact({ + artifact: selectedArtifact(), + destinationPath, + signal: controller.signal + }) + await vi.waitFor(() => expect(netFetchMock).toHaveBeenCalledOnce()) + controller.abort(new Error('cancel relay download')) + + await expect(download).rejects.toThrow(/cancel relay download/i) + expect(cancelled).toHaveBeenCalledOnce() + await expect(readFile(destinationPath)).rejects.toMatchObject({ code: 'ENOENT' }) + }) + + it('never overwrites an existing destination', async () => { + const destinationPath = await destination() + await writeFile(destinationPath, 'owner bytes') + + await expect( + downloadSshRelayArtifact({ artifact: selectedArtifact(), destinationPath }) + ).rejects.toMatchObject({ code: 'EEXIST' }) + expect(await readFile(destinationPath, 'utf8')).toBe('owner bytes') + expect(netFetchMock).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/ssh/ssh-relay-artifact-download.ts b/src/main/ssh/ssh-relay-artifact-download.ts new file mode 100644 index 00000000000..ef998f30c65 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-download.ts @@ -0,0 +1,187 @@ +import { createHash } from 'node:crypto' +import { open, rm, type FileHandle } from 'node:fs/promises' + +import { net } from 'electron' + +import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector' + +const DOWNLOAD_TIMEOUT_MS = 5 * 60_000 +const RELEASE_ASSET_HOST = 'release-assets.githubusercontent.com' +const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]) +const REQUEST_INIT = { + credentials: 'omit', + headers: { Accept: 'application/octet-stream' }, + redirect: 'manual' +} as const + +export type SshRelayArtifactDownloadResult = { + destinationPath: string + finalUrl: string + size: number + sha256: SshRelaySelectedArtifact['archive']['sha256'] +} + +async function discardResponse(response: Response): Promise { + await response.body?.cancel().catch(() => {}) +} + +function approvedRedirect(response: Response, sourceUrl: string): string { + const location = response.headers.get('location') + if (!location) { + throw new Error('SSH relay artifact redirect is missing Location') + } + const url = new URL(location, sourceUrl) + if (url.protocol !== 'https:') { + throw new Error('SSH relay artifact redirect must use HTTPS') + } + if (url.hostname !== RELEASE_ASSET_HOST) { + throw new Error('SSH relay artifact redirect has an unapproved origin') + } + if (url.username || url.password) { + throw new Error('SSH relay artifact redirect must not contain credentials') + } + if (url.port) { + throw new Error('SSH relay artifact redirect must not use a custom port') + } + return url.href +} + +async function fetchArtifactResponse( + artifact: SshRelaySelectedArtifact, + signal: AbortSignal +): Promise<{ response: Response; finalUrl: string }> { + const initialUrl = artifact.archive.downloadUrl + const initial = await net.fetch(initialUrl, { ...REQUEST_INIT, signal }) + if (initial.status === 200) { + return { response: initial, finalUrl: initialUrl } + } + if (!REDIRECT_STATUSES.has(initial.status)) { + await discardResponse(initial) + throw new Error(`SSH relay artifact download failed with status ${initial.status}`) + } + + let finalUrl: string + try { + finalUrl = approvedRedirect(initial, initialUrl) + } finally { + await discardResponse(initial) + } + // Why: every request uses a fresh fixed header set and omits session credentials so a signed CDN + // redirect cannot inherit authorization, cookies, or caller-controlled headers. + const redirected = await net.fetch(finalUrl, { ...REQUEST_INIT, signal }) + if (REDIRECT_STATUSES.has(redirected.status)) { + await discardResponse(redirected) + throw new Error('SSH relay artifact download exceeded the approved redirect limit') + } + if (redirected.status !== 200) { + await discardResponse(redirected) + throw new Error(`SSH relay artifact download failed with status ${redirected.status}`) + } + return { response: redirected, finalUrl } +} + +async function writeCompleteChunk(file: FileHandle, buffer: Buffer): Promise { + let offset = 0 + while (offset < buffer.length) { + const { bytesWritten } = await file.write(buffer, offset, buffer.length - offset, null) + if (bytesWritten <= 0) { + throw new Error('SSH relay artifact download could not persist response bytes') + } + offset += bytesWritten + } +} + +async function streamVerifiedResponse( + response: Response, + artifact: SshRelaySelectedArtifact, + signal: AbortSignal, + file: FileHandle +): Promise<{ size: number; sha256: SshRelaySelectedArtifact['archive']['sha256'] }> { + const expected = artifact.archive + const contentLength = response.headers.get('content-length') + if (contentLength !== null) { + const parsed = Number(contentLength) + if (!Number.isSafeInteger(parsed) || parsed !== expected.size) { + await discardResponse(response) + throw new Error('SSH relay artifact Content-Length disagrees with the signed manifest') + } + } + if (!response.body) { + throw new Error('SSH relay artifact response body is missing') + } + + const hash = createHash('sha256') + const reader = response.body.getReader() + let size = 0 + let complete = false + const cancelRead = (): void => { + void reader.cancel(signal.reason).catch(() => {}) + } + signal.addEventListener('abort', cancelRead, { once: true }) + try { + while (true) { + signal.throwIfAborted() + const { done, value } = await reader.read() + signal.throwIfAborted() + if (done) { + complete = true + break + } + const buffer = Buffer.from(value) + size += buffer.length + if (size > expected.size) { + throw new Error('SSH relay artifact response exceeds the signed size') + } + hash.update(buffer) + await writeCompleteChunk(file, buffer) + } + } finally { + signal.removeEventListener('abort', cancelRead) + if (!complete) { + await reader.cancel(signal.aborted ? signal.reason : undefined).catch(() => {}) + } + reader.releaseLock() + } + + if (size !== expected.size) { + throw new Error('SSH relay artifact response size does not match the signed manifest') + } + const sha256 = `sha256:${hash.digest('hex')}` as SshRelaySelectedArtifact['archive']['sha256'] + if (sha256 !== expected.sha256) { + throw new Error('SSH relay artifact response SHA-256 does not match the signed manifest') + } + return { size, sha256 } +} + +export async function downloadSshRelayArtifact({ + artifact, + destinationPath, + signal +}: { + artifact: SshRelaySelectedArtifact + destinationPath: string + signal?: AbortSignal +}): Promise { + const effectiveSignal = signal + ? AbortSignal.any([signal, AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)]) + : AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) + effectiveSignal.throwIfAborted() + + // Why: this boundary owns only a new staging file; cache publication and replacement are later, + // separately gated transactions. + const file = await open(destinationPath, 'wx', 0o600) + let complete = false + try { + const { response, finalUrl } = await fetchArtifactResponse(artifact, effectiveSignal) + const verified = await streamVerifiedResponse(response, artifact, effectiveSignal, file) + await file.sync() + effectiveSignal.throwIfAborted() + complete = true + return { destinationPath, finalUrl, ...verified } + } finally { + await file.close().catch(() => {}) + if (!complete) { + await rm(destinationPath, { force: true }).catch(() => {}) + } + } +} diff --git a/src/main/ssh/ssh-relay-artifact-selector.ts b/src/main/ssh/ssh-relay-artifact-selector.ts index d3cf74818cd..c8088f21891 100644 --- a/src/main/ssh/ssh-relay-artifact-selector.ts +++ b/src/main/ssh/ssh-relay-artifact-selector.ts @@ -67,6 +67,8 @@ export type SshRelayArtifactSelection = } | { kind: 'legacy'; reason: SshRelayArtifactLegacyReason } +export type SshRelaySelectedArtifact = Extract + function selectedArtifact( tuple: VerifiedSshRelayRuntimeTuple, releaseTag: string From 6473801e76793b5afedff68e83c88f6f303d27f2 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 15:40:34 -0400 Subject: [PATCH 155/267] docs: record relay downloader local proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 25 +++-- ...github-release-implementation-checklist.md | 105 ++++++++++++++---- 2 files changed, 98 insertions(+), 32 deletions(-) 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 83bc0cc61c4..c6ddc6e24c6 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 @@ -9,18 +9,16 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop download boundary — offline selection -closed; begin disconnected bounded-download RED, 2026-07-15, Codex implementation owner.** Exact -head `3be4e6867` passes verified immutable tuple/content/archive/URL selection and full runtime -construction on all six native Node 24 jobs in -[run 29443291349](https://github.com/stablyai/orca/actions/runs/29443291349) under -`E-M5-OFFLINE-SELECTION-CI-001`. PR Checks and both Golden E2E jobs are green. The artifact run is -red only after Windows arm64 successfully smokes the runtime and rejects hosted build 26200 against -the retained 26100 floor. Next, implement only a relay-specific bounded Electron download contract; -cache publication/extraction, desktop consumers, SSH transfer/install, mode wiring, tuple enablement, -publication, production keys/environment/seed, and merge to `main` remain disconnected. The -implementation owner is authorized to drive commits, pushes, CI, and PR-contained rehearsals end to -end without merging. +Active checkpoint: **Milestone 5 / Work Package 4 desktop download boundary — disconnected bounded +Electron downloader locally GREEN; exact-head native CI pending, 2026-07-15, Codex implementation +owner.** The purpose-named RED and local GREEN are recorded under +`E-M5-ARTIFACT-DOWNLOAD-LOCAL-RED-001` and `E-M5-ARTIFACT-DOWNLOAD-LOCAL-001`: 16 focused tests, 89 +adjacent trust-chain tests, 307 broad relay contracts, typecheck, full lint, formatting, and static +gates pass. Next, commit/push the isolated package, require its suite in both native workflow +families, and collect all-six Node 24 evidence. Cache publication/extraction, desktop consumers, SSH +transfer/install, mode wiring, tuple enablement, publication, production keys/environment/seed, and +merge to `main` remain disconnected. The implementation owner is authorized to drive commits, +pushes, CI, and PR-contained rehearsals end to end without merging. ## Safety status @@ -301,6 +299,9 @@ end without merging. `E-M5-OFFLINE-SELECTION-CI-WIRING-LOCAL-001`, and exact-head all-six native proof is closed by `E-M5-OFFLINE-SELECTION-CI-001`. The actual packaged embedded-manifest loader remains open. - [ ] Stream bounded downloads; verify signature, size, archive hash, and extracted tree. + The disconnected Electron downloader is locally green under + `E-M5-ARTIFACT-DOWNLOAD-LOCAL-RED-001` and `E-M5-ARTIFACT-DOWNLOAD-LOCAL-001`; exact-head + native CI, packaged signature loading, and extracted-tree verification remain open. - [ ] Add exclusive staging, atomic publication, quarantine, locking, and the 2 GiB cache policy. - [ ] Prove verified cached bytes can be transferred while the client is offline. - [ ] Preserve `ORCA_RELAY_PATH` behind the official-build trust boundary. 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 bf2af1eefae..cd98e6b71bb 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 5 / Work Package 4 desktop download boundary — **Offline selection closed; begin disconnected bounded-download RED — 2026-07-15, Codex implementation owner**. The verified immutable offline tuple/content/archive/URL selector is closed locally and on all six exact-head native Node 24 jobs under E-M5-OFFLINE-SELECTION-LOCAL-001 and E-M5-OFFLINE-SELECTION-CI-001. Begin only relay-specific Electron download contracts for bounded streaming, cancellation, exact size/hash, approved GitHub redirects, and credential/header stripping. Do not add cache publication/extraction, desktop call sites, SSH transfer/install, tuple enablement, mode wiring, or default behavior.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact pushed head `3be4e68679f6f235165c8784eca61576dc2fd828` passes the portable artifact/manifest/selector suites and full runtime construction/execution on all six target-native jobs in run 29443291349 under E-M5-OFFLINE-SELECTION-CI-001. PR Checks 29443290994 and both Golden E2E jobs in 29443291091 are green. Both Linux supplements and Windows x64 floor pass. Windows arm64 floor smoke passes in 5,982.393ms with 48,451,584-byte RSS; the floor gate then correctly rejects hosted build 26200 versus required 26100. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop download boundary — **Disconnected bounded Electron downloader locally GREEN; exact-head native CI pending — 2026-07-15, Codex implementation owner**. The purpose-named missing-module RED and local GREEN are recorded under E-M5-ARTIFACT-DOWNLOAD-LOCAL-RED-001 and E-M5-ARTIFACT-DOWNLOAD-LOCAL-001. This package remains limited to relay-specific bounded streaming, cancellation, exact signed size/hash, one approved GitHub asset redirect, credential/header stripping, exclusive staging, and failure cleanup. Do not add cache publication/extraction, desktop call sites, SSH transfer/install, tuple enablement, mode wiring, or default behavior.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — 16 focused downloader tests, 89 adjacent desktop trust-chain tests, 307 broad relay build/release contracts, typecheck, full lint, formatting, protected-file zero-diff, and `git diff --check` pass locally under E-M5-ARTIFACT-DOWNLOAD-LOCAL-001. Exact-head Node 24 execution on Linux, macOS, and Windows x64/arm64 is still required. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, cache publication/extraction, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -131,10 +131,11 @@ same change as the work it records. E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-RED-001, E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001, and E-M4-DRAFT-RELEASE-COMPOSITION-CI-001. -- Active package: Work Package 4 offline artifact selection. Begin with a purpose-named RED for - exact offline tuple/content selection and immutable GitHub release URL construction from an - already-verified embedded manifest. Do not add signature key policy, networking, download/cache, - desktop call sites, SSH behavior, mode wiring, tuple enablement, or default behavior in this slice. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — Work Package 4 + disconnected bounded Electron downloader. Prove exact signed size/hash streaming into an + exclusive staging file, cancellation/cleanup, one approved GitHub asset redirect, and fixed + credential-free request headers. Keep cache publication/extraction, desktop call sites, SSH + behavior, mode wiring, tuple enablement, and default behavior outside this slice. - Completed Work Package 2 gate: target-native Windows source-signature reports from exact-head artifact jobs 87267322867 and 87267322870 were independently downloaded and matched to their identities and signing-stage reports under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001. PR Checks @@ -11920,7 +11921,7 @@ config/vitest.config.ts config/scripts/ssh-relay-runtime-draft-release-verificat `8c925c24990ac93f6d2dd8de2081a50a74f30d97`; draft PR #8741. Local runner is macOS 26.2 build 25C56 arm64, Node v26.0.0, and pnpm 10.24.0. No network request or external write occurred. - Command: `/usr/bin/time -l pnpm exec vitest run src/main/ssh/ssh-relay-artifact-selector.test.ts - src/main/ssh/ssh-relay-manifest-signature.test.ts` — expected FAIL, 2 files / 26 tests, 7 failed +src/main/ssh/ssh-relay-manifest-signature.test.ts` — expected FAIL, 2 files / 26 tests, 7 failed and 19 passed in 430ms Vitest / 1.31s wall, maximum RSS 136,265,728 bytes, zero swaps. - Contract failures: a compatible selection returned only tuple ID/tuple and omitted the exact content ID, release tag, authenticated archive metadata, and direct release URL. A verified @@ -11945,16 +11946,16 @@ config/vitest.config.ts config/scripts/ssh-relay-runtime-draft-release-verificat 25C56 arm64, Node v26.0.0, and pnpm 10.24.0. All accepted keys and manifests are deterministic test fixtures; no GitHub request or external write occurs. - Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts - --maxWorkers=1 src/main/ssh/ssh-relay-artifact-selector.test.ts - src/main/ssh/ssh-relay-manifest-signature.test.ts` — PASS, 2 files / 27 tests in 1.42s Vitest / +--maxWorkers=1 src/main/ssh/ssh-relay-artifact-selector.test.ts +src/main/ssh/ssh-relay-manifest-signature.test.ts` — PASS, 2 files / 27 tests in 1.42s Vitest / 3.04s wall; maximum RSS 131,923,968 bytes and zero swaps. - Adjacent command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts - --maxWorkers=1` with artifact schema, signature, release asset, selector, runtime identity, manifest +--maxWorkers=1` with artifact schema, signature, release asset, selector, runtime identity, manifest aggregate, aggregate command, and signing-handoff suites — PASS, 8 files / 91 tests in 3.60s Vitest / 4.47s wall; maximum RSS 189,333,504 bytes and zero swaps. - Broad command: `/usr/bin/time -l sh -c "rg --files config/scripts | rg - '/ssh-relay.*[.]test[.]mjs$' | xargs pnpm exec vitest run --config config/vitest.config.ts - --maxWorkers=1"` — PASS, 56 files / 307 tests in 16.83s Vitest / 17.93s wall; maximum RSS +'/ssh-relay.*[.]test[.]mjs$' | xargs pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1"` — PASS, 56 files / 307 tests in 16.83s Vitest / 17.93s wall; maximum RSS 189,857,792 bytes and zero swaps. - Static gates: `/usr/bin/time -l pnpm typecheck` passes in 2.46s wall with 1,252,245,504-byte maximum RSS. `/usr/bin/time -l pnpm lint` passes in 26.98s wall with 2,021,146,624-byte maximum @@ -11987,11 +11988,11 @@ config/vitest.config.ts config/scripts/ssh-relay-runtime-draft-release-verificat 29443022528, and 29443022549 had started but cannot close the selector package because the native artifact workflow lacked its purpose-named source suites. - Audit signal: `rg -n "artifact-selector|manifest-signature|artifact-schema|release-asset|vitest.*src/main/ssh" - .github/workflows config/scripts` returned no selector/source-suite invocation from +.github/workflows config/scripts` returned no selector/source-suite invocation from `.github/workflows/ssh-relay-runtime-artifacts.yml`. - Purpose-named RED command after adding only the workflow assertion: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 - config/scripts/ssh-relay-runtime-workflow.test.mjs` — expected FAIL, 1 failed / 6 passed of 7 in +config/scripts/ssh-relay-runtime-workflow.test.mjs` — expected FAIL, 1 failed / 6 passed of 7 in 473ms Vitest / 2.07s wall, maximum RSS 131,694,592 bytes, zero swaps. The workflow contained zero occurrences where two job-family invocations were required. - Contract pinned by the RED: artifact schema, signature, release asset, selector, and runtime @@ -12070,6 +12071,69 @@ config/vitest.config.ts config/scripts/ssh-relay-runtime-draft-release-verificat bounded relay-download RED. Preserve every cache, consumer, SSH, tuple, publication, and default disconnection. +### E-M5-ARTIFACT-DOWNLOAD-LOCAL-RED-001 — Relay-specific bounded downloader was absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted RED atop exact pushed head + `1e3356cc6fa4e4090935184879b5a77db94ad2e5`; draft PR #8741. Local runner is macOS 26.2 build + 25C56 arm64, Node v26.0.0, and pnpm 10.24.0. No GitHub request or external write occurred. +- Command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-artifact-download.test.ts` — expected FAIL because + `ssh-relay-artifact-download` did not exist, 1 failed suite / 0 tests in 157ms Vitest / 1.05s + wall; maximum RSS 132,038,656 bytes and zero swaps. +- Contract pinned by the RED: consume only an already signature-verified selected artifact; use + Electron networking; stream into a caller-named exclusive staging file; impose a five-minute + total timeout; enforce signed response size and SHA-256; allow at most one exact approved GitHub + asset-host redirect; use fresh fixed credential-free request options; settle cancellation; and + remove only incomplete owned output. +- Does not prove: implementation, GREEN, live networking/proxy/certificates, a production manifest, + cache publication/extraction, desktop consumers, SSH transfer/install, or any enabled tuple. + +### E-M5-ARTIFACT-DOWNLOAD-LOCAL-001 — Disconnected bounded Electron downloader passes locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: implementation commit `ef9681d32` atop exact pushed head + `1e3356cc6fa4e4090935184879b5a77db94ad2e5`; draft PR #8741. Local runner is macOS 26.2 build + 25C56 arm64, Node v26.0.0, and pnpm 10.24.0. Electron `net.fetch` and HTTP responses are mocked; + no GitHub request, release write, or artifact publication occurred. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-artifact-download.test.ts` — PASS before the final + response-disposal hardening, 1 file / 15 tests in 7.49s Vitest / 30.53s wall; maximum RSS + 130,990,080 bytes and zero swaps. The adjacent command below includes the final 16-test suite. +- Adjacent command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1` with artifact download, selector, manifest signature, artifact schema, release asset, + and runtime identity suites — PASS, 6 files / 89 tests in 22.99s Vitest / 40.99s wall; maximum RSS + 128,516,096 bytes and zero swaps. +- Broad command: `/usr/bin/time -l sh -c "rg --files config/scripts | rg +'/ssh-relay.*[.]test[.]mjs$' | xargs pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1"` — PASS, 56 files / 307 tests in 45.84s Vitest / 64.95s wall; maximum RSS + 189,382,656 bytes and zero swaps. +- Static gates: `/usr/bin/time -l pnpm typecheck` passes in 33.08s wall with 1,121,845,248-byte + maximum RSS. `/usr/bin/time -l pnpm lint` passes in 55.13s wall with 2,071,396,352-byte maximum + RSS; all 41 reliability gates and the 355-entry max-lines ratchet pass with the existing 26 + unrelated warnings. Focused `oxlint` passes in 13.59s wall with 123,125,760-byte maximum RSS. + Focused `oxfmt --check`, protected resolver/required-asset zero-diff checks, and `git diff --check` + pass. +- Oracle proved: both direct 200 and a single exact `release-assets.githubusercontent.com` redirect + use a fresh `{ credentials: 'omit', redirect: 'manual', Accept: application/octet-stream }` + request. HTTP, unapproved-origin, credential-bearing, custom-port, missing-Location, chain, + 404/429/503, missing-body, invalid Content-Length, truncation, oversize, wrong-hash, cancellation, + and pre-existing-destination cases fail without selecting partial output. Rejected response bodies + are cancelled, successful bytes are streamed with bounded chunk memory, fully written, fsynced, + and returned only after exact signed size/hash verification. +- Consumer-disconnection oracle: the downloader has no cache publication/extraction, embedded- + manifest loader, retry policy, desktop caller, SSH transfer/install, mode, tuple enablement, or + default behavior. Legacy remains the only production path. +- Does not prove: Node 24/native-runner portability, live GitHub/CDN redirects, system proxy or + certificate behavior, retry/backoff, cache/extraction/atomic publication, disk-full/readonly/ + multi-process races, full-size runtime memory, client-offline transfer, live SSH, performance + against legacy, or any enabled tuple. +- Follow-up: commit and push the isolated implementation/evidence, add a workflow-contract RED/GREEN + requiring the downloader suite in both native job families, and collect exact-head all-six Node 24 + evidence before starting cache/extraction. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -12128,12 +12192,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Begin a purpose-named disconnected download-boundary RED for -bounded streaming, cancellation, archive hash/size, approved GitHub redirect origins, and credential/ -header stripping. Do not add a desktop call site, cache publication, extraction, SSH transfer/install, -mode wiring, tuple enablement, or default behavior. Keep `ORCA_RELAY_PATH`, existing desktop required- -assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux -kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. +Commit and push the locally green disconnected downloader, add a purpose-named workflow-contract +RED/GREEN that requires its portable suite in both native job families, then collect exact-head Node +24 evidence on Linux, macOS, and Windows x64/arm64. Do not add a desktop call site, cache publication, +extraction, SSH transfer/install, mode wiring, tuple enablement, or default behavior. Keep +`ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, +Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, +and every tuple separately gated. 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 From 0d7e0077dd648686b05e8a15e4b72334945f809f Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 15:44:27 -0400 Subject: [PATCH 156/267] ci(ssh): run relay downloader tests natively Co-authored-by: Orca --- .github/workflows/ssh-relay-runtime-artifacts.yml | 2 ++ config/scripts/ssh-relay-runtime-workflow.test.mjs | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index e7f4e39eb40..e4fdd4efadc 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -261,6 +261,7 @@ jobs: src/main/ssh/ssh-relay-manifest-signature.test.ts \ src/main/ssh/ssh-relay-release-asset.test.ts \ src/main/ssh/ssh-relay-artifact-selector.test.ts \ + src/main/ssh/ssh-relay-artifact-download.test.ts \ src/main/ssh/ssh-relay-runtime-identity.test.ts \ config/scripts/ssh-relay-runtime-compatibility.test.mjs \ config/scripts/ssh-relay-node-release-verification.test.mjs \ @@ -763,6 +764,7 @@ jobs: src/main/ssh/ssh-relay-manifest-signature.test.ts ` src/main/ssh/ssh-relay-release-asset.test.ts ` src/main/ssh/ssh-relay-artifact-selector.test.ts ` + src/main/ssh/ssh-relay-artifact-download.test.ts ` src/main/ssh/ssh-relay-runtime-identity.test.ts ` config/scripts/ssh-relay-runtime-compatibility.test.mjs ` config/scripts/ssh-relay-node-release-verification.test.mjs ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 8dff5cdeee6..a6455ec0457 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -104,6 +104,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-manifest-signature.test.ts', 'ssh-relay-release-asset.test.ts', 'ssh-relay-artifact-selector.test.ts', + 'ssh-relay-artifact-download.test.ts', 'ssh-relay-runtime-identity.test.ts' ]) { // Why: portable desktop selection contracts need proof on every native runner family, not From fc6b6d2197fa8cc012317a5ea155e57886183c61 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 15:45:55 -0400 Subject: [PATCH 157/267] docs: record relay downloader CI wiring Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 19 ++++----- ...github-release-implementation-checklist.md | 42 ++++++++++++++++++- 2 files changed, 49 insertions(+), 12 deletions(-) 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 c6ddc6e24c6..75d2cc456ba 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 @@ -9,16 +9,15 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop download boundary — disconnected bounded -Electron downloader locally GREEN; exact-head native CI pending, 2026-07-15, Codex implementation -owner.** The purpose-named RED and local GREEN are recorded under -`E-M5-ARTIFACT-DOWNLOAD-LOCAL-RED-001` and `E-M5-ARTIFACT-DOWNLOAD-LOCAL-001`: 16 focused tests, 89 -adjacent trust-chain tests, 307 broad relay contracts, typecheck, full lint, formatting, and static -gates pass. Next, commit/push the isolated package, require its suite in both native workflow -families, and collect all-six Node 24 evidence. Cache publication/extraction, desktop consumers, SSH -transfer/install, mode wiring, tuple enablement, publication, production keys/environment/seed, and -merge to `main` remain disconnected. The implementation owner is authorized to drive commits, -pushes, CI, and PR-contained rehearsals end to end without merging. +Active checkpoint: **Milestone 5 / Work Package 4 desktop download boundary — downloader native CI +wiring locally GREEN; exact-head native CI pending, 2026-07-15, Codex implementation owner.** The +workflow-contract RED/GREEN is recorded under `E-M5-ARTIFACT-DOWNLOAD-CI-WIRING-RED-001` and +`E-M5-ARTIFACT-DOWNLOAD-CI-WIRING-LOCAL-001`; both native job families now invoke the 16-test +downloader suite. Next, commit/push this test-only wiring and collect all-six Node 24 evidence. Cache +publication/extraction, desktop consumers, SSH transfer/install, mode wiring, tuple enablement, +publication, production keys/environment/seed, and merge to `main` remain disconnected. The +implementation owner is authorized to drive commits, pushes, CI, and PR-contained rehearsals end to +end without merging. ## Safety status 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 cd98e6b71bb..febf5438b97 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 5 / Work Package 4 desktop download boundary — **Disconnected bounded Electron downloader locally GREEN; exact-head native CI pending — 2026-07-15, Codex implementation owner**. The purpose-named missing-module RED and local GREEN are recorded under E-M5-ARTIFACT-DOWNLOAD-LOCAL-RED-001 and E-M5-ARTIFACT-DOWNLOAD-LOCAL-001. This package remains limited to relay-specific bounded streaming, cancellation, exact signed size/hash, one approved GitHub asset redirect, credential/header stripping, exclusive staging, and failure cleanup. Do not add cache publication/extraction, desktop call sites, SSH transfer/install, tuple enablement, mode wiring, or default behavior.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — 16 focused downloader tests, 89 adjacent desktop trust-chain tests, 307 broad relay build/release contracts, typecheck, full lint, formatting, protected-file zero-diff, and `git diff --check` pass locally under E-M5-ARTIFACT-DOWNLOAD-LOCAL-001. Exact-head Node 24 execution on Linux, macOS, and Windows x64/arm64 is still required. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, cache publication/extraction, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop download boundary — **Downloader native CI wiring locally GREEN; exact-head native CI pending — 2026-07-15, Codex implementation owner**. Both native job families invoke the purpose-named downloader suite under E-M5-ARTIFACT-DOWNLOAD-CI-WIRING-LOCAL-001. Commit/push this test-only wiring and collect all-six exact-head Node 24 evidence. Do not add cache publication/extraction, desktop call sites, SSH transfer/install, tuple enablement, mode wiring, or default behavior.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — implementation `ef9681d32` and local evidence `6473801e7` are pushed to draft PR #8741. The workflow-contract RED and local GREEN are recorded under E-M5-ARTIFACT-DOWNLOAD-CI-WIRING-RED-001 and E-M5-ARTIFACT-DOWNLOAD-CI-WIRING-LOCAL-001; 7 files / 96 tests pass. Exact-head native execution remains mandatory. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, cache publication/extraction, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -12134,6 +12134,44 @@ src/main/ssh/ssh-relay-artifact-download.test.ts` — expected FAIL because requiring the downloader suite in both native job families, and collect exact-head all-six Node 24 evidence before starting cache/extraction. +### E-M5-ARTIFACT-DOWNLOAD-CI-WIRING-RED-001 — Native jobs omitted downloader contracts + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted workflow assertion atop exact pushed downloader head + `6473801e7`; draft PR #8741. +- Command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +config/scripts/ssh-relay-runtime-workflow.test.mjs` — expected FAIL, 1 failed / 6 passed of 7 tests + in 980ms Vitest / 5.28s wall; maximum RSS 132,448,256 bytes and zero swaps. The downloader suite + occurred zero times where the contract requires one POSIX-family and one Windows-family invocation. +- Contract pinned by the RED: `ssh-relay-artifact-download.test.ts` must execute as part of both + target-native workflow test families so a generic PR/Linux job cannot be misreported as + Linux/macOS/Windows x64/arm64 downloader portability proof. +- Does not prove: corrected wiring, native execution, live Electron networking, product behavior, or + any enabled tuple. + +### E-M5-ARTIFACT-DOWNLOAD-CI-WIRING-LOCAL-001 — Both native job families invoke downloader tests + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: workflow/test commit `0d7e0077d` atop exact pushed downloader head `6473801e7`; draft PR + #8741. +- Command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1` + with the workflow contract plus artifact download, selector, manifest signature, artifact schema, + release asset, and runtime identity suites — PASS, 7 files / 96 tests in 11.97s Vitest / 16.98s + wall; maximum RSS 141,099,008 bytes and zero swaps. Focused `oxlint`, `oxfmt --check`, protected- + file zero-diff, and `git diff --check` pass. +- Oracle proved: the downloader suite is named exactly once by the four-runner POSIX job family and + exactly once by the two-runner Windows job family. YAML parsing, runner labels, and existing + credential/publication prohibitions remain covered by the same workflow contract. +- Consumer-disconnection oracle: this changes test execution only. It adds no network caller, + credential, release write, cache/extraction path, desktop consumer, SSH behavior, tuple enablement, + or default behavior. +- Does not prove: actual Linux/macOS/Windows x64/arm64 execution. Replacement exact-head CI after + this correction is mandatory. +- Follow-up: commit/push the workflow/test/evidence correction and collect all-six native Node 24 + jobs plus PR Checks and Golden E2E. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From d345cbfc7ccbba1113981ebf915c1e4f6dd9ee53 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 16:03:11 -0400 Subject: [PATCH 158/267] docs: record relay downloader native CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 23 +++--- ...github-release-implementation-checklist.md | 82 +++++++++++++++---- 2 files changed, 80 insertions(+), 25 deletions(-) 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 75d2cc456ba..6bdb9f59c7d 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 @@ -9,15 +9,17 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop download boundary — downloader native CI -wiring locally GREEN; exact-head native CI pending, 2026-07-15, Codex implementation owner.** The -workflow-contract RED/GREEN is recorded under `E-M5-ARTIFACT-DOWNLOAD-CI-WIRING-RED-001` and -`E-M5-ARTIFACT-DOWNLOAD-CI-WIRING-LOCAL-001`; both native job families now invoke the 16-test -downloader suite. Next, commit/push this test-only wiring and collect all-six Node 24 evidence. Cache -publication/extraction, desktop consumers, SSH transfer/install, mode wiring, tuple enablement, -publication, production keys/environment/seed, and merge to `main` remain disconnected. The -implementation owner is authorized to drive commits, pushes, CI, and PR-contained rehearsals end to -end without merging. +Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — disconnected bounded +downloader closed; begin isolated extraction/cache contract audit, 2026-07-15, Codex implementation +owner.** Exact-head run +[29445770009](https://github.com/stablyai/orca/actions/runs/29445770009) passes the +downloader-containing contracts and full runtime build/smoke/equality on all six native Node 24 jobs +under `E-M5-ARTIFACT-DOWNLOAD-CI-001`. PR Checks and both Golden E2E jobs are green; the artifact run +is red only after Windows arm64 passes smoke and rejects hosted build 26200 against the retained +26100 floor. Next, audit the proven release extraction boundary against desktop constraints and begin +an isolated extraction/cache-staging RED. Desktop consumers, SSH transfer/install, mode wiring, +tuple enablement, publication, production keys/environment/seed, and merge to `main` remain +disconnected. ## Safety status @@ -300,7 +302,8 @@ end without merging. - [ ] Stream bounded downloads; verify signature, size, archive hash, and extracted tree. The disconnected Electron downloader is locally green under `E-M5-ARTIFACT-DOWNLOAD-LOCAL-RED-001` and `E-M5-ARTIFACT-DOWNLOAD-LOCAL-001`; exact-head - native CI, packaged signature loading, and extracted-tree verification remain open. + all-six native CI is closed under `E-M5-ARTIFACT-DOWNLOAD-CI-001`. Packaged signature loading + and extracted-tree verification remain open. - [ ] Add exclusive staging, atomic publication, quarantine, locking, and the 2 GiB cache policy. - [ ] Prove verified cached bytes can be transferred while the client is offline. - [ ] Preserve `ORCA_RELAY_PATH` behind the official-build trust boundary. 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 febf5438b97..710a74baf26 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 5 / Work Package 4 desktop download boundary — **Downloader native CI wiring locally GREEN; exact-head native CI pending — 2026-07-15, Codex implementation owner**. Both native job families invoke the purpose-named downloader suite under E-M5-ARTIFACT-DOWNLOAD-CI-WIRING-LOCAL-001. Commit/push this test-only wiring and collect all-six exact-head Node 24 evidence. Do not add cache publication/extraction, desktop call sites, SSH transfer/install, tuple enablement, mode wiring, or default behavior.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — implementation `ef9681d32` and local evidence `6473801e7` are pushed to draft PR #8741. The workflow-contract RED and local GREEN are recorded under E-M5-ARTIFACT-DOWNLOAD-CI-WIRING-RED-001 and E-M5-ARTIFACT-DOWNLOAD-CI-WIRING-LOCAL-001; 7 files / 96 tests pass. Exact-head native execution remains mandatory. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, cache publication/extraction, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **Disconnected bounded downloader closed; begin isolated extraction/cache contract audit — 2026-07-15, Codex implementation owner**. Exact-head all-six native Node 24 proof is complete under E-M5-ARTIFACT-DOWNLOAD-CI-001. Begin only the next purpose-named RED for desktop archive extraction/cache staging and reconcile it with the already-proven archive-safety contracts. Do not add desktop call sites, SSH transfer/install, tuple enablement, mode wiring, or default behavior.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact implementation/evidence head `fc6b6d2197fa8cc012317a5ea155e57886183c61` passes the downloader-containing contract suite and full runtime build/smoke/equality on all six native jobs in run 29445770009 under E-M5-ARTIFACT-DOWNLOAD-CI-001. PR Checks 29445769922 and both Golden E2E jobs in 29445769934 are green. The artifact run is red only after the hosted Windows arm64 build 26200 passes runtime verification/smoke and fails the retained build-26100 floor. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, cache publication/extraction, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
    Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693) @@ -1057,17 +1057,23 @@ Suggested concrete modules: - [x] Construct the exact `/releases/download//` URL without an API lookup. E-M5-OFFLINE-SELECTION-LOCAL-001 derives the tag-qualified direct URL from authenticated fields and proves that no `latest` lookup is present. -- [ ] Reject redirects that violate the approved GitHub release-asset origin policy. -- [ ] Ensure authorization, cookies, proxy credentials, and custom headers do not leak across redirect - origins. +- [x] Reject redirects that violate the approved GitHub release-asset origin policy. + E-M5-ARTIFACT-DOWNLOAD-LOCAL-001 and E-M5-ARTIFACT-DOWNLOAD-CI-001 prove one exact approved + asset host and fail-closed protocol/origin/credential/port/chain cases. +- [x] Ensure authorization, cookies, proxy credentials, and custom headers do not leak across redirect + origins. E-M5-ARTIFACT-DOWNLOAD-LOCAL-001 and E-M5-ARTIFACT-DOWNLOAD-CI-001 prove fresh fixed + credential-free options on both requests. - [ ] Use Electron networking that respects supported system proxy and certificate behavior. ### Download and local cache - [ ] Store archives and extracted trees under an app-owned cache, keyed by signed content identity. -- [ ] Stream downloads into exclusive temporary files; never buffer a full runtime in memory. -- [ ] Apply download size and time budgets while streaming. -- [ ] Verify archive SHA-256 before extraction. +- [x] Stream downloads into exclusive temporary files; never buffer a full runtime in memory. + E-M5-ARTIFACT-DOWNLOAD-LOCAL-001 and E-M5-ARTIFACT-DOWNLOAD-CI-001. +- [x] Apply download size and time budgets while streaming. + E-M5-ARTIFACT-DOWNLOAD-LOCAL-001 and E-M5-ARTIFACT-DOWNLOAD-CI-001. +- [x] Verify archive SHA-256 before extraction. + E-M5-ARTIFACT-DOWNLOAD-LOCAL-001 and E-M5-ARTIFACT-DOWNLOAD-CI-001. - [ ] Extract into an exclusive temporary directory with all archive-safety checks from Milestone 2. - [ ] Verify the exact expanded tree before atomically publishing the cache entry. - [ ] Coordinate cache ownership across multiple Orca processes and windows. @@ -12172,6 +12178,53 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — expected FAIL, 1 failed - Follow-up: commit/push the workflow/test/evidence correction and collect all-six native Node 24 jobs plus PR Checks and Golden E2E. +### E-M5-ARTIFACT-DOWNLOAD-CI-001 — Bounded downloader contracts pass all six native Node 24 jobs + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `fc6b6d2197fa8cc012317a5ea155e57886183c61`; downloader + implementation `ef9681d32`, native test wiring `0d7e0077d`; draft PR #8741. +- Workflow: [SSH Relay Runtime Artifacts run 29445770009](https://github.com/stablyai/orca/actions/runs/29445770009), + 19:46:32Z–20:00:26Z. The aggregate conclusion is the expected failure only because the separate + Windows arm64 floor job rejects hosted build 26200 after successful runtime verification/smoke. +- Native jobs and downloader-containing portable/release contract suites: + - Linux x64 job 87455756464, image `ubuntu-24.04` `20260714.240.1`, PASS in 5m49s; 61 files / + 392 tests in 10.20s. + - Linux arm64 job 87455756439, image `ubuntu-24.04-arm` `20260714.61.1`, PASS in 4m39s; 61 files / + 392 tests in 7.70s. + - Darwin x64 job 87455756475, image `macos-15` `20260629.0276.1`, PASS in 5m26s; 61 files / + 392 tests in 23.13s. + - Darwin arm64 job 87455756647, image `macos-15-arm64` `20260706.0213.1`, PASS in 5m13s; 61 + files / 392 tests in 15.96s. + - Windows x64 job 87455756492, image `windows-2022` `20260706.237.1`, PASS in 4m36s; 62 files / + 386 passed + 10 platform skips of 396 in 13.78s. + - Windows arm64 job 87455756474, image `windows-11-arm64` `20260714.109.1`, PASS in 9m32s; 62 + files / 386 passed + 10 platform skips of 396 in 15.26s. +- Downstream/adjacent evidence: Linux x64 supplement job 87456995626 and Linux arm64 supplement job + 87456995632 pass. Windows x64 floor job 87457773650 passes. Windows arm64 floor job 87457773621 + passes Node v24.18.0, PTY, watcher, resource settlement, and full-tree verification; smoke takes + 5,781.9983ms with 47,329,280-byte RSS and the complete verifier takes 7,750.6872ms before the floor + gate rejects observed build 26200 versus required build 26100. +- PR Checks [run 29445769922](https://github.com/stablyai/orca/actions/runs/29445769922), job + 87455727487, passes in 11m12s. Golden E2E + [run 29445769934](https://github.com/stablyai/orca/actions/runs/29445769934) passes Linux job + 87455727461 in 4m32s and macOS job 87455727503 in 4m01s. +- Oracle proved: the relay-specific downloader consumes only the verified selected-artifact type and + its direct immutable URL; exact signed size/hash, fixed credential-free requests, one approved + redirect, rejected-response disposal, bounded chunk streaming, exclusive output, cancellation, + fsync, and failure cleanup contracts execute consistently under Node 24 on Linux, macOS, and + Windows x64/arm64. Full artifact build, smoke, equality, and adjacent product gates remain green. +- Consumer-disconnection oracle: there is no live network caller, packaged manifest/key, cache + publication/extraction, desktop consumer, SSH transfer/install, mode, tuple enablement, release + write, or default behavior. No runtime artifact is published. Legacy remains the only production + path. +- Does not prove: live Electron/GitHub/CDN/proxy/certificate behavior, production manifest keys or + packaged embedding, retry/backoff, cache/extraction/atomic publication, disk/resource/concurrency + failures, full-size downloader memory, client-offline transfer, live SSH, performance against + legacy, oldest Windows arm64/macOS/Linux-kernel floors, native signing/trust, or any enabled tuple. +- Follow-up: checkpoint this evidence, update PR/worktree status, then begin only the isolated + desktop extraction/cache contract audit and purpose-named RED. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -12230,13 +12283,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the locally green disconnected downloader, add a purpose-named workflow-contract -RED/GREEN that requires its portable suite in both native job families, then collect exact-head Node -24 evidence on Linux, macOS, and Windows x64/arm64. Do not add a desktop call site, cache publication, -extraction, SSH transfer/install, mode wiring, tuple enablement, or default behavior. Keep -`ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, -Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, -and every tuple separately gated. +Checkpoint the downloader's exact-head native evidence, then audit the already-proven release archive +extraction boundary against desktop constraints and begin only a purpose-named disconnected desktop +extraction/cache-staging RED. Do not add a desktop call site, SSH transfer/install, mode wiring, tuple +enablement, or default behavior. Keep `ORCA_RELAY_PATH`, existing desktop required-assets behavior, +detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, +release-cut, desktop builds, publication, and every tuple separately gated. 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 From c5fe474e6f3f84e8b6c0fed930dd6dc580454e74 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 16:19:04 -0400 Subject: [PATCH 159/267] docs: select portable relay archive format Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 14 ++- ...github-release-implementation-checklist.md | 97 ++++++++++++++++--- ...6-07-14-ssh-relay-github-release-plan.html | 7 +- 3 files changed, 96 insertions(+), 22 deletions(-) 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 6bdb9f59c7d..8f11686bd93 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 @@ -9,15 +9,16 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — disconnected bounded -downloader closed; begin isolated extraction/cache contract audit, 2026-07-15, Codex implementation -owner.** Exact-head run +Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — in-progress portable +POSIX archive prerequisite correction, 2026-07-15, Codex implementation owner.** Exact-head run [29445770009](https://github.com/stablyai/orca/actions/runs/29445770009) passes the downloader-containing contracts and full runtime build/smoke/equality on all six native Node 24 jobs under `E-M5-ARTIFACT-DOWNLOAD-CI-001`. PR Checks and both Golden E2E jobs are green; the artifact run is red only after Windows arm64 passes smoke and rejects hosted build 26200 against the retained -26100 floor. Next, audit the proven release extraction boundary against desktop constraints and begin -an isolated extraction/cache-staging RED. Desktop consumers, SSH transfer/install, mode wiring, +26100 floor. `E-M5-ARCHIVE-PORTABILITY-AUDIT-001` proves the unpublished `.tar.xz` output cannot +meet the cross-client 64 MiB extraction contract and selects bounded Node-native `.tar.br`. Replace +the archive contracts and all-six artifact evidence before the isolated extraction/cache-staging +RED. Desktop consumers, SSH transfer/install, mode wiring, tuple enablement, publication, production keys/environment/seed, and merge to `main` remain disconnected. @@ -153,6 +154,9 @@ disconnected. - [x] Build and smoke-test Node, patched `node-pty`, and `@parcel/watcher` on GitHub runners for Linux, macOS, and Windows on x64 and arm64. - [x] Prove exact clean-build equality and exact runtime-tree closure on all six runner families. +- [ ] Replace unpublished POSIX `.tar.xz` outputs with deterministic bounded `.tar.br` and rerun + exact clean-build/archive/execution proof on all six native jobs + (`E-M5-ARCHIVE-PORTABILITY-AUDIT-001` selects the correction; implementation evidence open). - [x] Complete the all-six SBOM, license, provenance, toolchain, and prohibited-content audit. - [x] Rebuild both Linux artifacts in the digest-pinned glibc 2.28/libstdc++ 6.0.25 userland on native x64/arm64 runners; smoke and compare them there. (`E-M3-LINUX-NATIVE-USERLAND-CI-001`) 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 710a74baf26..790f10ad2c6 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 5 / Work Package 4 desktop cache boundary — **Disconnected bounded downloader closed; begin isolated extraction/cache contract audit — 2026-07-15, Codex implementation owner**. Exact-head all-six native Node 24 proof is complete under E-M5-ARTIFACT-DOWNLOAD-CI-001. Begin only the next purpose-named RED for desktop archive extraction/cache staging and reconcile it with the already-proven archive-safety contracts. Do not add desktop call sites, SSH transfer/install, tuple enablement, mode wiring, or default behavior.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact implementation/evidence head `fc6b6d2197fa8cc012317a5ea155e57886183c61` passes the downloader-containing contract suite and full runtime build/smoke/equality on all six native jobs in run 29445770009 under E-M5-ARTIFACT-DOWNLOAD-CI-001. PR Checks 29445769922 and both Golden E2E jobs in 29445769934 are green. The artifact run is red only after the hosted Windows arm64 build 26200 passes runtime verification/smoke and fails the retained build-26100 floor. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, cache publication/extraction, SSH transfer/install, tuple enablement, or production/default behavior change. No accepted production key, protected environment/seed, native-signing trust proof, publication path, or tuple enablement is connected. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: portable POSIX archive prerequisite correction**. The audit under E-M5-ARCHIVE-PORTABILITY-AUDIT-001 proves the current 64 MiB-dictionary `.tar.xz` output cannot meet the reviewed cross-client and 64 MiB desktop extraction contracts without an unsafe system/native dependency. Correct only the unpublished POSIX relay archive family to bounded Node-native `.tar.br`, replace its exact-head all-six artifact evidence, and then resume the disconnected desktop extraction RED. Windows ZIP, Node upstream input archives, desktop call sites, cache publication, SSH transfer/install, tuple enablement, mode wiring, and default behavior stay unchanged.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact implementation/evidence head `fc6b6d2197fa8cc012317a5ea155e57886183c61` passes the downloader-containing contract suite and full runtime build/smoke/equality on all six native jobs in run 29445770009 under E-M5-ARTIFACT-DOWNLOAD-CI-001. E-M5-ARCHIVE-PORTABILITY-AUDIT-001 measures the exact Linux x64 artifact from that run and selects deterministic Brotli quality 9, `lgwin=20`, and 64 KiB chunks: full 119 MiB extraction through `tar` settles in 533.13ms with a 34,144,256-byte RSS increase. The existing XZ stream alone declares 67,175,032 bytes of decoder memory, already 66,168 bytes over the complete desktop budget. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, cache publication/extraction, SSH transfer/install, tuple enablement, or production/default behavior change. 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) @@ -131,11 +131,13 @@ same change as the work it records. E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-RED-001, E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001, and E-M4-DRAFT-RELEASE-COMPOSITION-CI-001. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — Work Package 4 - disconnected bounded Electron downloader. Prove exact signed size/hash streaming into an - exclusive staging file, cancellation/cleanup, one approved GitHub asset redirect, and fixed - credential-free request headers. Keep cache publication/extraction, desktop call sites, SSH - behavior, mode wiring, tuple enablement, and default behavior outside this slice. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — portable POSIX relay + archive prerequisite correction exposed by Work Package 4. E-M5-ARCHIVE-PORTABILITY-AUDIT-001 + rejects system XZ, the off-the-shelf WASM decoder, and the transitive p7zip binary and selects + deterministic Node-native `.tar.br` with quality 9, `lgwin=20`, and 64 KiB chunks. First replace + the archive builder/schema/inspection contracts and all-six artifact evidence; only then resume + disconnected desktop extraction. Keep Node upstream `.tar.xz` inputs, Windows ZIP, cache + publication, desktop call sites, SSH behavior, mode wiring, tuple enablement, and defaults intact. - 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 @@ -942,9 +944,12 @@ Each runtime must contain only the executable closure required by the relay. directories, and Orca-built debug symbols unless an approved diagnostics requirement needs them. Preserve verified official Node executable bytes without stripping or rewriting them, even when the upstream binary contains debug metadata. (E-M3-RUNTIME-CLOSURE-CI-001) -- [x] Make POSIX `tar.xz` and Windows ZIP output deterministic for the same tree, epoch, and pinned - compression toolchain. (E-M3-RUNTIME-LOCAL-001, E-M3-REPRODUCIBILITY-CI-001, - E-M3-RUNTIME-CLOSURE-CI-001) +- [ ] **In progress — 2026-07-15, Codex implementation owner:** make POSIX `.tar.br` and Windows ZIP + output deterministic for the same tree, epoch, and pinned compression contract. POSIX uses + Node's built-in Brotli quality 9, `lgwin=20`, and 64 KiB chunks so every client architecture + can extract within budget without a native/system decompressor. Historical `.tar.xz` evidence + remains valid only for its exact unpublished bytes and must be replaced for the new POSIX + archive family. (Decision evidence: E-M5-ARCHIVE-PORTABILITY-AUDIT-001.) - [x] Verify required executable modes before archiving. (E-M3-RUNTIME-LOCAL-001) - [ ] Sign native code according to the platform decisions in Milestone 1. - [ ] Verify platform-native signatures/policy on target-native runners after signing and before @@ -1075,6 +1080,7 @@ Suggested concrete modules: - [x] Verify archive SHA-256 before extraction. E-M5-ARTIFACT-DOWNLOAD-LOCAL-001 and E-M5-ARTIFACT-DOWNLOAD-CI-001. - [ ] Extract into an exclusive temporary directory with all archive-safety checks from Milestone 2. + Begin only after the `.tar.br` prerequisite has exact-head all-six artifact evidence. - [ ] Verify the exact expanded tree before atomically publishing the cache entry. - [ ] Coordinate cache ownership across multiple Orca processes and windows. - [ ] Recover stale download/extraction locks without deleting an active writer. @@ -12225,6 +12231,63 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — expected FAIL, 1 failed - Follow-up: checkpoint this evidence, update PR/worktree status, then begin only the isolated desktop extraction/cache contract audit and purpose-named RED. +### E-M5-ARCHIVE-PORTABILITY-AUDIT-001 — Current POSIX archive violates the cross-client memory contract + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Environment: macOS 26.2 build 25C56 arm64; Node v26.0.0; pnpm 10.24.0; XZ Utils/liblzma + 5.8.3. Source artifact is the exact Linux x64 output from run 29445770009, artifact 8355514197, + implementation/evidence head `fc6b6d2197fa8cc012317a5ea155e57886183c61`. +- Exact acquisition/identity commands: + `gh run download 29445770009 -n ssh-relay-runtime-linux-x64-glibc -D /tmp/orca-runtime-artifact-29445770009`; + `xz --robot --list -vv /tmp/orca-runtime-artifact-29445770009/orca-ssh-relay-runtime-v1-linux-x64-glibc-fc63ca342a5990f460ec6d72262a8542173dab20ce03c9b9cfb755b1c6057e6d.tar.xz`; + `xz --decompress --stdout --single-stream > /tmp/orca-runtime-real.tar`. +- Current artifact result: 29,268,104 compressed bytes, 124,881,408 expanded bytes, CRC64, and a + declared 67,175,032-byte LZMA2 decoder-memory requirement from `--lzma2=dict=64MiB`. The decoder + alone is 66,168 bytes over the complete 64 MiB desktop incremental-memory budget before TAR + parsing, hashing, or extraction. +- Portable WASM audit commands: `npm view xz-decompress@0.2.3 --json` and + `npm pack xz-decompress@0.2.3 --pack-destination /tmp/orca-xz-decompress-audit`, followed by a + Node `Readable.toWeb(createReadStream(..., { highWaterMark: 64 * 1024 }))` -> + `new XzReadableStream(...)` -> counting `Writable` pipeline over 350 MiB generated XZ streams. + The maintained dependency is MIT/public-domain, dependency-free, CRC64-capable, and emits 65,536- + byte chunks, but measured 104,529,920 bytes above baseline with a 16 MiB dictionary and + 138,854,400 bytes above baseline with the current 64 MiB dictionary. It is rejected. +- Packaged-binary audit commands: `pnpm why 7zip-bin`; `node -p "require('7zip-bin').path7za"`; + `file `; `codesign -dv --verbose=4 `; `spctl --assess --type execute --verbose=4 +`; and `/usr/bin/time -l x -so <350MiB.xz> | wc -c`. + The only existing package is a transitive dev dependency. Its macOS arm64 binary is p7zip 16.02 + from 2016, arrives without execute permission, is only ad-hoc linker-signed, and is rejected by + macOS assessment. It uses 75,628,544 bytes RSS for the current dictionary before the desktop TAR + parser. It is rejected; no production dependency or packaged executable was added. +- Selected correction command: Node `createReadStream` with a 65,536-byte high-water mark -> + `createBrotliCompress({ chunkSize: 65_536, params: { BROTLI_PARAM_QUALITY: 9, +BROTLI_PARAM_LGWIN: 20 } })` -> exclusive output. The exact real runtime becomes 35,977,664 bytes + in 11.78s: 6,709,560 bytes / 22.9245% larger than XZ, but 8,282,308 bytes smaller than gzip-9. +- Selected extraction command: `/usr/bin/time -l node --expose-gc -e createBrotliDecompress(65_536) -> tar.extract({ strict: true, +preserveOwner: false, noChmod: false, unlink: false })>` over that exact runtime. It reconstructs + 124,881,408 bytes / 34 files in 533.13ms. In-process sampling records a 54,968,320-byte baseline, + 89,112,576-byte peak, and 34,144,256-byte incremental RSS; the decompressed SHA-256 is + `b3fe955ae26b9413505c9c614842377851e686802bbb70e5c77397557fbaed6f` with a 65,536-byte maximum + output chunk. +- Decision: unpublished POSIX relay outputs change from `.tar.xz` to deterministic `.tar.br` using + Node's built-in Brotli quality 9, `lgwin=20`, and 64 KiB chunks. Windows relay ZIP and Node's + upstream `.tar.xz` inputs do not change. No native/WASM decompressor dependency is introduced. + The HTML plan and both living checklists are updated before implementation. +- Evidence invalidation: historical XZ artifact/tree/native evidence remains truthful for its exact + unpublished bytes, but it does not prove the new archive family. The POSIX archive builder, + schema, strict inspection/extraction, reproducibility, tuple descriptors, release composition, + read-back execution, all-six native artifact jobs, and downstream exact-archive evidence must be + rerun. Windows ZIP, runtime-tree/native smoke, manifest-signature primitives, downloader, SSH + legacy behavior, and production defaults are unchanged. +- Does not prove: implementation, deterministic repeat output, hostile/truncated Brotli rejection, + Linux/Windows/macOS client execution, Node 24 behavior, packaged Electron extraction, cache + publication, SSH transfer/install, native trust, oldest floors, or any enabled tuple. +- Follow-up: add a purpose-named `.tar.br` contract RED, implement only the artifact-format + correction, run local archive/release/desktop parity gates, then push and require exact-head + all-six native artifact proof before resuming desktop extraction. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -12283,12 +12346,14 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Checkpoint the downloader's exact-head native evidence, then audit the already-proven release archive -extraction boundary against desktop constraints and begin only a purpose-named disconnected desktop -extraction/cache-staging RED. Do not add a desktop call site, SSH transfer/install, mode wiring, tuple -enablement, or default behavior. Keep `ORCA_RELAY_PATH`, existing desktop required-assets behavior, -detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, -release-cut, desktop builds, publication, and every tuple separately gated. +Add the purpose-named `.tar.br` contract RED selected by E-M5-ARCHIVE-PORTABILITY-AUDIT-001, then +implement only the POSIX relay artifact-format correction and replace its exact-head all-six native +archive/reproducibility/execution evidence. Do not begin desktop extraction/cache staging until that +prerequisite is green. Do not add a desktop call site, SSH transfer/install, mode wiring, tuple +enablement, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, +`ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, +Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, +and every tuple separately gated. 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 diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index fd027ae3067..b790f921afd 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -942,7 +942,12 @@

    Build self-contained runtimes

    compiler/toolchain with executable C and C++ probes; only genuinely missing prerequisites use HTTPS with bounded connection timeouts and retries. Probe or install exhaustion fails the native build before any artifact is accepted. Run - executable runtime smoke probes before archiving. + executable runtime smoke probes before archiving. POSIX relay outputs use + deterministic .tar.br archives produced and consumed with Node's + built-in Brotli streams at quality 9, lgwin=20, and 64 KiB chunks; + Windows relay outputs remain ZIP. This lets every desktop client extract every + remote tuple without a system XZ tool, a packaged native decompressor, or the 64 + MiB XZ dictionary that exhausted the desktop extraction budget.

    From c9f6da55e41d2f201684e967ce4c3db37150571f Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 16:30:48 -0400 Subject: [PATCH 160/267] build(ssh): use portable relay archives Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 21 ++- .../ssh-relay-runtime-native-signing.yml | 4 +- .../ssh-relay-runtime-aggregate-input.mjs | 2 +- ...ssh-relay-runtime-aggregate-input.test.mjs | 6 +- .../ssh-relay-runtime-archive-extraction.mjs | 53 ++---- config/scripts/ssh-relay-runtime-archive.mjs | 154 ++++++++--------- ...me-draft-readback-materialization.test.mjs | 2 +- .../ssh-relay-runtime-draft-readback.test.mjs | 2 +- .../ssh-relay-runtime-draft-recovery.test.mjs | 2 +- ...lay-runtime-draft-release-verification.mjs | 2 +- ...untime-draft-release-verification.test.mjs | 2 +- .../ssh-relay-runtime-draft-upload.test.mjs | 2 +- ...time-macos-signature-verification.test.mjs | 2 +- ...untime-manifest-aggregate-command.test.mjs | 2 +- ...-relay-runtime-manifest-aggregate.test.mjs | 2 +- ...h-relay-runtime-manifest-assembly.test.mjs | 6 +- ...-runtime-manifest-signing-handoff.test.mjs | 2 +- .../ssh-relay-runtime-manifest-tuple.mjs | 2 +- .../ssh-relay-runtime-manifest-validation.mjs | 2 +- ...y-runtime-native-signing-workflow.test.mjs | 3 + ...sh-relay-runtime-portable-archive.test.mjs | 156 ++++++++++++++++++ .../ssh-relay-runtime-post-sign-metadata.mjs | 2 +- .../ssh-relay-runtime-provenance.test.mjs | 2 +- ...untime-readback-archive-execution.test.mjs | 2 +- ...-relay-runtime-release-stage-gate.test.mjs | 4 +- ...ssh-relay-runtime-reproducibility.test.mjs | 2 +- .../ssh-relay-runtime-workflow.test.mjs | 5 +- .../ssh/ssh-relay-artifact-schema.test.ts | 2 +- .../ssh/ssh-relay-manifest-signature.test.ts | 4 +- src/main/ssh/ssh-relay-release-asset.test.ts | 2 +- src/main/ssh/ssh-relay-release-asset.ts | 4 +- .../ssh/ssh-relay-runtime-identity.test.ts | 2 +- 32 files changed, 295 insertions(+), 165 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-portable-archive.test.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index e4fdd4efadc..3312e25d890 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -246,6 +246,7 @@ jobs: node --check config/scripts/ssh-relay-runtime-post-sign-metadata.test.mjs node --check config/scripts/ssh-relay-runtime-archive-extraction.mjs node --check config/scripts/ssh-relay-runtime-archive-extraction.test.mjs + node --check config/scripts/ssh-relay-runtime-portable-archive.test.mjs node --check config/scripts/ssh-relay-runtime-native-signing-stage-report.mjs node --check config/scripts/ssh-relay-runtime-macos-signing.mjs node --check config/scripts/ssh-relay-runtime-macos-signing.test.mjs @@ -272,6 +273,7 @@ jobs: config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs \ config/scripts/ssh-relay-node-zip-inspection.test.mjs \ config/scripts/ssh-relay-runtime-artifact.test.mjs \ + config/scripts/ssh-relay-runtime-portable-archive.test.mjs \ config/scripts/ssh-relay-runtime-baseline.test.mjs \ config/scripts/ssh-relay-runtime-build.test.mjs \ config/scripts/ssh-relay-runtime-linux-build-evidence.test.mjs \ @@ -388,7 +390,7 @@ jobs: --git-commit "$source_commit" first_output="$output_root/first" identity="$first_output/orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json" - archive=$(find "$first_output" -maxdepth 1 -name '*.tar.xz' -print -quit) + archive=$(find "$first_output" -maxdepth 1 -name '*.tar.br' -print -quit) test -n "$archive" executed_runtime="$output_root/readback-executed" # Why: exercise only local verified bytes; release writes and remote consumers stay disconnected. @@ -449,7 +451,7 @@ jobs: --source-date-epoch "$source_date_epoch" \ --git-commit "$source_commit" identity="$output/orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json" - archive=$(find "$output" -maxdepth 1 -name '*.tar.xz' -print -quit) + archive=$(find "$output" -maxdepth 1 -name '*.tar.br' -print -quit) test -n "$archive" /usr/bin/time -p node config/scripts/verify-ssh-relay-runtime.mjs \ --runtime-directory "$output/runtime" \ @@ -467,7 +469,7 @@ jobs: --first-output-directory "$first_output" \ --second-output-directory "$second_output" identity="$first_output/orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json" - archive=$(find "$first_output" -maxdepth 1 -name '*.tar.xz' -print -quit) + archive=$(find "$first_output" -maxdepth 1 -name '*.tar.br' -print -quit) test -n "$archive" executed_runtime="$output_root/readback-executed" # Why: exercise only local verified bytes; release writes and remote consumers stay disconnected. @@ -496,7 +498,7 @@ jobs: test ! -e "$signing_stage/bin/node" rm -rf -- "$signing_stage" mkdir -p "$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}" - cp "$first_output"/*.tar.xz "$first_output"/*.spdx.json "$first_output"/*.provenance.json "$first_output"/*.identity.json \ + cp "$first_output"/*.tar.br "$first_output"/*.spdx.json "$first_output"/*.provenance.json "$first_output"/*.identity.json \ "$signing_report" "$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}/" - name: Upload unpublished artifact evidence @@ -739,6 +741,8 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime archive extraction syntax check failed' } node --check config/scripts/ssh-relay-runtime-archive-extraction.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime archive extraction test syntax check failed' } + node --check config/scripts/ssh-relay-runtime-portable-archive.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'runtime portable archive test syntax check failed' } node --check config/scripts/ssh-relay-runtime-native-signing-stage-report.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime signing stage report syntax check failed' } node --check config/scripts/ssh-relay-runtime-macos-signing.mjs @@ -775,6 +779,7 @@ jobs: config/scripts/ssh-relay-node-pty-windows-settlement.test.mjs ` config/scripts/ssh-relay-node-zip-inspection.test.mjs ` config/scripts/ssh-relay-runtime-artifact.test.mjs ` + config/scripts/ssh-relay-runtime-portable-archive.test.mjs ` config/scripts/ssh-relay-runtime-baseline.test.mjs ` config/scripts/ssh-relay-runtime-build.test.mjs ` config/scripts/ssh-relay-runtime-linux-build-evidence.test.mjs ` @@ -1079,7 +1084,7 @@ jobs: set -euo pipefail evidence="$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}" mapfile -d '' identities < <(find "$evidence" -maxdepth 1 -type f -name '*.identity.json' -print0) - mapfile -d '' archives < <(find "$evidence" -maxdepth 1 -type f -name '*.tar.xz' -print0) + mapfile -d '' archives < <(find "$evidence" -maxdepth 1 -type f -name '*.tar.br' -print0) if (( ${#identities[@]} != 1 || ${#archives[@]} != 1 )); then printf 'runtime baseline evidence is ambiguous: identities=%s archives=%s\n' \ "${#identities[@]}" "${#archives[@]}" >&2 @@ -1088,8 +1093,10 @@ jobs: identity=${identities[0]} archive=${archives[0]} runtime="$RUNNER_TEMP/baseline-runtime/${{ matrix.tuple }}" - mkdir -p "$runtime" - tar -xJf "$archive" -C "$runtime" + node config/scripts/ssh-relay-runtime-archive-extraction.mjs \ + --identity "$identity" \ + --archive "$archive" \ + --output-directory "$runtime" node config/scripts/verify-ssh-relay-runtime.mjs \ --runtime-directory "$runtime" \ --identity "$identity" \ diff --git a/.github/workflows/ssh-relay-runtime-native-signing.yml b/.github/workflows/ssh-relay-runtime-native-signing.yml index b0d45edd69a..68a94a25633 100644 --- a/.github/workflows/ssh-relay-runtime-native-signing.yml +++ b/.github/workflows/ssh-relay-runtime-native-signing.yml @@ -63,8 +63,6 @@ jobs: shell: bash run: | set -euo pipefail - brew list xz >/dev/null 2>&1 || brew install xz - command -v xz test -x /usr/bin/codesign security help >/dev/null @@ -80,7 +78,7 @@ jobs: set -euo pipefail evidence="$RUNNER_TEMP/ssh-relay-runtime-build/${{ matrix.tuple }}" identity="$evidence/orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json" - archive=$(find "$evidence" -maxdepth 1 -type f -name 'orca-ssh-relay-runtime-v1-*.tar.xz' -print) + archive=$(find "$evidence" -maxdepth 1 -type f -name 'orca-ssh-relay-runtime-v1-*.tar.br' -print) test "$(printf '%s\n' "$archive" | sed '/^$/d' | wc -l | tr -d ' ')" = 1 runtime="$RUNNER_TEMP/ssh-relay-runtime-source/${{ matrix.tuple }}" node config/scripts/ssh-relay-runtime-archive-extraction.mjs \ diff --git a/config/scripts/ssh-relay-runtime-aggregate-input.mjs b/config/scripts/ssh-relay-runtime-aggregate-input.mjs index 6279593dcee..a328e652847 100644 --- a/config/scripts/ssh-relay-runtime-aggregate-input.mjs +++ b/config/scripts/ssh-relay-runtime-aggregate-input.mjs @@ -33,7 +33,7 @@ function assertExactFields(value, fields, label) { function expectedArchiveName(tupleId, contentId) { const digest = contentId.slice('sha256:'.length) - const extension = tupleId.startsWith('win32-') ? 'zip' : 'tar.xz' + const extension = tupleId.startsWith('win32-') ? 'zip' : 'tar.br' return `orca-ssh-relay-runtime-v1-${tupleId}-${digest}.${extension}` } diff --git a/config/scripts/ssh-relay-runtime-aggregate-input.test.mjs b/config/scripts/ssh-relay-runtime-aggregate-input.test.mjs index 727016e2753..dcb1e86b6ff 100644 --- a/config/scripts/ssh-relay-runtime-aggregate-input.test.mjs +++ b/config/scripts/ssh-relay-runtime-aggregate-input.test.mjs @@ -16,7 +16,7 @@ async function fixture() { temporaryDirectories.push(inputDirectory) const contentId = `sha256:${'a'.repeat(64)}` const bytes = Buffer.from('verified runtime bytes') - const name = `orca-ssh-relay-runtime-v1-linux-x64-glibc-${contentId.slice(7)}.tar.xz` + const name = `orca-ssh-relay-runtime-v1-linux-x64-glibc-${contentId.slice(7)}.tar.br` await writeFile(join(inputDirectory, name), bytes) return { inputDirectory, @@ -80,7 +80,7 @@ describe('SSH relay runtime aggregate inputs', () => { it('rejects duplicate tuples, names, and unsupported identities', async () => { const duplicateTuple = await fixture() - duplicateTuple.assets.push({ ...duplicateTuple.assets[0], name: 'different.tar.xz' }) + duplicateTuple.assets.push({ ...duplicateTuple.assets[0], name: 'different.tar.br' }) await expect(verifySshRelayRuntimeAggregateInputs(duplicateTuple)).rejects.toThrow( /duplicate tuple/i ) @@ -100,7 +100,7 @@ describe('SSH relay runtime aggregate inputs', () => { it('derives the archive name from tuple and content identity', async () => { const input = await fixture() - input.assets[0].name = 'latest.tar.xz' + input.assets[0].name = 'latest.tar.br' await expect(verifySshRelayRuntimeAggregateInputs(input)).rejects.toThrow(/archive name/i) }) diff --git a/config/scripts/ssh-relay-runtime-archive-extraction.mjs b/config/scripts/ssh-relay-runtime-archive-extraction.mjs index b0491aec047..e221a7714a1 100644 --- a/config/scripts/ssh-relay-runtime-archive-extraction.mjs +++ b/config/scripts/ssh-relay-runtime-archive-extraction.mjs @@ -1,14 +1,17 @@ -import { spawn } from 'node:child_process' import { createHash } from 'node:crypto' import { createReadStream } from 'node:fs' import { lstat, mkdir, realpath, rm } from 'node:fs/promises' import { basename, dirname, resolve } from 'node:path' import { pipeline } from 'node:stream/promises' import { pathToFileURL } from 'node:url' +import { createBrotliDecompress } from 'node:zlib' import { extract } from 'tar' -import { inspectSshRelayRuntimeArchive } from './ssh-relay-runtime-archive.mjs' +import { + inspectSshRelayRuntimeArchive, + SSH_RELAY_RUNTIME_POSIX_ARCHIVE_LIMITS +} from './ssh-relay-runtime-archive.mjs' import { assertSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closure.mjs' import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' import { readSshRelayRuntimeNativeSigningIdentity } from './ssh-relay-runtime-native-signing-plan.mjs' @@ -17,10 +20,9 @@ import { verifyRuntimeTree } from './verify-ssh-relay-runtime.mjs' const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024 const EXTRACTION_TIMEOUT_MS = 5 * 60_000 -const MAX_DIAGNOSTIC_BYTES = 64 * 1024 function expectedArchiveName(identity) { - const extension = identity.os === 'win32' ? 'zip' : 'tar.xz' + const extension = identity.os === 'win32' ? 'zip' : 'tar.br' return `orca-ssh-relay-runtime-v1-${identity.tupleId}-${identity.contentId.slice('sha256:'.length)}.${extension}` } @@ -97,27 +99,7 @@ async function assertOutputAbsent(outputDirectory) { throw new Error('Runtime archive extraction requires an exclusive output directory') } -async function extractTarXz(archivePath, outputDirectory, signal) { - const child = spawn('xz', ['--decompress', '--stdout', '--single-stream', '--', archivePath], { - stdio: ['ignore', 'pipe', 'pipe'], - windowsHide: true, - signal - }) - let stderr = '' - child.stderr.setEncoding('utf8') - child.stderr.on('data', (chunk) => { - stderr = `${stderr}${chunk}`.slice(-MAX_DIAGNOSTIC_BYTES) - }) - const completion = new Promise((resolvePromise, reject) => { - child.once('error', reject) - child.once('close', (code, closeSignal) => { - if (code === 0) { - resolvePromise() - } else { - reject(new Error(`xz extraction failed (${code ?? closeSignal ?? 'unknown'}): ${stderr}`)) - } - }) - }) +async function extractTarBrotli(archivePath, outputDirectory, signal) { const unpack = extract({ cwd: outputDirectory, strict: true, @@ -125,15 +107,16 @@ async function extractTarXz(archivePath, outputDirectory, signal) { noChmod: false, unlink: false }) - try { - await Promise.all([pipeline(child.stdout, unpack, { signal }), completion]) - } catch (error) { - if (child.exitCode === null && child.signalCode === null) { - child.kill() - } - await completion.catch(() => {}) - throw error - } + await pipeline( + createReadStream(archivePath, { + highWaterMark: SSH_RELAY_RUNTIME_POSIX_ARCHIVE_LIMITS.chunkBytes + }), + createBrotliDecompress({ + chunkSize: SSH_RELAY_RUNTIME_POSIX_ARCHIVE_LIMITS.chunkBytes + }), + unpack, + { signal } + ) } export async function extractSshRelayRuntimeArchive({ @@ -169,7 +152,7 @@ export async function extractSshRelayRuntimeArchive({ identity: finalIdentity, signal: effectiveSignal }) - : extractTarXz(absoluteArchive, physicalOutput, effectiveSignal)) + : extractTarBrotli(absoluteArchive, physicalOutput, effectiveSignal)) const tree = await verifyRuntimeTree(physicalOutput, finalIdentity, { signal: effectiveSignal }) const after = await describeArchive(absoluteArchive, identity.archive.size, effectiveSignal) if (!sameFileState(before.state, after.state) || before.sha256 !== after.sha256) { diff --git a/config/scripts/ssh-relay-runtime-archive.mjs b/config/scripts/ssh-relay-runtime-archive.mjs index 67a0bd4f9d1..62a2880bd05 100644 --- a/config/scripts/ssh-relay-runtime-archive.mjs +++ b/config/scripts/ssh-relay-runtime-archive.mjs @@ -1,95 +1,66 @@ -import { spawn } from 'node:child_process' import { createHash } from 'node:crypto' import { createReadStream, createWriteStream } from 'node:fs' -import { mkdtemp, rm, stat } from 'node:fs/promises' -import { tmpdir } from 'node:os' +import { rm, stat } from 'node:fs/promises' import { join } from 'node:path' import { pipeline } from 'node:stream/promises' +import { constants as zlibConstants, createBrotliCompress, createBrotliDecompress } from 'node:zlib' import { create, Parser } from 'tar' import { createSshRelayRuntimeZip, inspectSshRelayRuntimeZip } from './ssh-relay-runtime-zip.mjs' const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024 -const MAX_DIAGNOSTIC_BYTES = 64 * 1024 -const XZ_TIMEOUT_MS = 5 * 60 * 1000 +const ARCHIVE_TIMEOUT_MS = 5 * 60 * 1000 +const BROTLI_CHUNK_BYTES = 64 * 1024 +const BROTLI_QUALITY = 9 +const BROTLI_WINDOW_BITS = 20 function archiveName(tuple, contentId) { const match = /^sha256:([0-9a-f]{64})$/.exec(contentId) if (!match) { throw new Error('Runtime content identity is not a SHA-256 digest') } - const suffix = tuple.startsWith('win32-') ? 'zip' : 'tar.xz' + const suffix = tuple.startsWith('win32-') ? 'zip' : 'tar.br' return `orca-ssh-relay-runtime-v1-${tuple}-${match[1]}.${suffix}` } -async function compressTar(tarPath, archivePath, signal) { - const child = spawn( - 'xz', - ['--compress', '--stdout', '--threads=1', '--check=crc64', '-9e', '--', tarPath], - { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, signal } +async function compressRuntimeTree({ + runtimeRoot, + paths, + sourceDateEpoch, + archivePath, + signal, + markOutputCreated +}) { + const archiveOutput = createWriteStream(archivePath, { flags: 'wx', mode: 0o600 }) + archiveOutput.once('open', markOutputCreated) + const tarStream = create( + { + cwd: runtimeRoot, + portable: true, + noPax: true, + noDirRecurse: true, + mtime: new Date(sourceDateEpoch * 1000) + }, + paths ) - let stderr = '' - child.stderr.setEncoding('utf8') - child.stderr.on('data', (chunk) => { - stderr = `${stderr}${chunk}`.slice(-MAX_DIAGNOSTIC_BYTES) - }) - const completion = new Promise((resolve, reject) => { - child.once('error', reject) - child.once('close', (code, closeSignal) => { - if (code === 0) { - resolve() - } else { - reject(new Error(`xz failed (${code ?? closeSignal ?? 'unknown'}): ${stderr.trim()}`)) - } - }) - }) - try { - await Promise.all([ - pipeline(child.stdout, createWriteStream(archivePath, { flags: 'wx', mode: 0o600 }), { - signal - }), - completion - ]) - } catch (error) { - if (child.exitCode === null && child.signalCode === null) { - child.kill() + const compression = createBrotliCompress({ + chunkSize: BROTLI_CHUNK_BYTES, + params: { + [zlibConstants.BROTLI_PARAM_QUALITY]: BROTLI_QUALITY, + [zlibConstants.BROTLI_PARAM_LGWIN]: BROTLI_WINDOW_BITS } - await completion.catch(() => {}) - throw error - } + }) + await pipeline(tarStream, compression, archiveOutput, { signal }) } async function decompressArchive(archivePath, destination, signal) { - const child = spawn('xz', ['--decompress', '--stdout', '--single-stream', '--', archivePath], { - stdio: ['ignore', 'pipe', 'pipe'], - windowsHide: true, - signal - }) - let stderr = '' - child.stderr.setEncoding('utf8') - child.stderr.on('data', (chunk) => { - stderr = `${stderr}${chunk}`.slice(-MAX_DIAGNOSTIC_BYTES) - }) - const completion = new Promise((resolve, reject) => { - child.once('error', reject) - child.once('close', (code, closeSignal) => { - if (code === 0) { - resolve() - } else { - reject(new Error(`xz failed (${code ?? closeSignal ?? 'unknown'}): ${stderr.trim()}`)) - } - }) - }) - try { - await Promise.all([pipeline(child.stdout, destination, { signal }), completion]) - } catch (error) { - if (child.exitCode === null && child.signalCode === null) { - child.kill() - } - await completion.catch(() => {}) - throw error - } + await pipeline( + createReadStream(archivePath, { highWaterMark: BROTLI_CHUNK_BYTES }), + createBrotliDecompress({ chunkSize: BROTLI_CHUNK_BYTES }), + destination, + { signal } + ) } export async function createSshRelayRuntimeArchive({ @@ -111,25 +82,25 @@ export async function createSshRelayRuntimeArchive({ if (!Number.isSafeInteger(sourceDateEpoch) || sourceDateEpoch < 0) { throw new Error('Runtime archive SOURCE_DATE_EPOCH must be a non-negative safe integer') } - const temporaryDirectory = await mkdtemp(join(tmpdir(), 'orca-runtime-tar-')) - const tarPath = join(temporaryDirectory, 'runtime.tar') const name = archiveName(identity.tupleId, identity.contentId) const archivePath = join(outputDirectory, name) - const effectiveSignal = signal ?? AbortSignal.timeout(XZ_TIMEOUT_MS) + const timeoutSignal = AbortSignal.timeout(ARCHIVE_TIMEOUT_MS) + const effectiveSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal + effectiveSignal.throwIfAborted() + let outputCreated = false try { const paths = identity.entries.map((entry) => entry.path).sort() - await create( - { - cwd: runtimeRoot, - file: tarPath, - portable: true, - noPax: true, - noDirRecurse: true, - mtime: new Date(sourceDateEpoch * 1000) - }, - paths - ) - await compressTar(tarPath, archivePath, effectiveSignal) + await compressRuntimeTree({ + runtimeRoot, + paths, + sourceDateEpoch, + archivePath, + signal: effectiveSignal, + // Why: an exclusive-open failure must never delete an output owned by another build. + markOutputCreated: () => { + outputCreated = true + } + }) const metadata = await stat(archivePath) if (!metadata.isFile() || metadata.size === 0 || metadata.size > MAX_ARCHIVE_BYTES) { throw new Error('Runtime archive exceeds the release-manifest compressed-size limit') @@ -145,10 +116,10 @@ export async function createSshRelayRuntimeArchive({ sha256: `sha256:${digest.digest('hex')}` } } catch (error) { - await rm(archivePath, { force: true }) + if (outputCreated) { + await rm(archivePath, { force: true }) + } throw error - } finally { - await rm(temporaryDirectory, { recursive: true, force: true }) } } @@ -202,7 +173,10 @@ export async function inspectSshRelayRuntimeArchive(archivePath, identity, { sig parser.abort(error) } }) - await decompressArchive(archivePath, parser, signal ?? AbortSignal.timeout(XZ_TIMEOUT_MS)) + const timeoutSignal = AbortSignal.timeout(ARCHIVE_TIMEOUT_MS) + const effectiveSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal + effectiveSignal.throwIfAborted() + await decompressArchive(archivePath, parser, effectiveSignal) await Promise.all(pendingFiles) const missing = [...expected.keys()].filter((path) => !seen.has(path)) if (missing.length > 0) { @@ -214,3 +188,9 @@ export async function inspectSshRelayRuntimeArchive(archivePath, identity, { sig expandedBytes: identity.expandedSize } } + +export const SSH_RELAY_RUNTIME_POSIX_ARCHIVE_LIMITS = Object.freeze({ + chunkBytes: BROTLI_CHUNK_BYTES, + quality: BROTLI_QUALITY, + windowBits: BROTLI_WINDOW_BITS +}) diff --git a/config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs b/config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs index dd1a52ca0c3..44f3afef6e4 100644 --- a/config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs +++ b/config/scripts/ssh-relay-runtime-draft-readback-materialization.test.mjs @@ -11,7 +11,7 @@ const REPO = 'stablyai/orca' const RELEASE_ID = 42 const TAG = 'v1.4.140-rc.1' const TOKEN = 'secret-token' -const NAME = 'orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.xz' +const NAME = 'orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.br' const BYTES = Buffer.from('immutable release bytes') const SECOND_NAME = 'orca-ssh-relay-runtime-manifest.json' const SECOND_BYTES = Buffer.from('immutable manifest bytes') diff --git a/config/scripts/ssh-relay-runtime-draft-readback.test.mjs b/config/scripts/ssh-relay-runtime-draft-readback.test.mjs index 5903acd5ef2..10c88a2353e 100644 --- a/config/scripts/ssh-relay-runtime-draft-readback.test.mjs +++ b/config/scripts/ssh-relay-runtime-draft-readback.test.mjs @@ -7,7 +7,7 @@ const REPO = 'stablyai/orca' const TOKEN = 'secret-token' const bytes = Buffer.from('immutable release bytes') const expected = { - name: 'orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.xz', + name: 'orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.br', sha256: 'sha256:5396515749878bd28c5dae110040b4fbae1c33f59318b9c23f446319d68e236a', size: bytes.length } diff --git a/config/scripts/ssh-relay-runtime-draft-recovery.test.mjs b/config/scripts/ssh-relay-runtime-draft-recovery.test.mjs index 0567062b053..479bfafcc8b 100644 --- a/config/scripts/ssh-relay-runtime-draft-recovery.test.mjs +++ b/config/scripts/ssh-relay-runtime-draft-recovery.test.mjs @@ -11,7 +11,7 @@ function asset(name, digit) { function fixture() { const expectedAssets = [ - asset('orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.xz', '1'), + asset('orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.br', '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') diff --git a/config/scripts/ssh-relay-runtime-draft-release-verification.mjs b/config/scripts/ssh-relay-runtime-draft-release-verification.mjs index aa60f962ee2..da073241c22 100644 --- a/config/scripts/ssh-relay-runtime-draft-release-verification.mjs +++ b/config/scripts/ssh-relay-runtime-draft-release-verification.mjs @@ -11,7 +11,7 @@ const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u const REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u const TAG_PATTERN = /^v\d+\.\d+\.\d+(?:-rc\.\d+(?:\.[0-9A-Za-z]+)?)?$/u const TUPLE_PATTERN = /^(?:linux-(?:x64|arm64)-glibc|darwin-(?:x64|arm64)|win32-(?:x64|arm64))$/u -const RUNTIME_ARCHIVE_PATTERN = /^orca-ssh-relay-runtime-v1-.+\.(?:tar\.xz|zip)$/u +const RUNTIME_ARCHIVE_PATTERN = /^orca-ssh-relay-runtime-v1-.+\.(?:tar\.br|zip)$/u const MANAGED_ASSET_PATTERN = /^orca-ssh-relay-runtime-[A-Za-z0-9._-]+$/u const MAX_ASSET_BYTES = 100 * 1024 * 1024 const MAX_ASSETS = 26 diff --git a/config/scripts/ssh-relay-runtime-draft-release-verification.test.mjs b/config/scripts/ssh-relay-runtime-draft-release-verification.test.mjs index e6b2e71cf82..672e2e4aed9 100644 --- a/config/scripts/ssh-relay-runtime-draft-release-verification.test.mjs +++ b/config/scripts/ssh-relay-runtime-draft-release-verification.test.mjs @@ -12,7 +12,7 @@ const RELEASE_ID = 42 const TAG = 'v1.5.0-rc.1' const SOURCE_COMMIT = 'a'.repeat(40) const TOKEN = 'test-token' -const ARCHIVE_NAME = 'orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.xz' +const ARCHIVE_NAME = 'orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.br' const ARCHIVE_BYTES = Buffer.from('verified runtime archive') const SECOND_ARCHIVE_NAME = 'orca-ssh-relay-runtime-v1-win32-x64-c.zip' const SECOND_ARCHIVE_BYTES = Buffer.from('verified Windows runtime archive') diff --git a/config/scripts/ssh-relay-runtime-draft-upload.test.mjs b/config/scripts/ssh-relay-runtime-draft-upload.test.mjs index 3de52f73284..bc85d711b79 100644 --- a/config/scripts/ssh-relay-runtime-draft-upload.test.mjs +++ b/config/scripts/ssh-relay-runtime-draft-upload.test.mjs @@ -12,7 +12,7 @@ const RELEASE_ID = 42 const TAG = 'v1.5.0-rc.1' const SOURCE_COMMIT = 'a'.repeat(40) const TOKEN = 'test-token' -const NAME = 'orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.xz' +const NAME = 'orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.br' const BYTES = Buffer.from('immutable relay runtime bytes') const SECOND_NAME = 'orca-ssh-relay-runtime-manifest.json' const SECOND_BYTES = Buffer.from('immutable signed manifest bytes') diff --git a/config/scripts/ssh-relay-runtime-macos-signature-verification.test.mjs b/config/scripts/ssh-relay-runtime-macos-signature-verification.test.mjs index d3903158359..a3d78291636 100644 --- a/config/scripts/ssh-relay-runtime-macos-signature-verification.test.mjs +++ b/config/scripts/ssh-relay-runtime-macos-signature-verification.test.mjs @@ -333,7 +333,7 @@ describe('SSH relay runtime macOS signature verification', () => { } } const staleArchive = structuredClone(fixture.finalIdentity) - staleArchive.archive = { fileName: 'unsigned.tar.xz' } + staleArchive.archive = { fileName: 'unsigned.tar.br' } await expect( verifySshRelayRuntimeMacosSignatures({ ...common, finalIdentity: staleArchive }) ).rejects.toThrow(/does not match its unsigned source/i) diff --git a/config/scripts/ssh-relay-runtime-manifest-aggregate-command.test.mjs b/config/scripts/ssh-relay-runtime-manifest-aggregate-command.test.mjs index 16e9fb78f7c..5a752dc5924 100644 --- a/config/scripts/ssh-relay-runtime-manifest-aggregate-command.test.mjs +++ b/config/scripts/ssh-relay-runtime-manifest-aggregate-command.test.mjs @@ -133,7 +133,7 @@ function tupleFor(tupleId) { tuple.metadataAssets.sbom.name = `orca-ssh-relay-runtime-${tupleId}.spdx.json` tuple.metadataAssets.provenance.name = `orca-ssh-relay-runtime-${tupleId}.provenance.json` tuple.contentId = computeSshRelayRuntimeContentId(tuple) - const extension = os === 'win32' ? 'zip' : 'tar.xz' + const extension = os === 'win32' ? 'zip' : 'tar.br' tuple.archive.name = `orca-ssh-relay-runtime-v1-${tupleId}-${tuple.contentId.slice(7)}.${extension}` return tuple } diff --git a/config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs b/config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs index 63df8669147..ca99fdb86d1 100644 --- a/config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs +++ b/config/scripts/ssh-relay-runtime-manifest-aggregate.test.mjs @@ -42,7 +42,7 @@ function arm64TupleFrom(source) { tuple.metadataAssets.sbom.name = 'orca-ssh-relay-runtime-linux-arm64-glibc.spdx.json' tuple.metadataAssets.provenance.name = 'orca-ssh-relay-runtime-linux-arm64-glibc.provenance.json' tuple.contentId = computeSshRelayRuntimeContentId(tuple) - tuple.archive.name = `orca-ssh-relay-runtime-v1-${tuple.tupleId}-${tuple.contentId.slice('sha256:'.length)}.tar.xz` + tuple.archive.name = `orca-ssh-relay-runtime-v1-${tuple.tupleId}-${tuple.contentId.slice('sha256:'.length)}.tar.br` return tuple } diff --git a/config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs b/config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs index c5b2bce8f9c..69c6293d994 100644 --- a/config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs +++ b/config/scripts/ssh-relay-runtime-manifest-assembly.test.mjs @@ -30,7 +30,7 @@ function arm64TupleFrom(source) { tuple.metadataAssets.sbom.name = 'orca-ssh-relay-runtime-linux-arm64-glibc.spdx.json' tuple.metadataAssets.provenance.name = 'orca-ssh-relay-runtime-linux-arm64-glibc.provenance.json' tuple.contentId = computeSshRelayRuntimeContentId(tuple) - tuple.archive.name = `orca-ssh-relay-runtime-v1-${tuple.tupleId}-${tuple.contentId.slice('sha256:'.length)}.tar.xz` + tuple.archive.name = `orca-ssh-relay-runtime-v1-${tuple.tupleId}-${tuple.contentId.slice('sha256:'.length)}.tar.br` return tuple } @@ -40,10 +40,10 @@ describe('SSH relay runtime canonical manifest assembly', () => { expect(assembled.canonicalBytes.at(-1)).not.toBe('\n'.charCodeAt(0)) expect(createHash('sha256').update(assembled.canonicalBytes).digest('hex')).toBe( - 'e78bf4416628a91055035dc7926035cbf633f29d3618be34e041c6dc5e0794fb' + 'dc36bff51330eb3aa4791bf30c25eeedd6e8d4cbc57470d50f377c24e0a5ed06' ) expect(assembled.sha256).toBe( - 'sha256:e78bf4416628a91055035dc7926035cbf633f29d3618be34e041c6dc5e0794fb' + 'sha256:dc36bff51330eb3aa4791bf30c25eeedd6e8d4cbc57470d50f377c24e0a5ed06' ) expect(parseCanonicalSshRelayRuntimeManifestBytes(assembled.canonicalBytes)).toEqual( assembled.manifest diff --git a/config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs b/config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs index 91b93ad487a..31d0946062c 100644 --- a/config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs +++ b/config/scripts/ssh-relay-runtime-manifest-signing-handoff.test.mjs @@ -50,7 +50,7 @@ describe('SSH relay runtime manifest signing handoff', () => { ]) expect(request).toMatchObject({ algorithm: 'ed25519-v1', - payloadSha256: 'sha256:e78bf4416628a91055035dc7926035cbf633f29d3618be34e041c6dc5e0794fb', + payloadSha256: 'sha256:dc36bff51330eb3aa4791bf30c25eeedd6e8d4cbc57470d50f377c24e0a5ed06', payloadSize: canonical.length }) expect(request.canonicalBytes).toEqual(canonical) diff --git a/config/scripts/ssh-relay-runtime-manifest-tuple.mjs b/config/scripts/ssh-relay-runtime-manifest-tuple.mjs index 8755986c57e..ea770e0aa48 100644 --- a/config/scripts/ssh-relay-runtime-manifest-tuple.mjs +++ b/config/scripts/ssh-relay-runtime-manifest-tuple.mjs @@ -57,7 +57,7 @@ async function physicalDirectory(path, label) { function expectedNames(identity) { const digest = identity.contentId.slice('sha256:'.length) - const extension = identity.tupleId.startsWith('win32-') ? 'zip' : 'tar.xz' + const extension = identity.tupleId.startsWith('win32-') ? 'zip' : 'tar.br' const prefix = `orca-ssh-relay-runtime-${identity.tupleId}` return { archive: `orca-ssh-relay-runtime-v1-${identity.tupleId}-${digest}.${extension}`, diff --git a/config/scripts/ssh-relay-runtime-manifest-validation.mjs b/config/scripts/ssh-relay-runtime-manifest-validation.mjs index 2bbf6d18d04..ed8d672a97e 100644 --- a/config/scripts/ssh-relay-runtime-manifest-validation.mjs +++ b/config/scripts/ssh-relay-runtime-manifest-validation.mjs @@ -197,7 +197,7 @@ function expectedTupleId(tuple) { } function expectedArchiveName(tuple) { - const extension = tuple.os === 'win32' ? 'zip' : 'tar.xz' + const extension = tuple.os === 'win32' ? 'zip' : 'tar.br' return `orca-ssh-relay-runtime-v1-${tuple.tupleId}-${tuple.contentId.slice(7)}.${extension}` } diff --git a/config/scripts/ssh-relay-runtime-native-signing-workflow.test.mjs b/config/scripts/ssh-relay-runtime-native-signing-workflow.test.mjs index cab01b06900..524fff1a1a1 100644 --- a/config/scripts/ssh-relay-runtime-native-signing-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-native-signing-workflow.test.mjs @@ -69,6 +69,9 @@ describe('SSH relay runtime native signing workflow', () => { expect(actionRefs(source).every((ref) => /^[0-9a-f]{40}$/u.test(ref))).toBe(true) expect(source).not.toContain('continue-on-error') expect(source).not.toMatch(/\b(?:gh\s+release|softprops\/action-gh-release)\b/u) + expect(source).toContain('orca-ssh-relay-runtime-v1-*.tar.br') + expect(source).not.toContain('brew install xz') + expect(source).not.toContain('command -v xz') expect(source.match(/ssh-relay-runtime-archive-extraction\.mjs/g)).toHaveLength(2) expect(source.match(/ssh-relay-runtime-native-signing-stage\.mjs/g)).toHaveLength(2) expect(source.match(/ssh-relay-runtime-native-signing-finalization\.mjs/g)).toHaveLength(2) diff --git a/config/scripts/ssh-relay-runtime-portable-archive.test.mjs b/config/scripts/ssh-relay-runtime-portable-archive.test.mjs new file mode 100644 index 00000000000..3698a25be9a --- /dev/null +++ b/config/scripts/ssh-relay-runtime-portable-archive.test.mjs @@ -0,0 +1,156 @@ +import { createHash } from 'node:crypto' +import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import * as runtimeArchive from './ssh-relay-runtime-archive.mjs' +import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' + +const temporaryDirectories = [] + +function digest(bytes) { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +function archiveName(identity) { + return `orca-ssh-relay-runtime-v1-${identity.tupleId}-${identity.contentId.slice('sha256:'.length)}.tar.br` +} + +async function fixture() { + const root = await mkdtemp(join(tmpdir(), 'ssh-relay-portable-archive-')) + temporaryDirectories.push(root) + const runtimeRoot = join(root, 'runtime') + const outputDirectory = join(root, 'output') + const nodeBytes = Buffer.from('portable bundled node') + await Promise.all([mkdir(join(runtimeRoot, 'bin'), { recursive: true }), mkdir(outputDirectory)]) + await writeFile(join(runtimeRoot, 'bin', 'node'), nodeBytes) + await chmod(join(runtimeRoot, 'bin', 'node'), 0o755) + const base = { + tupleId: 'darwin-arm64', + os: 'darwin', + architecture: 'arm64', + compatibility: { kind: 'darwin', minimumVersion: '13.5' }, + nodeVersion: '24.18.0', + dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, + entries: [ + { path: 'bin', type: 'directory', mode: 0o755 }, + { + path: 'bin/node', + type: 'file', + role: 'node', + size: nodeBytes.length, + mode: 0o755, + sha256: digest(nodeBytes) + } + ], + fileCount: 1, + expandedSize: nodeBytes.length + } + return { + runtimeRoot, + outputDirectory, + identity: { ...base, contentId: computeSshRelayRuntimeContentId(base) } + } +} + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })) + ) +}) + +describe('SSH relay portable POSIX archive', () => { + it.skipIf(process.platform === 'win32')( + 'creates deterministic Brotli archives and verifies their exact entries', + async () => { + const value = await fixture() + const secondOutput = join(value.outputDirectory, 'second') + await mkdir(secondOutput) + + const first = await runtimeArchive.createSshRelayRuntimeArchive({ + ...value, + sourceDateEpoch: 1_788_739_200 + }) + const second = await runtimeArchive.createSshRelayRuntimeArchive({ + runtimeRoot: value.runtimeRoot, + outputDirectory: secondOutput, + identity: value.identity, + sourceDateEpoch: 1_788_739_200 + }) + + expect(first.name).toMatch(/\.tar\.br$/u) + expect(await readFile(first.path)).toEqual(await readFile(second.path)) + await expect( + runtimeArchive.inspectSshRelayRuntimeArchive(first.path, value.identity) + ).resolves.toEqual({ entries: 2, files: 1, expandedBytes: value.identity.expandedSize }) + } + ) + + it('declares the bounded architecture-independent compression contract', () => { + expect(runtimeArchive.SSH_RELAY_RUNTIME_POSIX_ARCHIVE_LIMITS).toEqual({ + chunkBytes: 64 * 1024, + quality: 9, + windowBits: 20 + }) + }) + + it.skipIf(process.platform === 'win32')( + 'rejects truncated Brotli and preserves outputs it did not create', + async () => { + const truncated = await fixture() + const archive = await runtimeArchive.createSshRelayRuntimeArchive({ + ...truncated, + sourceDateEpoch: 1_788_739_200 + }) + const bytes = await readFile(archive.path) + await writeFile(archive.path, bytes.subarray(0, -1)) + await expect( + runtimeArchive.inspectSshRelayRuntimeArchive(archive.path, truncated.identity) + ).rejects.toThrow() + + const existing = await fixture() + const existingPath = join(existing.outputDirectory, archiveName(existing.identity)) + await writeFile(existingPath, 'owned by another build') + await expect( + runtimeArchive.createSshRelayRuntimeArchive({ + ...existing, + sourceDateEpoch: 1_788_739_200 + }) + ).rejects.toMatchObject({ code: 'EEXIST' }) + await expect(readFile(existingPath, 'utf8')).resolves.toBe('owned by another build') + } + ) + + it.skipIf(process.platform === 'win32')( + 'settles pre-cancellation without retaining a partial archive', + async () => { + const value = await fixture() + await expect( + runtimeArchive.createSshRelayRuntimeArchive({ + ...value, + sourceDateEpoch: 1_788_739_200, + signal: AbortSignal.abort() + }) + ).rejects.toThrow(/abort/i) + await expect( + readFile(join(value.outputDirectory, archiveName(value.identity))) + ).rejects.toMatchObject({ code: 'ENOENT' }) + } + ) + + it('does not depend on a client-native or system XZ executable', async () => { + const source = await readFile( + new URL('./ssh-relay-runtime-archive.mjs', import.meta.url), + 'utf8' + ) + + expect(source).not.toContain("from 'node:child_process'") + expect(source).not.toContain("spawn('xz'") + expect(source).toContain('createBrotliCompress') + expect(source).toContain('createBrotliDecompress') + }) +}) diff --git a/config/scripts/ssh-relay-runtime-post-sign-metadata.mjs b/config/scripts/ssh-relay-runtime-post-sign-metadata.mjs index c06f4e11c3f..016e3a90194 100644 --- a/config/scripts/ssh-relay-runtime-post-sign-metadata.mjs +++ b/config/scripts/ssh-relay-runtime-post-sign-metadata.mjs @@ -37,7 +37,7 @@ async function physicalDirectory(path, label) { function expectedNames(identity) { const prefix = `orca-ssh-relay-runtime-${identity.tupleId}` - const extension = identity.os === 'win32' ? 'zip' : 'tar.xz' + const extension = identity.os === 'win32' ? 'zip' : 'tar.br' return { archive: `orca-ssh-relay-runtime-v1-${identity.tupleId}-${identity.contentId.slice('sha256:'.length)}.${extension}`, sbom: `${prefix}.spdx.json`, diff --git a/config/scripts/ssh-relay-runtime-provenance.test.mjs b/config/scripts/ssh-relay-runtime-provenance.test.mjs index 112c1a06d0e..8a12d6422fd 100644 --- a/config/scripts/ssh-relay-runtime-provenance.test.mjs +++ b/config/scripts/ssh-relay-runtime-provenance.test.mjs @@ -44,7 +44,7 @@ function fixture() { tuple, identity, archive: { - name: `orca-ssh-relay-runtime-v1-${tuple}-${'f'.repeat(64)}.tar.xz`, + name: `orca-ssh-relay-runtime-v1-${tuple}-${'f'.repeat(64)}.tar.br`, size: 123, sha256: sha256('a') }, diff --git a/config/scripts/ssh-relay-runtime-readback-archive-execution.test.mjs b/config/scripts/ssh-relay-runtime-readback-archive-execution.test.mjs index db7c6589601..caaf82e140b 100644 --- a/config/scripts/ssh-relay-runtime-readback-archive-execution.test.mjs +++ b/config/scripts/ssh-relay-runtime-readback-archive-execution.test.mjs @@ -20,7 +20,7 @@ import { parseSshRelayRuntimeReadbackArchiveExecutionArguments } from './ssh-relay-runtime-readback-archive-execution.mjs' -const ARCHIVE_NAME = 'orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.xz' +const ARCHIVE_NAME = 'orca-ssh-relay-runtime-v1-linux-x64-glibc-a.tar.br' const ARCHIVE_BYTES = Buffer.from('verified materialized archive bytes') const ARCHIVE_SHA256 = `sha256:${createHash('sha256').update(ARCHIVE_BYTES).digest('hex')}` diff --git a/config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs b/config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs index 7aeddfcbe51..dcccab46ddb 100644 --- a/config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs +++ b/config/scripts/ssh-relay-runtime-release-stage-gate.test.mjs @@ -37,7 +37,7 @@ function stage(id, output, overrides = {}) { } function successfulFixture() { - const linux = asset(LINUX, 'orca-ssh-relay-runtime-linux.tar.xz', SHA.linux, 'a') + const linux = asset(LINUX, 'orca-ssh-relay-runtime-linux.tar.br', SHA.linux, 'a') const windowsUnsigned = asset( WINDOWS, 'orca-ssh-relay-runtime-windows-unsigned.zip', @@ -111,7 +111,7 @@ describe('SSH relay runtime release stage gate', () => { 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-linux.tar.br', 'orca-ssh-relay-runtime-windows-signed.zip', 'orca-ssh-relay-runtime-manifest.json', 'orca-ssh-relay-runtime-manifest.sig' diff --git a/config/scripts/ssh-relay-runtime-reproducibility.test.mjs b/config/scripts/ssh-relay-runtime-reproducibility.test.mjs index d14e9a0b752..456bce7d9cc 100644 --- a/config/scripts/ssh-relay-runtime-reproducibility.test.mjs +++ b/config/scripts/ssh-relay-runtime-reproducibility.test.mjs @@ -12,7 +12,7 @@ const tuple = 'linux-x64-glibc' const contentId = `sha256:${'a'.repeat(64)}` const archiveBytes = 'archive bytes' const archiveSha256 = `sha256:${createHash('sha256').update(archiveBytes).digest('hex')}` -const archiveName = `orca-ssh-relay-runtime-v1-${tuple}-${contentId.slice('sha256:'.length)}.tar.xz` +const archiveName = `orca-ssh-relay-runtime-v1-${tuple}-${contentId.slice('sha256:'.length)}.tar.br` afterEach(async () => { await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true }))) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index a6455ec0457..1a2a6de185d 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -232,7 +232,10 @@ describe('SSH relay runtime artifact workflow', () => { ) expect(source).not.toContain('work_directory="$output_root/build-work"') expect(source).not.toContain("Join-Path $outputRoot 'build-work'") - expect(source).toContain('cp "$first_output"/*.tar.xz') + expect(source).toContain('cp "$first_output"/*.tar.br') + expect(source).toContain("-name '*.tar.br'") + expect(source).not.toContain('tar -xJf "$archive"') + expect(source.match(/ssh-relay-runtime-portable-archive\.test\.mjs/g)).toHaveLength(4) expect(source).toContain("Get-ChildItem -LiteralPath $firstOutput -Filter '*.zip'") expect(source).toContain('ssh-relay-node-zip-inspection.test.mjs') expect(source).toContain('ssh-relay-runtime-pty-smoke.test.mjs') diff --git a/src/main/ssh/ssh-relay-artifact-schema.test.ts b/src/main/ssh/ssh-relay-artifact-schema.test.ts index ef004d3655c..bb4ecfa36fe 100644 --- a/src/main/ssh/ssh-relay-artifact-schema.test.ts +++ b/src/main/ssh/ssh-relay-artifact-schema.test.ts @@ -336,7 +336,7 @@ describe('SSH relay artifact manifest schema', () => { expect(() => parseSshRelayArtifactManifest(identityMismatch)).toThrow(/content identity/i) const nameMismatch = createSshRelayArtifactTestManifest() - nameMismatch.tuples[0].archive.name = 'latest.tar.xz' + nameMismatch.tuples[0].archive.name = 'latest.tar.br' expect(() => parseSshRelayArtifactManifest(nameMismatch)).toThrow(/archive name/i) }) diff --git a/src/main/ssh/ssh-relay-manifest-signature.test.ts b/src/main/ssh/ssh-relay-manifest-signature.test.ts index 8516579e104..1ff71cc9f7c 100644 --- a/src/main/ssh/ssh-relay-manifest-signature.test.ts +++ b/src/main/ssh/ssh-relay-manifest-signature.test.ts @@ -50,7 +50,7 @@ describe('SSH relay manifest signatures', () => { const canonical = canonicalUnsignedSshRelayManifestBytes(manifest) expect(canonicalUnsignedSshRelayManifestBytes(reordered)).toEqual(canonical) expect(createHash('sha256').update(canonical).digest('hex')).toBe( - 'e78bf4416628a91055035dc7926035cbf633f29d3618be34e041c6dc5e0794fb' + 'dc36bff51330eb3aa4791bf30c25eeedd6e8d4cbc57470d50f377c24e0a5ed06' ) }) @@ -95,7 +95,7 @@ describe('SSH relay manifest signatures', () => { ]) expect(() => { - Object.defineProperty(verified.tuples[0].archive, 'name', { value: 'latest.tar.xz' }) + Object.defineProperty(verified.tuples[0].archive, 'name', { value: 'latest.tar.br' }) }).toThrow(TypeError) }) diff --git a/src/main/ssh/ssh-relay-release-asset.test.ts b/src/main/ssh/ssh-relay-release-asset.test.ts index 1ec020bb24d..d7a5bdd938f 100644 --- a/src/main/ssh/ssh-relay-release-asset.test.ts +++ b/src/main/ssh/ssh-relay-release-asset.test.ts @@ -25,7 +25,7 @@ describe('SSH relay release asset identity', () => { it('derives a content-qualified archive name and exact direct URL', () => { const name = sshRelayRuntimeArchiveName('linux-x64-glibc', contentId) - expect(name).toBe(`orca-ssh-relay-runtime-v1-linux-x64-glibc-${'a'.repeat(64)}.tar.xz`) + expect(name).toBe(`orca-ssh-relay-runtime-v1-linux-x64-glibc-${'a'.repeat(64)}.tar.br`) expect(sshRelayRuntimeDownloadUrl('v1.2.3-rc.4', name)).toBe( `https://github.com/stablyai/orca/releases/download/v1.2.3-rc.4/${name}` ) diff --git a/src/main/ssh/ssh-relay-release-asset.ts b/src/main/ssh/ssh-relay-release-asset.ts index 1305507a11b..16bb82e9cbe 100644 --- a/src/main/ssh/ssh-relay-release-asset.ts +++ b/src/main/ssh/ssh-relay-release-asset.ts @@ -33,13 +33,13 @@ export function sshRelayRuntimeArchiveName( if (!match) { throw new Error(`Invalid SSH relay runtime content identity: ${contentId}`) } - const extension = tupleId.startsWith('win32-') ? 'zip' : 'tar.xz' + const extension = tupleId.startsWith('win32-') ? 'zip' : 'tar.br' return `orca-ssh-relay-runtime-v1-${tupleId}-${match[1]}.${extension}` } export function sshRelayRuntimeDownloadUrl(tag: string, archiveName: string): string { parseSshRelayReleaseTag(tag) - if (!/^orca-ssh-relay-runtime-v1-[A-Za-z0-9.-]+\.(?:tar\.xz|zip)$/.test(archiveName)) { + if (!/^orca-ssh-relay-runtime-v1-[A-Za-z0-9.-]+\.(?:tar\.br|zip)$/.test(archiveName)) { throw new Error(`Invalid SSH relay runtime archive name: ${archiveName}`) } return `https://github.com/stablyai/orca/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(archiveName)}` diff --git a/src/main/ssh/ssh-relay-runtime-identity.test.ts b/src/main/ssh/ssh-relay-runtime-identity.test.ts index d86759021b0..f270edb63f4 100644 --- a/src/main/ssh/ssh-relay-runtime-identity.test.ts +++ b/src/main/ssh/ssh-relay-runtime-identity.test.ts @@ -49,7 +49,7 @@ describe('SSH relay runtime content identity', () => { const manifest = createSshRelayArtifactTestManifest() const tuple = structuredClone(manifest.tuples[0]) tuple.entries.reverse() - tuple.archive.name = 'ignored-by-runtime-identity.tar.xz' + tuple.archive.name = 'ignored-by-runtime-identity.tar.br' tuple.archive.sha256 = `sha256:${'f'.repeat(64)}` tuple.metadataAssets.sbom.sha256 = `sha256:${'e'.repeat(64)}` tuple.nativeVerification.verifiedAt = '2030-01-01T00:00:00.000Z' From cdb318df339941325a1055d841c6ae7d5484dfaf Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 16:33:50 -0400 Subject: [PATCH 161/267] docs: record portable relay archive proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 9 +- ...github-release-implementation-checklist.md | 108 ++++++++++++++++-- 2 files changed, 103 insertions(+), 14 deletions(-) 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 8f11686bd93..1e7ff4afe2e 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 @@ -18,7 +18,10 @@ is red only after Windows arm64 passes smoke and rejects hosted build 26200 agai 26100 floor. `E-M5-ARCHIVE-PORTABILITY-AUDIT-001` proves the unpublished `.tar.xz` output cannot meet the cross-client 64 MiB extraction contract and selects bounded Node-native `.tar.br`. Replace the archive contracts and all-six artifact evidence before the isolated extraction/cache-staging -RED. Desktop consumers, SSH transfer/install, mode wiring, +RED. The purpose-named missing-contract RED is recorded under +`E-M5-PORTABLE-ARCHIVE-LOCAL-RED-001`; commit `c9f6da55e` is locally green under +`E-M5-PORTABLE-ARCHIVE-LOCAL-001`. Exact-head all-six Node 24 proof is the current gate. Desktop +consumers, SSH transfer/install, mode wiring, tuple enablement, publication, production keys/environment/seed, and merge to `main` remain disconnected. @@ -156,7 +159,9 @@ disconnected. - [x] Prove exact clean-build equality and exact runtime-tree closure on all six runner families. - [ ] Replace unpublished POSIX `.tar.xz` outputs with deterministic bounded `.tar.br` and rerun exact clean-build/archive/execution proof on all six native jobs - (`E-M5-ARCHIVE-PORTABILITY-AUDIT-001` selects the correction; implementation evidence open). + (`E-M5-ARCHIVE-PORTABILITY-AUDIT-001` selects the correction; + `E-M5-PORTABLE-ARCHIVE-LOCAL-RED-001` proves the old boundary fails; + `E-M5-PORTABLE-ARCHIVE-LOCAL-001` passes locally; exact-head native evidence open). - [x] Complete the all-six SBOM, license, provenance, toolchain, and prohibited-content audit. - [x] Rebuild both Linux artifacts in the digest-pinned glibc 2.28/libstdc++ 6.0.25 userland on native x64/arm64 runners; smoke and compare them there. (`E-M3-LINUX-NATIVE-USERLAND-CI-001`) 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 790f10ad2c6..26899fdd13d 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 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: portable POSIX archive prerequisite correction**. The audit under E-M5-ARCHIVE-PORTABILITY-AUDIT-001 proves the current 64 MiB-dictionary `.tar.xz` output cannot meet the reviewed cross-client and 64 MiB desktop extraction contracts without an unsafe system/native dependency. Correct only the unpublished POSIX relay archive family to bounded Node-native `.tar.br`, replace its exact-head all-six artifact evidence, and then resume the disconnected desktop extraction RED. Windows ZIP, Node upstream input archives, desktop call sites, cache publication, SSH transfer/install, tuple enablement, mode wiring, and default behavior stay unchanged.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact implementation/evidence head `fc6b6d2197fa8cc012317a5ea155e57886183c61` passes the downloader-containing contract suite and full runtime build/smoke/equality on all six native jobs in run 29445770009 under E-M5-ARTIFACT-DOWNLOAD-CI-001. E-M5-ARCHIVE-PORTABILITY-AUDIT-001 measures the exact Linux x64 artifact from that run and selects deterministic Brotli quality 9, `lgwin=20`, and 64 KiB chunks: full 119 MiB extraction through `tar` settles in 533.13ms with a 34,144,256-byte RSS increase. The existing XZ stream alone declares 67,175,032 bytes of decoder memory, already 66,168 bytes over the complete desktop budget. The user authorizes end-to-end commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no real release write, publication caller, desktop consumer, cache publication/extraction, SSH transfer/install, tuple enablement, or production/default behavior change. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: portable POSIX archive prerequisite exact-head CI**. The audit under E-M5-ARCHIVE-PORTABILITY-AUDIT-001 proves the current 64 MiB-dictionary `.tar.xz` output cannot meet the reviewed cross-client and 64 MiB desktop extraction contracts. Commit `c9f6da55e` implements bounded Node-native `.tar.br` and is locally green under E-M5-PORTABLE-ARCHIVE-LOCAL-001. Push the exact implementation/evidence commits and replace all-six native artifact proof before resuming the disconnected desktop extraction RED. Windows ZIP, Node upstream input archives, desktop call sites, cache publication, SSH transfer/install, tuple enablement, mode wiring, and default behavior stay unchanged.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — implementation commit `c9f6da55e41d2f201684e967ce4c3db37150571f` streams deterministic Brotli/TAR creation and strict inspection/extraction without system/native XZ, updates exact relay-archive consumers, and wires the purpose suite into both native job families. E-M5-PORTABLE-ARCHIVE-LOCAL-001 passes 278 release tests, 233 desktop SSH-relay tests including one existing skip, typecheck, full lint/reliability/line gates, and a two-build exact-runtime byte comparison. Native Node 24 execution at this exact head remains required. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no release write, publication caller, desktop consumer, cache publication/extraction, SSH transfer/install, tuple enablement, or production/default behavior change. 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) @@ -132,12 +132,12 @@ same change as the work it records. E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001, and E-M4-DRAFT-RELEASE-COMPOSITION-CI-001. - Active package: **In progress — 2026-07-15, Codex implementation owner** — portable POSIX relay - archive prerequisite correction exposed by Work Package 4. E-M5-ARCHIVE-PORTABILITY-AUDIT-001 - rejects system XZ, the off-the-shelf WASM decoder, and the transitive p7zip binary and selects - deterministic Node-native `.tar.br` with quality 9, `lgwin=20`, and 64 KiB chunks. First replace - the archive builder/schema/inspection contracts and all-six artifact evidence; only then resume - disconnected desktop extraction. Keep Node upstream `.tar.xz` inputs, Windows ZIP, cache - publication, desktop call sites, SSH behavior, mode wiring, tuple enablement, and defaults intact. + archive exact-head native proof. The audit, purpose RED, and local implementation are closed under + E-M5-ARCHIVE-PORTABILITY-AUDIT-001, E-M5-PORTABLE-ARCHIVE-LOCAL-RED-001, and + E-M5-PORTABLE-ARCHIVE-LOCAL-001. Push `c9f6da55e` plus exact evidence metadata and require all six + Node 24 build/smoke/equality jobs before resuming disconnected desktop extraction. Keep Node + upstream `.tar.xz` inputs, Windows ZIP, cache publication, desktop call sites, SSH behavior, mode + wiring, tuple enablement, and defaults intact. - 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 @@ -12288,6 +12288,91 @@ preserveOwner: false, noChmod: false, unlink: false })>` over that exact runtime correction, run local archive/release/desktop parity gates, then push and require exact-head all-six native artifact proof before resuming desktop extraction. +### E-M5-PORTABLE-ARCHIVE-LOCAL-RED-001 — Portable POSIX archive contract fails against XZ builder + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted purpose-named contract test + `config/scripts/ssh-relay-runtime-portable-archive.test.mjs` atop local plan-decision commit + `c5fe474e6`. +- Exact command: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-portable-archive.test.mjs` +- Environment: macOS 26.2 arm64, Node v26.0.0, pnpm 10.24.0, Vitest 4.1.5. +- Result: required RED; one file / three tests fail in 189ms Vitest / 1.04s wall; maximum RSS + 131,629,056 bytes, zero swaps. +- Failure oracles: + - the builder returns the exact old `.tar.xz` name instead of `.tar.br`; + - `SSH_RELAY_RUNTIME_POSIX_ARCHIVE_LIMITS` is absent instead of declaring 65,536-byte chunks, + quality 9, and window bits 20; + - the source imports `node:child_process`, spawns system `xz`, and has no built-in Brotli + compressor/decompressor. +- Does not prove: implementation, GREEN, hostile archive behavior, deterministic real full-size + output, Node 24 or native-runner behavior, schema/release parity, desktop extraction, cache, SSH, + publication, or any tuple. +- Follow-up: implement the smallest Node-native archive builder/inspector correction, then update + exact relay-archive naming consumers without changing Node upstream archives or Windows ZIP. + +### E-M5-PORTABLE-ARCHIVE-LOCAL-001 — Bounded Node-native relay archives pass locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: implementation commit `c9f6da55e41d2f201684e967ce4c3db37150571f` atop plan-decision + commit `c5fe474e6f3f84e8b6c0fed930dd6dc580454e74`; draft PR #8741. Evidence metadata + is the following docs-only commit. +- Environment: macOS 26.2 build 25C56 arm64; Node v26.0.0; pnpm 10.24.0; Vitest 4.1.5. +- Implementation oracle: + - POSIX relay output is `.tar.br`; Windows remains ZIP and upstream Node inputs remain `.tar.xz`. + - deterministic TAR is streamed directly through Node's built-in Brotli quality 9, + `lgwin=20`, and 65,536-byte chunks into an exclusive mode-0600 output, avoiding both system XZ + and a full uncompressed temporary TAR; + - caller cancellation is combined with the five-minute artifact timeout and checked before output + creation; failure removes only an output this invocation opened, never an existing file; + - strict inspection and release-side exclusive extraction use the same bounded built-in Brotli + stream before exact path/type/mode/size/hash and full-tree verification; + - aggregate, manifest, tuple, post-sign, draft/read-back, direct-URL, reproducibility, workflow, + and canonical-vector contracts use the new exact archive name; the canonical manifest SHA-256 is + updated from the changed authenticated filename, not bypassed; + - both native job families syntax-check and execute the five-test purpose suite. The Linux + supplement and native-signing reconstruction use the strict Node extractor; CI-only `tar -xJf` + and native-signing XZ installation are removed and guarded against regression. +- Purpose/workflow command: + `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-portable-archive.test.mjs config/scripts/ssh-relay-runtime-workflow.test.mjs config/scripts/ssh-relay-runtime-native-signing-workflow.test.mjs` + passes three files / 13 tests in 462ms. It covers deterministic output, exact rehash, bounded + constants, no system/native XZ, truncated Brotli rejection, existing-output preservation, + pre-cancel settlement/cleanup, and both native workflow families. +- Release regression command: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs` + passes 50 files / 278 tests in 9.62s Vitest / 10.63s wall; maximum RSS 188,940,288 bytes, zero + swaps. +- Desktop parity command: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts` + passes 16 files and skips one existing file; 232 tests pass and one is skipped in 6.09s Vitest / + 7.46s wall; maximum RSS 297,877,504 bytes, zero swaps. +- Full-size deterministic command: run the committed + `createSshRelayRuntimeArchive` twice with source epoch 1788739200 over the exact reconstructed Linux + x64 artifact 8355514197 from run 29445770009, then call `inspectSshRelayRuntimeArchive`, `cmp`, + `shasum -a 256`, and `stat -f '%N %z'` on both outputs. The 124,846,430 declared file bytes / 49 + entries / 34 files produce two byte-identical 36,411,047-byte archives with SHA-256 + `23a35a0db4b36166c6ad327c4986978b79f0281ef979dcd5f16525428e9ecdaa`. Builds take + 7,824.18ms and 8,683.93ms; strict inspection takes 321.16ms. The combined two-build/inspection + process completes in 16.92s with 226,557,952-byte maximum RSS and zero swaps. This is release-job + construction memory, not desktop extraction memory; the separately measured complete extraction + delta is 34,144,256 bytes under E-M5-ARCHIVE-PORTABILITY-AUDIT-001. +- Static commands: changed-file `oxlint`; `pnpm typecheck`; `pnpm lint`; `node --check +config/scripts/ssh-relay-runtime-portable-archive.test.mjs`; changed-file `oxfmt`; purpose/workflow + Vitest; `git diff --check`; and protected-file zero-diff all pass. Full lint retains only unrelated + pre-existing warnings and passes switch exhaustiveness, 41 reliability gates, max-lines ratchet, + bundled guides, localization catalog, and localization coverage. +- Consumer-disconnection oracle: there is no desktop extraction/cache module, caller, SSH + transfer/install, release write, production key, published artifact, tuple enablement, mode, or + default change. Legacy remains the only production path. +- Does not prove: Node 24, Linux/Windows/macOS x64/arm64 native execution, exact clean-build equality + for new archive bytes, packaged Electron extraction, desktop two-minute timeout, live SSH, + release publication/signing/native trust, oldest floors, or any enabled tuple. +- Follow-up: push the exact implementation/evidence heads and require the new purpose suite plus + complete runtime build/smoke/equality/read-back to pass on all six native Node 24 jobs before + checking the archive item or resuming desktop extraction. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -12346,11 +12431,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Add the purpose-named `.tar.br` contract RED selected by E-M5-ARCHIVE-PORTABILITY-AUDIT-001, then -implement only the POSIX relay artifact-format correction and replace its exact-head all-six native +Push implementation `c9f6da55e` and its evidence checkpoint, then replace exact-head all-six native archive/reproducibility/execution evidence. Do not begin desktop extraction/cache staging until that -prerequisite is green. Do not add a desktop call site, SSH transfer/install, mode wiring, tuple -enablement, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, +prerequisite is green on Node 24. Do not add a desktop call site, SSH transfer/install, mode wiring, +tuple enablement, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. From af181dcc773c2bc103aaf6694339591330d73e28 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 16:50:17 -0400 Subject: [PATCH 162/267] fix(ssh): prepare relay extraction parent Co-authored-by: Orca --- .../ssh-relay-runtime-archive-extraction.mjs | 5 +- ...-relay-runtime-archive-extraction.test.mjs | 23 +++- ...elease-implementation-checklist-summary.md | 8 +- ...github-release-implementation-checklist.md | 106 ++++++++++++++++-- 4 files changed, 129 insertions(+), 13 deletions(-) diff --git a/config/scripts/ssh-relay-runtime-archive-extraction.mjs b/config/scripts/ssh-relay-runtime-archive-extraction.mjs index e221a7714a1..22fccf9da40 100644 --- a/config/scripts/ssh-relay-runtime-archive-extraction.mjs +++ b/config/scripts/ssh-relay-runtime-archive-extraction.mjs @@ -132,7 +132,10 @@ export async function extractSshRelayRuntimeArchive({ const absoluteArchive = resolve(archivePath) const absoluteOutput = resolve(outputDirectory) const finalIdentity = assertIdentityArchive(identity, absoluteArchive) - const physicalParent = await realpath(dirname(absoluteOutput)) + const outputParent = dirname(absoluteOutput) + // Why: clean signing/floor runners supply a fresh parent; only the final staging leaf is exclusive. + await mkdir(outputParent, { recursive: true, mode: 0o700 }) + const physicalParent = await realpath(outputParent) const physicalOutput = resolve(physicalParent, basename(absoluteOutput)) await assertOutputAbsent(physicalOutput) const before = await describeArchive(absoluteArchive, identity.archive.size, effectiveSignal) diff --git a/config/scripts/ssh-relay-runtime-archive-extraction.test.mjs b/config/scripts/ssh-relay-runtime-archive-extraction.test.mjs index 7540b9e2eae..54df8a5bd67 100644 --- a/config/scripts/ssh-relay-runtime-archive-extraction.test.mjs +++ b/config/scripts/ssh-relay-runtime-archive-extraction.test.mjs @@ -1,5 +1,5 @@ import { createHash } from 'node:crypto' -import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { dirname, join } from 'node:path' @@ -87,6 +87,27 @@ afterEach(async () => { }) describe('SSH relay runtime archive extraction', () => { + it('prepares a fresh staging parent while keeping the output leaf exclusive', async () => { + const tupleId = process.platform === 'win32' ? `win32-${process.arch}` : 'darwin-arm64' + const value = await fixture(tupleId) + const outputDirectory = join(value.root, 'fresh-parent', 'reconstructed') + + const result = await extractSshRelayRuntimeArchive({ + archivePath: value.archive.path, + outputDirectory, + identity: value.identity + }) + expect(result.runtimeRoot).toBe(await realpath(outputDirectory)) + + await expect( + extractSshRelayRuntimeArchive({ + archivePath: value.archive.path, + outputDirectory, + identity: value.identity + }) + ).rejects.toThrow(/exclusive/i) + }) + it('reconstructs exact POSIX and Windows runtime trees into exclusive directories', async () => { // Why: NTFS cannot materialize the executable modes needed to construct a POSIX tar fixture. const tuples = 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 1e7ff4afe2e..c2cc2cfdff5 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 @@ -20,8 +20,12 @@ meet the cross-client 64 MiB extraction contract and selects bounded Node-native the archive contracts and all-six artifact evidence before the isolated extraction/cache-staging RED. The purpose-named missing-contract RED is recorded under `E-M5-PORTABLE-ARCHIVE-LOCAL-RED-001`; commit `c9f6da55e` is locally green under -`E-M5-PORTABLE-ARCHIVE-LOCAL-001`. Exact-head all-six Node 24 proof is the current gate. Desktop -consumers, SSH transfer/install, mode wiring, +`E-M5-PORTABLE-ARCHIVE-LOCAL-001`. Exact-head run +[29448792600](https://github.com/stablyai/orca/actions/runs/29448792600) passes all six primary native +jobs but exposes a fresh-parent staging defect in both Linux oldest-userland supplements: +strict extraction calls `realpath` before preparing the non-existent parent +(`E-M5-PORTABLE-ARCHIVE-CI-RED-001`). Correct and prove only that exclusive-staging precondition, +then replace the all-six Node 24 evidence. Desktop consumers, SSH transfer/install, mode wiring, tuple enablement, publication, production keys/environment/seed, and merge to `main` remain disconnected. 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 26899fdd13d..d94a2de4cd3 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 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: portable POSIX archive prerequisite exact-head CI**. The audit under E-M5-ARCHIVE-PORTABILITY-AUDIT-001 proves the current 64 MiB-dictionary `.tar.xz` output cannot meet the reviewed cross-client and 64 MiB desktop extraction contracts. Commit `c9f6da55e` implements bounded Node-native `.tar.br` and is locally green under E-M5-PORTABLE-ARCHIVE-LOCAL-001. Push the exact implementation/evidence commits and replace all-six native artifact proof before resuming the disconnected desktop extraction RED. Windows ZIP, Node upstream input archives, desktop call sites, cache publication, SSH transfer/install, tuple enablement, mode wiring, and default behavior stay unchanged.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — implementation commit `c9f6da55e41d2f201684e967ce4c3db37150571f` streams deterministic Brotli/TAR creation and strict inspection/extraction without system/native XZ, updates exact relay-archive consumers, and wires the purpose suite into both native job families. E-M5-PORTABLE-ARCHIVE-LOCAL-001 passes 278 release tests, 233 desktop SSH-relay tests including one existing skip, typecheck, full lint/reliability/line gates, and a two-build exact-runtime byte comparison. Native Node 24 execution at this exact head remains required. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no release write, publication caller, desktop consumer, cache publication/extraction, SSH transfer/install, tuple enablement, or production/default behavior change. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: portable POSIX archive fresh-parent correction**. Exact-head run 29448792600 proves all six primary native jobs but exposes one implementation defect in both Linux oldest-userland supplements: strict extraction resolves a fresh staging parent before preparing it. E-M5-PORTABLE-ARCHIVE-CI-RED-001 and the local RED/GREEN correction evidence are recorded; checkpoint and push only that precondition correction without weakening exclusive leaf creation or archive/tree verification. Then replace the all-six native artifact proof before resuming disconnected desktop extraction. Windows ZIP, Node upstream input archives, desktop call sites, cache publication, SSH transfer/install, tuple enablement, mode wiring, and default behavior stay unchanged.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact implementation/evidence head `cdb318df339941325a1055d841c6ae7d5484dfaf` is complete under artifact run 29448792600, PR Checks 29448792389, and Golden E2E 29448792403. All six primary native jobs, PR Checks, Golden E2E, and Windows x64 floor are green. Windows arm64 passes runtime smoke and then rejects hosted build 26200 against the retained build-26100 floor. Supplement jobs 87466788553 and 87466788626 fail before extraction with `ENOENT` from `realpath(.../baseline-runtime)`, because the immediate parent is intentionally fresh. The local correction preserves authenticated-input inspection, exclusive final-leaf staging, cleanup ownership, full-tree verification, and all existing cancellation/bounds and is green under E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-001. This is an implementation correction, not a design change. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no release write, publication caller, desktop consumer, cache publication/extraction, SSH transfer/install, tuple enablement, or production/default behavior change. 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) @@ -132,12 +132,13 @@ same change as the work it records. E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001, and E-M4-DRAFT-RELEASE-COMPOSITION-CI-001. - Active package: **In progress — 2026-07-15, Codex implementation owner** — portable POSIX relay - archive exact-head native proof. The audit, purpose RED, and local implementation are closed under - E-M5-ARCHIVE-PORTABILITY-AUDIT-001, E-M5-PORTABLE-ARCHIVE-LOCAL-RED-001, and - E-M5-PORTABLE-ARCHIVE-LOCAL-001. Push `c9f6da55e` plus exact evidence metadata and require all six - Node 24 build/smoke/equality jobs before resuming disconnected desktop extraction. Keep Node - upstream `.tar.xz` inputs, Windows ZIP, cache publication, desktop call sites, SSH behavior, mode - wiring, tuple enablement, and defaults intact. + archive fresh-parent staging correction. Exact-head run 29448792600 passes all six primary native + jobs but both Linux oldest-userland supplements fail under + E-M5-PORTABLE-ARCHIVE-CI-RED-001 because strict extraction resolves an intentionally fresh parent + before preparing it. Add the missing regression contract and correct only parent preparation; + keep exclusive final-leaf creation, authenticated archive/tree verification, cleanup ownership, + cache publication, desktop call sites, SSH behavior, mode wiring, tuple enablement, and defaults + intact. Replacement all-six Node 24 proof remains required. - 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 @@ -12373,6 +12374,92 @@ config/scripts/ssh-relay-runtime-portable-archive.test.mjs`; changed-file `oxfmt complete runtime build/smoke/equality/read-back to pass on all six native Node 24 jobs before checking the archive item or resuming desktop extraction. +### E-M5-PORTABLE-ARCHIVE-CI-RED-001 — Fresh Linux floor staging parent breaks strict extraction + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `cdb318df339941325a1055d841c6ae7d5484dfaf`; draft PR #8741. +- Workflows: [SSH Relay Runtime Artifacts run + 29448792600](https://github.com/stablyai/orca/actions/runs/29448792600), [PR Checks run + 29448792389](https://github.com/stablyai/orca/actions/runs/29448792389), and [Golden E2E run + 29448792403](https://github.com/stablyai/orca/actions/runs/29448792403). +- Passing boundary: all six primary Node 24 target-native jobs pass the portable-archive contract + suite and complete two-build inspection, bundled Node/PTY/watcher smoke, exact archive equality, + read-back execution, and unpublished evidence upload. Primary job IDs are Linux x64 87465865029, + Linux arm64 87465865049, macOS x64 87465865021, macOS arm64 87465865039, Windows x64 87465865008, + and Windows arm64 87465865028. PR Checks and Golden E2E are green. Windows x64 oldest-floor job + 87467928882 is green. Windows arm64 floor job 87467928866 passes exact archive reconstruction and + bundled Node/PTY/watcher smoke in 8,112.4613ms overall / 5,970.8928ms smoke with 49,889,280-byte + RSS, then fails only the retained floor oracle because hosted image `windows-11-arm64/20260714.109` + is build 26200 rather than required build 26100. +- Required RED: Linux oldest-userland supplement jobs + [87466788626](https://github.com/stablyai/orca/actions/runs/29448792600/job/87466788626) and + [87466788553](https://github.com/stablyai/orca/actions/runs/29448792600/job/87466788553) + authenticate and download the exact 36,327,346-byte x64 / 36,261,822-byte arm64 Actions artifacts, + then both fail before archive extraction with `ENOENT` from `realpath(.../baseline-runtime)`. + The workflow intentionally supplies fresh nested output paths, but + `extractSshRelayRuntimeArchive` resolves the immediate parent before preparing it. +- Security oracle: this is not an archive/hash/tree/native-smoke failure and does not justify a + fallback or weakened verifier. The correction must prepare only the parent hierarchy while + retaining exclusive final-leaf creation, physical-path resolution, authenticated input + inspection, cleanup ownership, complete-tree verification, and cancellation/timeout bounds. +- Does not prove: the correction, replacement Linux supplement execution, packaged Electron + extraction/cache behavior, live SSH, native signing/trust, Windows arm64 build 26100, publication, + tuple enablement, or any default-path change. +- Follow-up: add a local missing-parent RED, implement the narrow parent-preparation correction, and + replace the exact-head native run rather than rerunning only the failed jobs on the old SHA. + +### E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-RED-001 — Missing-parent extraction contract reproduces CI + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted regression test atop exact CI RED head `cdb318df3`. +- Exact command: + `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-archive-extraction.test.mjs` +- Environment: macOS 26.2 arm64, Node v26.0.0, pnpm 10.24.0, Vitest 4.1.5. +- Result: required RED; one file / three tests runs in 1.52s, with the new test failing in 86ms and + the two existing extraction tests passing. The failure is the same `ENOENT` from `realpath` of a + deliberately absent `fresh-parent` directory observed in both Linux supplement jobs. +- Does not prove: the correction, Linux/Windows behavior, exclusive-leaf preservation, Node 24, + full-size extraction, supplements, desktop cache, SSH, or any tuple. + +### E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-001 — Fresh parent and exclusive leaf pass locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted correction following E-M5-PORTABLE-ARCHIVE-CI-RED-001 and + E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-RED-001; its commit SHA is recorded after commit creation. +- Implementation oracle: prepare the resolved output's parent hierarchy with mode 0700 where newly + created, then resolve its physical path and preserve the existing exclusive final-leaf `mkdir`. + A second extraction to the same leaf remains rejected. Archive identity/hash/size inspection, + bounded Brotli/ZIP extraction, full-tree verification, input mutation detection, cleanup + ownership, timeout, and cancellation code are unchanged. +- Focused command: + `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-archive-extraction.test.mjs config/scripts/ssh-relay-runtime-portable-archive.test.mjs config/scripts/ssh-relay-runtime-native-signing-workflow.test.mjs` + passes three files / nine tests in 7.22s. POSIX runners exercise TAR/Brotli and Windows runners + exercise ZIP for the platform-selected fresh-parent contract. +- Release regression command: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs` + passes 50 files / 279 tests in 18.70s Vitest / 20.75s wall with 189,284,352-byte maximum RSS and + zero swaps. +- Desktop parity command: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts` + passes 16 files and skips one existing file; 232 tests pass and one is skipped in 15.66s Vitest / + 17.10s wall with 257,802,240-byte maximum RSS and zero swaps. +- Static commands: `node --check` for the extractor and its test, `pnpm typecheck`, and `pnpm lint` + pass. Full lint retains only unrelated pre-existing warnings and passes switch exhaustiveness, 41 + reliability gates, max-lines ratchet, bundled guides, localization catalog, and localization + coverage. Changed-file formatting, `git diff --check`, and protected-file zero-diff remain required + before commit. +- Consumer-disconnection oracle: no desktop consumer, cache publication, SSH transfer/install, + release write, production key, tuple, mode, or default changes. Legacy remains the only production + path. +- Does not prove: Node 24/native-runner behavior, corrected Linux supplements, packaged Electron + extraction, desktop cache, live SSH, native signing/trust, oldest incomplete floors, publication, + or any enabled tuple. +- Follow-up: format/checkpoint the correction, push it, and require replacement exact-head all-six + primary jobs plus both Linux supplements before resuming disconnected desktop extraction. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -12431,7 +12518,8 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push implementation `c9f6da55e` and its evidence checkpoint, then replace exact-head all-six native +Format/checkpoint the fresh-parent correction under E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-001, push it, +and replace exact-head all-six primary plus both Linux supplement archive/reproducibility/execution evidence. Do not begin desktop extraction/cache staging until that prerequisite is green on Node 24. Do not add a desktop call site, SSH transfer/install, mode wiring, tuple enablement, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, From 3080eda77fd6a11ed87f536c1963e5b67abe7621 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 16:51:17 -0400 Subject: [PATCH 163/267] docs: record relay extraction staging proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 5 +++-- ...github-release-implementation-checklist.md | 22 ++++++++++--------- 2 files changed, 15 insertions(+), 12 deletions(-) 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 c2cc2cfdff5..672844fee2f 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 @@ -24,8 +24,9 @@ RED. The purpose-named missing-contract RED is recorded under [29448792600](https://github.com/stablyai/orca/actions/runs/29448792600) passes all six primary native jobs but exposes a fresh-parent staging defect in both Linux oldest-userland supplements: strict extraction calls `realpath` before preparing the non-existent parent -(`E-M5-PORTABLE-ARCHIVE-CI-RED-001`). Correct and prove only that exclusive-staging precondition, -then replace the all-six Node 24 evidence. Desktop consumers, SSH transfer/install, mode wiring, +(`E-M5-PORTABLE-ARCHIVE-CI-RED-001`). Commit `af181dcc7` corrects and locally proves only that +exclusive-staging precondition under `E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-001`; replacement all-six +Node 24 evidence is the current gate. Desktop consumers, SSH transfer/install, mode wiring, tuple enablement, publication, production keys/environment/seed, and merge to `main` remain disconnected. 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 d94a2de4cd3..de0c9feea20 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 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: portable POSIX archive fresh-parent correction**. Exact-head run 29448792600 proves all six primary native jobs but exposes one implementation defect in both Linux oldest-userland supplements: strict extraction resolves a fresh staging parent before preparing it. E-M5-PORTABLE-ARCHIVE-CI-RED-001 and the local RED/GREEN correction evidence are recorded; checkpoint and push only that precondition correction without weakening exclusive leaf creation or archive/tree verification. Then replace the all-six native artifact proof before resuming disconnected desktop extraction. Windows ZIP, Node upstream input archives, desktop call sites, cache publication, SSH transfer/install, tuple enablement, mode wiring, and default behavior stay unchanged.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact implementation/evidence head `cdb318df339941325a1055d841c6ae7d5484dfaf` is complete under artifact run 29448792600, PR Checks 29448792389, and Golden E2E 29448792403. All six primary native jobs, PR Checks, Golden E2E, and Windows x64 floor are green. Windows arm64 passes runtime smoke and then rejects hosted build 26200 against the retained build-26100 floor. Supplement jobs 87466788553 and 87466788626 fail before extraction with `ENOENT` from `realpath(.../baseline-runtime)`, because the immediate parent is intentionally fresh. The local correction preserves authenticated-input inspection, exclusive final-leaf staging, cleanup ownership, full-tree verification, and all existing cancellation/bounds and is green under E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-001. This is an implementation correction, not a design change. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no release write, publication caller, desktop consumer, cache publication/extraction, SSH transfer/install, tuple enablement, or production/default behavior change. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: portable POSIX archive fresh-parent replacement CI**. Exact-head run 29448792600 proves all six primary native jobs but exposes one implementation defect in both Linux oldest-userland supplements: strict extraction resolves a fresh staging parent before preparing it. E-M5-PORTABLE-ARCHIVE-CI-RED-001 records the failure; commit `af181dcc7` and E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-001 correct and locally prove only that precondition without weakening exclusive leaf creation or archive/tree verification. Push and replace the all-six native artifact proof before resuming disconnected desktop extraction. Windows ZIP, Node upstream input archives, desktop call sites, cache publication, SSH transfer/install, tuple enablement, mode wiring, and default behavior stay unchanged.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — correction head `af181dcc7` prepares a fresh mode-0700 parent hierarchy, then preserves physical-path resolution and exclusive final-leaf creation. It passes 279 release tests, 233 desktop parity tests including one existing skip, typecheck, full lint/reliability/line/localization gates, formatting, and protected-file zero-diff under E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-001. The previous exact head `cdb318df3` passed all six primary native jobs, PR Checks, Golden E2E, and Windows x64 floor; Windows arm64 passed runtime smoke before rejecting hosted build 26200 against build 26100. Replacement exact-head Node 24 primary and Linux-supplement proof is required. This is an implementation correction, not a design change. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no release write, publication caller, desktop consumer, cache publication/extraction, SSH transfer/install, tuple enablement, or production/default behavior change. 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) @@ -135,10 +135,11 @@ same change as the work it records. archive fresh-parent staging correction. Exact-head run 29448792600 passes all six primary native jobs but both Linux oldest-userland supplements fail under E-M5-PORTABLE-ARCHIVE-CI-RED-001 because strict extraction resolves an intentionally fresh parent - before preparing it. Add the missing regression contract and correct only parent preparation; - keep exclusive final-leaf creation, authenticated archive/tree verification, cleanup ownership, - cache publication, desktop call sites, SSH behavior, mode wiring, tuple enablement, and defaults - intact. Replacement all-six Node 24 proof remains required. + before preparing it. Commit `af181dcc7` adds the missing regression contract and corrects only + parent preparation under E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-001; keep exclusive final-leaf + creation, authenticated archive/tree verification, cleanup ownership, cache publication, desktop + call sites, SSH behavior, mode wiring, tuple enablement, and defaults intact. Replacement all-six + Node 24 proof remains required. - 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 @@ -12427,8 +12428,9 @@ config/scripts/ssh-relay-runtime-portable-archive.test.mjs`; changed-file `oxfmt - Date: 2026-07-15 - Owner: Codex implementation owner -- Source: uncommitted correction following E-M5-PORTABLE-ARCHIVE-CI-RED-001 and - E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-RED-001; its commit SHA is recorded after commit creation. +- Source: implementation/evidence commit `af181dcc7` following E-M5-PORTABLE-ARCHIVE-CI-RED-001 and + E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-RED-001; exact full SHA + `af181dcc7dfce1321434466fe0917789a4de8bd9`. This docs-only metadata checkpoint follows it. - Implementation oracle: prepare the resolved output's parent hierarchy with mode 0700 where newly created, then resolve its physical path and preserve the existing exclusive final-leaf `mkdir`. A second extraction to the same leaf remains rejected. Archive identity/hash/size inspection, @@ -12518,8 +12520,8 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Format/checkpoint the fresh-parent correction under E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-001, push it, -and replace exact-head all-six primary plus both Linux supplement +Push fresh-parent correction `af181dcc7` and this evidence checkpoint, then replace exact-head +all-six primary plus both Linux supplement archive/reproducibility/execution evidence. Do not begin desktop extraction/cache staging until that prerequisite is green on Node 24. Do not add a desktop call site, SSH transfer/install, mode wiring, tuple enablement, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, From a93e67413a2a1446e8e0f7348af1fdf72737287d Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 17:10:46 -0400 Subject: [PATCH 164/267] docs: record portable relay archive CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 31 ++--- ...github-release-implementation-checklist.md | 116 +++++++++++++++--- 2 files changed, 111 insertions(+), 36 deletions(-) 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 672844fee2f..7176802a3f2 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 @@ -9,24 +9,14 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — in-progress portable -POSIX archive prerequisite correction, 2026-07-15, Codex implementation owner.** Exact-head run -[29445770009](https://github.com/stablyai/orca/actions/runs/29445770009) passes the -downloader-containing contracts and full runtime build/smoke/equality on all six native Node 24 jobs -under `E-M5-ARTIFACT-DOWNLOAD-CI-001`. PR Checks and both Golden E2E jobs are green; the artifact run -is red only after Windows arm64 passes smoke and rejects hosted build 26200 against the retained -26100 floor. `E-M5-ARCHIVE-PORTABILITY-AUDIT-001` proves the unpublished `.tar.xz` output cannot -meet the cross-client 64 MiB extraction contract and selects bounded Node-native `.tar.br`. Replace -the archive contracts and all-six artifact evidence before the isolated extraction/cache-staging -RED. The purpose-named missing-contract RED is recorded under -`E-M5-PORTABLE-ARCHIVE-LOCAL-RED-001`; commit `c9f6da55e` is locally green under -`E-M5-PORTABLE-ARCHIVE-LOCAL-001`. Exact-head run -[29448792600](https://github.com/stablyai/orca/actions/runs/29448792600) passes all six primary native -jobs but exposes a fresh-parent staging defect in both Linux oldest-userland supplements: -strict extraction calls `realpath` before preparing the non-existent parent -(`E-M5-PORTABLE-ARCHIVE-CI-RED-001`). Commit `af181dcc7` corrects and locally proves only that -exclusive-staging precondition under `E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-001`; replacement all-six -Node 24 evidence is the current gate. Desktop consumers, SSH transfer/install, mode wiring, +Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — in-progress disconnected +extraction/cache contract audit, 2026-07-15, Codex implementation owner.** Exact-head run +[29449869519](https://github.com/stablyai/orca/actions/runs/29449869519) closes deterministic bounded +`.tar.br` under `E-M5-PORTABLE-ARCHIVE-CI-001`: all six primary Node 24 jobs and both Linux +oldest-userland supplements pass. PR Checks and Golden E2E are green; Windows x64 floor passes and +Windows arm64 passes smoke before the retained hosted build-26200 versus required-26100 rejection. +The next safe slice is only the isolated desktop extraction/cache resource and failure-semantics +audit followed by a purpose-named RED. Desktop consumers, SSH transfer/install, mode wiring, tuple enablement, publication, production keys/environment/seed, and merge to `main` remain disconnected. @@ -162,11 +152,12 @@ disconnected. - [x] Build and smoke-test Node, patched `node-pty`, and `@parcel/watcher` on GitHub runners for Linux, macOS, and Windows on x64 and arm64. - [x] Prove exact clean-build equality and exact runtime-tree closure on all six runner families. -- [ ] Replace unpublished POSIX `.tar.xz` outputs with deterministic bounded `.tar.br` and rerun +- [x] Replace unpublished POSIX `.tar.xz` outputs with deterministic bounded `.tar.br` and rerun exact clean-build/archive/execution proof on all six native jobs (`E-M5-ARCHIVE-PORTABILITY-AUDIT-001` selects the correction; `E-M5-PORTABLE-ARCHIVE-LOCAL-RED-001` proves the old boundary fails; - `E-M5-PORTABLE-ARCHIVE-LOCAL-001` passes locally; exact-head native evidence open). + `E-M5-PORTABLE-ARCHIVE-LOCAL-001`, `E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-001`, and + `E-M5-PORTABLE-ARCHIVE-CI-001`). - [x] Complete the all-six SBOM, license, provenance, toolchain, and prohibited-content audit. - [x] Rebuild both Linux artifacts in the digest-pinned glibc 2.28/libstdc++ 6.0.25 userland on native x64/arm64 runners; smoke and compare them there. (`E-M3-LINUX-NATIVE-USERLAND-CI-001`) 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 de0c9feea20..50e14f97e3c 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 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: portable POSIX archive fresh-parent replacement CI**. Exact-head run 29448792600 proves all six primary native jobs but exposes one implementation defect in both Linux oldest-userland supplements: strict extraction resolves a fresh staging parent before preparing it. E-M5-PORTABLE-ARCHIVE-CI-RED-001 records the failure; commit `af181dcc7` and E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-001 correct and locally prove only that precondition without weakening exclusive leaf creation or archive/tree verification. Push and replace the all-six native artifact proof before resuming disconnected desktop extraction. Windows ZIP, Node upstream input archives, desktop call sites, cache publication, SSH transfer/install, tuple enablement, mode wiring, and default behavior stay unchanged.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — correction head `af181dcc7` prepares a fresh mode-0700 parent hierarchy, then preserves physical-path resolution and exclusive final-leaf creation. It passes 279 release tests, 233 desktop parity tests including one existing skip, typecheck, full lint/reliability/line/localization gates, formatting, and protected-file zero-diff under E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-001. The previous exact head `cdb318df3` passed all six primary native jobs, PR Checks, Golden E2E, and Windows x64 floor; Windows arm64 passed runtime smoke before rejecting hosted build 26200 against build 26100. Replacement exact-head Node 24 primary and Linux-supplement proof is required. This is an implementation correction, not a design change. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no release write, publication caller, desktop consumer, cache publication/extraction, SSH transfer/install, tuple enablement, or production/default behavior change. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: disconnected desktop extraction/cache contract audit and RED**. Exact-head run 29449869519 closes the portable POSIX artifact prerequisite under E-M5-PORTABLE-ARCHIVE-CI-001: all six primary Node 24 jobs and both Linux oldest-userland supplements pass deterministic build, strict inspection/extraction, bundled Node/PTY/watcher smoke, equality, and read-back. Audit the existing desktop boundaries and resource/failure contracts, then add only a purpose-named missing extraction/cache-staging RED. Do not add a desktop caller, cache publication, SSH transfer/install, tuple enablement, mode wiring, or default behavior.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact implementation/evidence head `3080eda77fd6a11ed87f536c1963e5b67abe7621` passes artifact run 29449869519 on all six primary native jobs and both Linux supplements. PR Checks 29449869451 and both Golden E2E jobs in run 29449869407 are green. Windows x64 oldest-floor execution passes; Windows arm64 reconstructs and smokes the exact runtime in 7,884.492ms with 49,733,632-byte RSS, then rejects hosted build 26200 against required build 26100. E-M5-PORTABLE-ARCHIVE-CI-001 records exact jobs, runner images, tests, artifact IDs/sizes/hashes, and smoke metrics. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no release write, publication caller, desktop consumer, cache publication/extraction, SSH transfer/install, tuple enablement, or production/default behavior change. 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) @@ -131,15 +131,17 @@ same change as the work it records. E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-RED-001, E-M4-DRAFT-RELEASE-COMPOSITION-LOCAL-001, and E-M4-DRAFT-RELEASE-COMPOSITION-CI-001. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — portable POSIX relay - archive fresh-parent staging correction. Exact-head run 29448792600 passes all six primary native - jobs but both Linux oldest-userland supplements fail under - E-M5-PORTABLE-ARCHIVE-CI-RED-001 because strict extraction resolves an intentionally fresh parent - before preparing it. Commit `af181dcc7` adds the missing regression contract and corrects only - parent preparation under E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-001; keep exclusive final-leaf - creation, authenticated archive/tree verification, cleanup ownership, cache publication, desktop - call sites, SSH behavior, mode wiring, tuple enablement, and defaults intact. Replacement all-six - Node 24 proof remains required. +- Completed package: portable POSIX relay archive correction. E-M5-ARCHIVE-PORTABILITY-AUDIT-001, + E-M5-PORTABLE-ARCHIVE-LOCAL-RED-001, E-M5-PORTABLE-ARCHIVE-LOCAL-001, + E-M5-PORTABLE-ARCHIVE-CI-RED-001, E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-RED-001, + E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-001, and E-M5-PORTABLE-ARCHIVE-CI-001 prove the selected bounded + archive, correction, all-six primary jobs, and both Linux oldest-userland supplements. Windows ZIP + and upstream Node `.tar.xz` remain unchanged. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — disconnected desktop + extraction/cache contract audit and purpose-named RED. Inventory existing cache/path/atomic-file + boundaries, formalize resource and integrity failure oracles, and prove the missing contract before + implementation. Keep cache publication, desktop call sites, SSH behavior, mode wiring, tuple + enablement, release writes, and defaults intact. - 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 @@ -12462,6 +12464,89 @@ config/scripts/ssh-relay-runtime-portable-archive.test.mjs`; changed-file `oxfmt - Follow-up: format/checkpoint the correction, push it, and require replacement exact-head all-six primary jobs plus both Linux supplements before resuming disconnected desktop extraction. +### E-M5-PORTABLE-ARCHIVE-CI-001 — Portable archives pass all-six native and Linux floor gates + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `3080eda77fd6a11ed87f536c1963e5b67abe7621`; archive implementation + `c9f6da55e41d2f201684e967ce4c3db37150571f`; fresh-parent correction + `af181dcc7dfce1321434466fe0917789a4de8bd9`; draft PR #8741. +- Workflow: [SSH Relay Runtime Artifacts run + 29449869519](https://github.com/stablyai/orca/actions/runs/29449869519), starting 20:51:36Z. The + aggregate conclusion is the expected failure only because the separate Windows arm64 floor job + rejects hosted build 26200 after successful archive reconstruction and runtime smoke. +- Native job/runtime-contract evidence: + - Linux x64 job 87469418941, `ubuntu-24.04` `20260705.232.1`, passes in 2m34s; 62 files / 398 tests + in 9.34s. Final read-back smoke is 160.642692ms with 56,541,184-byte RSS; the complete verifier is + 1,014.651163ms. + - Linux arm64 job 87469419077, `ubuntu-24.04-arm` `20260706.52.2`, passes in 5m55s; 62 files / 398 + tests in 7.91s. Final read-back smoke is 209.703393ms with 52,625,408-byte RSS; the complete + verifier is 853.006072ms. + - macOS x64 job 87469418977, `macos-15` `20260629.0276.1`, passes in 4m25s; 62 files / 398 tests in + 20.32s. Final read-back smoke is 362.966426ms with 40,611,840-byte RSS; the complete verifier is + 2,565.85366ms. + - macOS arm64 job 87469418947, `macos-15-arm64` `20260706.0213.1`, passes in 3m11s; 62 files / 398 + tests in 21.43s. Final read-back smoke is 444.272042ms with 51,445,760-byte RSS; the complete + verifier is 1,217.196375ms. + - Windows x64 job 87469419062, `windows-2022` `20260706.237.1`, passes in 5m32s; 63 files / 389 + passed + 13 platform skips of 402 in 14.82s. Final read-back smoke is 5,333.875ms with + 53,739,520-byte RSS; the complete verifier is 6,388.1631ms. + - Windows arm64 job 87469418940, `windows-11-arm64` `20260714.109.1`, passes in 11m21s; 63 files / + 389 passed + 13 platform skips of 402 in 15.97s. Final read-back smoke is 5,434.1993ms with + 52,772,864-byte RSS; the complete verifier is 8,194.2032ms. +- Authenticated archive bytes independently downloaded with + `gh run download 29449869519 --repo stablyai/orca --dir /tmp/orca-run-29449869519-evidence`, then + checked with `stat -f '%N\t%z'` and `shasum -a 256`; every result exactly matches its uploaded + identity: + - Linux x64 `.tar.br`: 36,412,421 bytes, + `3cb5abb37eb5e288addb4b8e9219176a052eccc1de64f639effa615ca2e762e1`; + - Linux arm64 `.tar.br`: 36,363,113 bytes, + `8b0e324325ccff3f58d4fb1952073a8cb1b5af3d4d2a2d429f00f7e7b19f4906`; + - macOS x64 `.tar.br`: 32,922,873 bytes, + `fd539f2acfa84342c4bd2e42e84d75a746da4b68254b2842bb90975da843c41f`; + - macOS arm64 `.tar.br`: 31,776,556 bytes, + `2031bea0f820107f8c92ab62a446fe0ebbeb64e78b63b6707c0610216ac68e3d`; + - Windows x64 ZIP: 37,168,778 bytes, + `1db7477d727d14672a919ec5ac35fb6a92ccdabdd80cf4228e877c648be8f466`; + - Windows arm64 ZIP: 33,204,738 bytes, + `0c79ac88aef8b73d6230c2995585449d4b14feddb4021f2c09bbd3ddf8b58acf`. +- GitHub artifact IDs/container evidence: Linux x64 8357070191 / 36,327,344 bytes / digest + `db611cf63a1925ecd715fd59573e4388a3dd51a0ad2e23f56f4380f328dd3e1a`; Linux arm64 8357149220 / + 36,261,905 / `e21e8e1662d5d608194b2f6bee48bee1de0c738c4bc0a117cd80ea9bd7730db2`; macOS x64 + 8357112462 / 32,786,285 / `874c3154b4e6e0c1766a2acecab7b0b0c920d165e11c89932759dc7b2a811615`; + macOS arm64 8357083684 / 31,651,122 / + `86f96887f5295404d3232a7630f88f82647c0f3ad6d5bbdbee232e1002b35393`; Windows x64 8357138535 / + 37,036,207 / `bab660412a7b88c01e77d8b12e2405586cbd2b26f80ab8bca91343642b5a6bf7`; Windows arm64 + 8357270708 / 33,086,421 / `7f8c89f0a405aab29cd4b5f3866f309ef6471492b981fcc04b495b3f6c2e60dc`. +- Fresh-parent regression/floor evidence: Linux x64 supplement job 87470575028 passes in 32s and + Linux arm64 supplement job 87470575020 passes in 1m08s. Each downloads its exact artifact, + reconstructs into the previously failing fresh `$RUNNER_TEMP/baseline-runtime/` path, and + re-verifies the complete tree. Rocky 8 execution proves glibc 2.28 and libstdc++ 6.0.25: x64 smoke + is 163.723423ms / 55,521,280-byte RSS and arm64 is 187.308458ms / 51,802,112-byte RSS. The shared + hosted kernel remains 6.17 rather than the separately required 4.18 floor. +- Windows floors: x64 job 87471620643 on `windows-2022` `20260714.244.1` observes build 20348 and + passes; smoke is 5,357.7362ms / 49,979,392-byte RSS and the complete verifier is 6,548.5886ms. + Arm64 job 87471620692 on `windows-11-arm64` `20260706.102.1` reconstructs and executes successfully; + smoke is 5,879.2748ms / 49,733,632-byte RSS and the complete verifier is 7,884.492ms, then the job + fails only because observed build 26200 does not prove required build 26100. +- Adjacent product evidence: [PR Checks run + 29449869451](https://github.com/stablyai/orca/actions/runs/29449869451), job 87469418118, passes in + 14m14s. [Golden E2E run 29449869407](https://github.com/stablyai/orca/actions/runs/29449869407) + passes Linux job 87469417960 in 4m26s and macOS job 87469417915 in 6m25s. +- Oracle proved: deterministic bounded POSIX Brotli/TAR and Windows ZIP construction, strict archive + inspection/extraction, exact clean-build equality, identity/SBOM/provenance closure, bundled + Node/PTY/watcher smoke, read-back execution, fresh-parent exclusive staging, and Linux glibc / + libstdc++ floors execute on all required native build families under Node 24. +- Consumer-disconnection oracle: there is no packaged manifest/key, desktop extraction/cache caller, + cache publication, SSH transfer/install, mode, tuple enablement, release write, or default behavior. + No runtime artifact is published. Legacy remains the only production path. +- Does not prove: packaged Electron extraction/cache concurrency and resource bounds, live + GitHub/CDN/proxy behavior, client-offline transfer, live SSH/SFTP/system-SSH paths, Linux kernel + 4.18, macOS 13.5, Windows arm64 build 26100, real native signatures/trust, publication, or any + enabled tuple. +- Follow-up: checkpoint this evidence and begin only the disconnected desktop extraction/cache + boundary audit and purpose-named RED. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -12520,11 +12605,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Push fresh-parent correction `af181dcc7` and this evidence checkpoint, then replace exact-head -all-six primary plus both Linux supplement -archive/reproducibility/execution evidence. Do not begin desktop extraction/cache staging until that -prerequisite is green on Node 24. Do not add a desktop call site, SSH transfer/install, mode wiring, -tuple enablement, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, +Audit the disconnected desktop extraction/cache boundary and add only the purpose-named missing +resource/failure-semantics RED. Do not implement a desktop call site, cache publication, SSH +transfer/install, mode wiring, tuple enablement, or default behavior in that audit/test package. Keep +Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. From 7580600e3753e355f9a3876a40e58d23ba03bc9a Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 17:51:26 -0400 Subject: [PATCH 165/267] feat(ssh): add verified relay artifact extraction Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 39 ++ .../ssh-relay-runtime-workflow.test.mjs | 22 +- ...elease-implementation-checklist-summary.md | 27 +- ...github-release-implementation-checklist.md | 240 ++++++++- ...elay-artifact-extraction-full-size.test.ts | 114 ++++ .../ssh/ssh-relay-artifact-extraction.test.ts | 503 ++++++++++++++++++ src/main/ssh/ssh-relay-artifact-extraction.ts | 177 ++++++ .../ssh/ssh-relay-artifact-tar-extraction.ts | 130 +++++ .../ssh-relay-artifact-tree-verification.ts | 103 ++++ .../ssh/ssh-relay-artifact-zip-extraction.ts | 103 ++++ src/main/ssh/ssh-relay-artifact-zip-reader.ts | 285 ++++++++++ 11 files changed, 1724 insertions(+), 19 deletions(-) create mode 100644 src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts create mode 100644 src/main/ssh/ssh-relay-artifact-extraction.test.ts create mode 100644 src/main/ssh/ssh-relay-artifact-extraction.ts create mode 100644 src/main/ssh/ssh-relay-artifact-tar-extraction.ts create mode 100644 src/main/ssh/ssh-relay-artifact-tree-verification.ts create mode 100644 src/main/ssh/ssh-relay-artifact-zip-extraction.ts create mode 100644 src/main/ssh/ssh-relay-artifact-zip-reader.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 3312e25d890..1378fb72afb 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -263,6 +263,7 @@ jobs: src/main/ssh/ssh-relay-release-asset.test.ts \ src/main/ssh/ssh-relay-artifact-selector.test.ts \ src/main/ssh/ssh-relay-artifact-download.test.ts \ + src/main/ssh/ssh-relay-artifact-extraction.test.ts \ src/main/ssh/ssh-relay-runtime-identity.test.ts \ config/scripts/ssh-relay-runtime-compatibility.test.mjs \ config/scripts/ssh-relay-node-release-verification.test.mjs \ @@ -501,6 +502,23 @@ jobs: cp "$first_output"/*.tar.br "$first_output"/*.spdx.json "$first_output"/*.provenance.json "$first_output"/*.identity.json \ "$signing_report" "$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}/" + - name: Measure full-size desktop extraction boundary + shell: bash + run: | + set -euo pipefail + first_output="$RUNNER_TEMP/ssh-relay-runtime/${{ matrix.tuple }}/first" + identity="$first_output/orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json" + archive=$(find "$first_output" -maxdepth 1 -name '*.tar.br' -print -quit) + test -n "$archive" + extraction_output="$RUNNER_TEMP/ssh-relay-runtime/${{ matrix.tuple }}/desktop-extraction-measurement" + # Why: exact payload size can expose memory behavior that synthetic archive fixtures cannot. + ORCA_SSH_RELAY_FULL_SIZE_ARCHIVE="$archive" \ + ORCA_SSH_RELAY_FULL_SIZE_IDENTITY="$identity" \ + ORCA_SSH_RELAY_FULL_SIZE_OUTPUT="$extraction_output" \ + pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 --reporter=verbose \ + src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts + test ! -e "$extraction_output" + - name: Upload unpublished artifact evidence uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: @@ -769,6 +787,7 @@ jobs: src/main/ssh/ssh-relay-release-asset.test.ts ` src/main/ssh/ssh-relay-artifact-selector.test.ts ` src/main/ssh/ssh-relay-artifact-download.test.ts ` + src/main/ssh/ssh-relay-artifact-extraction.test.ts ` src/main/ssh/ssh-relay-runtime-identity.test.ts ` config/scripts/ssh-relay-runtime-compatibility.test.mjs ` config/scripts/ssh-relay-node-release-verification.test.mjs ` @@ -1010,6 +1029,26 @@ jobs: Copy-Item -LiteralPath $signingReport -Destination $evidence Copy-Item -LiteralPath $sourceSignatureReport -Destination $evidence + - name: Measure full-size desktop extraction boundary + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $firstOutput = Join-Path $env:RUNNER_TEMP 'ssh-relay-runtime/${{ matrix.tuple }}/first' + $identity = Join-Path $firstOutput 'orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json' + $archive = Get-ChildItem -LiteralPath $firstOutput -Filter '*.zip' -File | Select-Object -First 1 + if (-not $archive) { throw 'runtime ZIP is missing for desktop extraction measurement' } + $extractionOutput = Join-Path $env:RUNNER_TEMP 'ssh-relay-runtime/${{ matrix.tuple }}/desktop-extraction-measurement' + # Why: exact payload size can expose memory behavior that synthetic archive fixtures cannot. + $env:ORCA_SSH_RELAY_FULL_SIZE_ARCHIVE = $archive.FullName + $env:ORCA_SSH_RELAY_FULL_SIZE_IDENTITY = $identity + $env:ORCA_SSH_RELAY_FULL_SIZE_OUTPUT = $extractionOutput + pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 --reporter=verbose ` + src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts + if ($LASTEXITCODE -ne 0) { throw 'full-size desktop extraction measurement failed' } + if (Test-Path -LiteralPath $extractionOutput) { + throw 'full-size desktop extraction measurement cleanup failed' + } + - name: Upload unpublished artifact evidence uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 1a2a6de185d..f43c628604e 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -94,9 +94,25 @@ describe('SSH relay runtime artifact workflow', () => { const uploadIndex = steps.findIndex( (step) => step.name === 'Upload unpublished artifact evidence' ) + const measurementIndex = steps.findIndex( + (step) => step.name === 'Measure full-size desktop extraction boundary' + ) + const windowsBuildIndex = windowsSteps.findIndex( + (step) => step.name === 'Build twice, inspect, smoke, and compare exact runtime' + ) + const windowsMeasurementIndex = windowsSteps.findIndex( + (step) => step.name === 'Measure full-size desktop extraction boundary' + ) + const windowsUploadIndex = windowsSteps.findIndex( + (step) => step.name === 'Upload unpublished artifact evidence' + ) expect(buildIndex).toBeGreaterThan(-1) - expect(uploadIndex).toBeGreaterThan(buildIndex) + expect(windowsBuildIndex).toBeGreaterThan(-1) + expect(measurementIndex).toBeGreaterThan(buildIndex) + expect(uploadIndex).toBeGreaterThan(measurementIndex) + expect(windowsMeasurementIndex).toBeGreaterThan(windowsBuildIndex) + expect(windowsUploadIndex).toBeGreaterThan(windowsMeasurementIndex) expect(source).toContain('verify-ssh-relay-runtime.mjs') expect(source).toContain('ssh-relay-runtime-workflow.test.mjs') for (const testName of [ @@ -105,12 +121,16 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-release-asset.test.ts', 'ssh-relay-artifact-selector.test.ts', 'ssh-relay-artifact-download.test.ts', + 'ssh-relay-artifact-extraction.test.ts', 'ssh-relay-runtime-identity.test.ts' ]) { // Why: portable desktop selection contracts need proof on every native runner family, not // only the local client architecture or the generic Linux PR job. expect(source.split(`src/main/ssh/${testName}`)).toHaveLength(3) } + expect( + source.split('src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts') + ).toHaveLength(3) expect(source).toContain('pnpm install --frozen-lockfile --ignore-scripts') expect(source).toContain('--connect-timeout 20 --max-time 300 --retry 2') expect(source).toContain('mkdir -p "$output_root"') 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 7176802a3f2..95294f2efce 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 @@ -9,16 +9,21 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — in-progress disconnected -extraction/cache contract audit, 2026-07-15, Codex implementation owner.** Exact-head run +Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — in-progress native-gate +preparation for disconnected extraction, 2026-07-15, Codex implementation owner.** Exact-head run [29449869519](https://github.com/stablyai/orca/actions/runs/29449869519) closes deterministic bounded `.tar.br` under `E-M5-PORTABLE-ARCHIVE-CI-001`: all six primary Node 24 jobs and both Linux oldest-userland supplements pass. PR Checks and Golden E2E are green; Windows x64 floor passes and Windows arm64 passes smoke before the retained hosted build-26200 versus required-26100 rejection. -The next safe slice is only the isolated desktop extraction/cache resource and failure-semantics -audit followed by a purpose-named RED. Desktop consumers, SSH transfer/install, mode wiring, -tuple enablement, publication, production keys/environment/seed, and merge to `main` remain -disconnected. +The isolated desktop extractor is locally green under `E-M5-ARTIFACT-EXTRACTION-LOCAL-001` and +`E-M5-ARTIFACT-EXTRACTION-BROAD-LOCAL-001`: 17 focused tests cover both archive families, hostile +inputs, exact tree/modes, mutation, cancellation, cleanup, and ownership; broad relay/release/static +gates pass. An exact downloaded 122,027,869-byte Darwin arm64 tree extracts in 878.73ms with +49,283,072-byte incremental RSS under `E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001`. Both native job +families now require synthetic cross-family and post-build full-size gates under +`E-M5-ARTIFACT-EXTRACTION-CI-WIRING-LOCAL-001`. Next is exact-head all-six Node 24 execution. Desktop +consumers, cache publication, SSH transfer/install, mode wiring, tuple enablement, publication, +production keys/environment/seed, and merge to `main` remain disconnected. ## Safety status @@ -307,9 +312,15 @@ disconnected. - [ ] Stream bounded downloads; verify signature, size, archive hash, and extracted tree. The disconnected Electron downloader is locally green under `E-M5-ARTIFACT-DOWNLOAD-LOCAL-RED-001` and `E-M5-ARTIFACT-DOWNLOAD-LOCAL-001`; exact-head - all-six native CI is closed under `E-M5-ARTIFACT-DOWNLOAD-CI-001`. Packaged signature loading - and extracted-tree verification remain open. + all-six native CI is closed under `E-M5-ARTIFACT-DOWNLOAD-CI-001`. Disconnected strict + TAR/Brotli and ZIP extraction plus complete-tree verification are locally green under + `E-M5-ARTIFACT-EXTRACTION-LOCAL-001`; an exact full-size Actions payload is locally within time + and memory budgets under `E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001`. Native all-six + execution and packaged signature loading remain open. - [ ] Add exclusive staging, atomic publication, quarantine, locking, and the 2 GiB cache policy. + Exclusive owned staging and failure/cancellation cleanup are locally green under + `E-M5-ARTIFACT-EXTRACTION-LOCAL-001`; publication, quarantine, locking, and eviction are not + implemented yet. - [ ] Prove verified cached bytes can be transferred while the client is offline. - [ ] Preserve `ORCA_RELAY_PATH` behind the official-build trust boundary. 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 50e14f97e3c..9a94d3a3a4a 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 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: disconnected desktop extraction/cache contract audit and RED**. Exact-head run 29449869519 closes the portable POSIX artifact prerequisite under E-M5-PORTABLE-ARCHIVE-CI-001: all six primary Node 24 jobs and both Linux oldest-userland supplements pass deterministic build, strict inspection/extraction, bundled Node/PTY/watcher smoke, equality, and read-back. Audit the existing desktop boundaries and resource/failure contracts, then add only a purpose-named missing extraction/cache-staging RED. Do not add a desktop caller, cache publication, SSH transfer/install, tuple enablement, mode wiring, or default behavior.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact implementation/evidence head `3080eda77fd6a11ed87f536c1963e5b67abe7621` passes artifact run 29449869519 on all six primary native jobs and both Linux supplements. PR Checks 29449869451 and both Golden E2E jobs in run 29449869407 are green. Windows x64 oldest-floor execution passes; Windows arm64 reconstructs and smokes the exact runtime in 7,884.492ms with 49,733,632-byte RSS, then rejects hosted build 26200 against required build 26100. E-M5-PORTABLE-ARCHIVE-CI-001 records exact jobs, runner images, tests, artifact IDs/sizes/hashes, and smoke metrics. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no release write, publication caller, desktop consumer, cache publication/extraction, SSH transfer/install, tuple enablement, or production/default behavior change. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: disconnected desktop extraction exact-head native gate**. Focused, broad, static, exact downloaded-payload, and native-workflow wiring gates pass locally under E-M5-ARTIFACT-EXTRACTION-LOCAL-001, E-M5-ARTIFACT-EXTRACTION-BROAD-LOCAL-001, E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001, and E-M5-ARTIFACT-EXTRACTION-CI-WIRING-LOCAL-001. Checkpoint the exact implementation SHA, push it, and require all-six Node 24 synthetic cross-family plus native full-size evidence before cache publication begins. Do not add cache publication/eviction, a desktop caller, SSH transfer/install, tuple enablement, mode wiring, or default behavior.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — the 17-test focused suite, 81-test artifact suite, 249-pass relay suite, 279-test release suite, typecheck, lint, and diff gates pass. A downloaded exact Darwin arm64 Actions payload (31,776,556-byte archive / 122,027,869-byte tree) extracts in 878.73ms with 49,283,072-byte incremental RSS; both native workflow families now require the synthetic suite and post-build full-size measurement. Native Node 24 evidence remains open. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no release write, publication caller, cache publication, SSH transfer/install, tuple enablement, or production/default behavior change. 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) @@ -138,10 +138,10 @@ same change as the work it records. archive, correction, all-six primary jobs, and both Linux oldest-userland supplements. Windows ZIP and upstream Node `.tar.xz` remain unchanged. - Active package: **In progress — 2026-07-15, Codex implementation owner** — disconnected desktop - extraction/cache contract audit and purpose-named RED. Inventory existing cache/path/atomic-file - boundaries, formalize resource and integrity failure oracles, and prove the missing contract before - implementation. Keep cache publication, desktop call sites, SSH behavior, mode wiring, tuple - enablement, release writes, and defaults intact. + extraction capability after E-M5-ARTIFACT-EXTRACTION-AUDIT-001 and + E-M5-ARTIFACT-EXTRACTION-LOCAL-RED-001. Implement only strict TAR/Brotli and ZIP staging plus full + signed-tree verification and bounded cleanup. Keep cache publication/eviction, desktop call sites, + SSH behavior, mode wiring, tuple enablement, release writes, and defaults intact. - 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 @@ -1084,8 +1084,11 @@ Suggested concrete modules: - [x] Verify archive SHA-256 before extraction. E-M5-ARTIFACT-DOWNLOAD-LOCAL-001 and E-M5-ARTIFACT-DOWNLOAD-CI-001. - [ ] Extract into an exclusive temporary directory with all archive-safety checks from Milestone 2. - Begin only after the `.tar.br` prerequisite has exact-head all-six artifact evidence. + Implemented locally under E-M5-ARTIFACT-EXTRACTION-LOCAL-001 after the `.tar.br` prerequisite; + native cross-client and exact full-size evidence remain required before this item closes. - [ ] Verify the exact expanded tree before atomically publishing the cache entry. + The disconnected full-tree verifier passes locally under E-M5-ARTIFACT-EXTRACTION-LOCAL-001; + atomic cache publication is deliberately absent and remains a later package. - [ ] Coordinate cache ownership across multiple Orca processes and windows. - [ ] Recover stale download/extraction locks without deleting an active writer. - [ ] Never select partial files, partial directories, or entries with a failed verification record. @@ -12547,6 +12550,222 @@ config/scripts/ssh-relay-runtime-portable-archive.test.mjs`; changed-file `oxfmt - Follow-up: checkpoint this evidence and begin only the disconnected desktop extraction/cache boundary audit and purpose-named RED. +### E-M5-ARTIFACT-EXTRACTION-AUDIT-001 — Desktop extraction boundary and package split + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact local/pushed head `a93e67413a2a1446e8e0f7348af1fdf72737287d` after portable archive + evidence; draft PR #8741. +- Exact audit commands: `rg --files src/main/ssh config/scripts | rg 'ssh-relay|relay-runtime' | +sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete reads of the desktop + schema/consistency/path-policy/selector/downloader plus release TAR/Brotli, ZIP, extraction, and + full-tree verification modules. +- Findings: + - the desktop has a signature-verified immutable selected-artifact value and an exclusive, + streaming size/hash-verified downloader, but no extraction or cache module and no consumer; + - release-job extractors under `config/scripts` are not packaged desktop modules and use the + five-minute release boundary rather than the two-minute desktop extraction budget; + - the verified selected artifact already carries the exact content ID, archive name/size/hash, + entry paths/types/modes/sizes/hashes, file count, expanded size, and portable native-attestation + hashes required for cross-client verification; + - extraction must revalidate the regular archive file and its signed size/hash before staging, + strictly inspect the authenticated archive family, extract only into an exclusive leaf, stream a + complete no-extra-file tree verification, detect source mutation, and remove only staging it + created on every failure/cancellation; + - native execution/trust probes are deliberately not part of desktop cross-client extraction. + Their signed hashes are bound by tuple consistency; target-native execution remains a later + remote-install gate. +- Package split: `ssh-relay-artifact-extraction.ts` owns input state, timeout, exclusive staging, and + cleanup; `ssh-relay-artifact-tar-extraction.ts` owns strict bounded TAR/Brotli behavior; + `ssh-relay-artifact-zip-extraction.ts` owns strict bounded ZIP behavior; and + `ssh-relay-artifact-tree-verification.ts` owns streamed complete-tree verification. Cache locking, + publication records, quarantine, eviction, and lookup stay in the later + `ssh-relay-artifact-cache.ts` package. +- Budgets: two-minute extraction timeout; 65,536-byte stream chunks; 64 MiB maximum measured desktop + incremental RSS; no buffer over 1 MiB; archive/expanded/file/count/path limits remain signed-schema + constraints. This package must expose the constants for contract and measurement tests. +- Consumer-disconnection oracle: this audit and the next capability do not create a cache root, + publish an entry, load an embedded key/manifest, call the downloader, transfer over SSH, enable a + tuple/mode, or change the legacy default. +- Does not prove: implementation, hostile archive behavior, packaged Electron behavior, full-size + memory/latency, cache concurrency/publication/quarantine/eviction, live SSH, native trust, or any + enabled tuple. +- Follow-up: add the purpose-named cross-family RED, then implement only the disconnected extraction + capability across the concrete modules above. + +### E-M5-ARTIFACT-EXTRACTION-LOCAL-RED-001 — Packaged desktop extraction capability is absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted purpose-named + `src/main/ssh/ssh-relay-artifact-extraction.test.ts` atop audit head `a93e67413`. +- Exact command: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-artifact-extraction.test.ts` +- Environment: macOS 26.2 arm64, Node v26.0.0, pnpm 10.24.0, Vitest 4.1.5. +- Result: required RED; one failed suite / zero collected tests in 1.75s Vitest / 7.67s wall with + 132,104,192-byte maximum RSS and zero swaps. Import fails because + `./ssh-relay-artifact-extraction` does not exist. +- Contract encoded before implementation: + - synthesize real TAR/Brotli and ZIP archives on every client OS; + - expose exact 120,000ms / 65,536-byte / 64 MiB incremental-RSS / 1 MiB maximum-buffer budgets; + - revalidate signed archive size/hash, strictly reject mismatched and undeclared entries, verify + every extracted file byte, and accept no extra tree entry; + - create a fresh parent but an exclusive final staging leaf; settle pre-cancellation; remove only + owned partial output and preserve an existing owner. +- Does not prove: implementation, collected test behavior, Node 24/native clients, full-size memory, + packaged Electron extraction, cache publication/concurrency, SSH, or any enabled tuple. +- Follow-up: implement the smallest disconnected extraction capability without cache or consumer + wiring, then run focused/release/desktop/static gates and require native CI. + +### E-M5-ARTIFACT-EXTRACTION-LOCAL-001 — Disconnected desktop extraction passes locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted extraction capability and purpose suite atop pushed head + `a93e67413a2a1446e8e0f7348af1fdf72737287d`; the exact implementation SHA will be appended after + the local broad gates pass and this evidence is checkpointed. +- Environment: macOS 26.2 arm64, Node v26.0.0, pnpm 10.24.0, Vitest 4.1.5. Node 24/native client + execution remains the next gate. +- Exact focused command: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-artifact-extraction.test.ts` +- Focused result: one passed file / 17 passed tests in 1.65s Vitest / 4.15s wall; 158,105,600-byte + maximum RSS, 96,179,552-byte peak memory footprint, zero swaps, and 353 page faults. The observed + process maximum is 26,001,408 bytes above the 132,104,192-byte missing-module RED process baseline, + below the 64 MiB incremental contract. This is a synthetic 8 MiB cancellation/mutation fixture, + not the separately required exact full-size artifact measurement. +- Exact static commands: + `pnpm exec oxlint src/main/ssh/ssh-relay-artifact-extraction.ts src/main/ssh/ssh-relay-artifact-tar-extraction.ts src/main/ssh/ssh-relay-artifact-tree-verification.ts src/main/ssh/ssh-relay-artifact-zip-reader.ts src/main/ssh/ssh-relay-artifact-zip-extraction.ts src/main/ssh/ssh-relay-artifact-extraction.test.ts`; + `pnpm typecheck`. +- Static result: both commands exit zero. The implementation is split by concrete responsibility; + no max-lines disable or vague helper/common module was added. +- Oracle proved: + - only the immutable selected tuple/archive identity crosses the boundary; the exact signed regular + archive size/hash and filesystem state are checked before staging and again after extraction; + - TAR/Brotli and ZIP are strictly inspected and streamed on every client OS into an exclusive leaf + beneath a resolved parent; POSIX modes and every signed path/type/size/hash are revalidated; + - extra/traversal/truncated/case-fold-colliding entries, ZIP symbolic links/encryption shapes, + wrong bytes, archive mutation, and existing-owner collisions fail closed; + - pre-cancellation and cancellation after staging begins settle, remove only owned partial output, + and leave no eligible partial runtime tree; + - extraction retains the declared 120,000ms timeout, 65,536-byte chunks, 64 MiB measured + incremental-RSS budget, and 1 MiB maximum-write-buffer contract. +- Consumer-disconnection oracle: there is no cache root or publication record, downloader/manifest + loader caller, SSH transfer/install, runtime execution, target setting, Beta option, tuple + enablement, release write, or artifact publication. Legacy remains the only production/default + path. +- Does not prove: Node 24 or native Windows/Linux/macOS clients, packaged Electron behavior, exact + full-size runtime memory/latency, cache locking/publication/quarantine/eviction, live SSH, native + trust, or any enabled tuple. +- Follow-up: run the broad relay/release/static gates, prove the focused source suite is invoked by + both POSIX and Windows native artifact job families, then collect all-six Node 24 and exact + full-size payload evidence before beginning cache publication. + +### E-M5-ARTIFACT-EXTRACTION-BROAD-RED-001 — Broad run exposes late rejection observer + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted E-M5-ARTIFACT-EXTRACTION-LOCAL-001 tree atop `a93e67413`. +- Exact command: + `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-artifact-*.test.ts` +- Result: required RED; three passed files / one failed file, 80 passed tests / one failed test, and + one unhandled rejection in 2.76s. The authenticated mutation test correctly fails closed with + `unexpected EOF`, but can reject during its awaited `writeFile` before the later Vitest assertion + attaches, so the runner reports `PromiseRejectionHandledWarning` and an unhandled rejection. +- Classification: purpose-test timing defect, not a product extraction acceptance. The owned staging + tree is still removed and the mutation is rejected. +- Correction: attach the Vitest rejection observer when extraction starts, before waiting for the + staging path or mutating/aborting; accept the valid fail-closed `unexpected EOF` diagnostic. +- Follow-up: rerun the exact broad artifact command and require zero unhandled errors before any CI + checkpoint. + +### E-M5-ARTIFACT-EXTRACTION-BROAD-LOCAL-001 — Broad local relay and release gates pass + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: corrected local extraction tree atop `a93e67413`; exact implementation SHA pending the + checkpoint commit. +- Exact commands and results: + - `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-artifact-*.test.ts`: + four passed files / 81 passed tests in 10.40s after the mutation observer correction, with zero + unhandled errors; + - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts`: + 17 passed files / one platform-skipped file, 249 passed tests / one platform-skipped test in + 21.60s Vitest / 24.97s wall, 244,383,744-byte maximum process RSS and zero swaps; + - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs`: + 50 passed files / 279 passed tests in 45.04s Vitest / 47.58s wall, 190,840,832-byte maximum + process RSS and zero swaps; + - `pnpm typecheck`; `pnpm lint`; `git diff --check`: all exit zero. Full lint retains only existing + warnings outside this package; reliability gates, max-lines ratchet, bundled skills, localization + catalog, and localization coverage pass. +- Diff oracle: no diff exists in the preserved Milestone 0 resolver pair or + `config/scripts/verify-release-required-assets.mjs` and its test. Only the concrete extraction, + measurement, workflow-wiring, and living-plan files are in scope. +- Does not prove: Node 24/native clients, exact full-size payload behavior, cache, live SSH, native + trust, or any enabled tuple. +- Follow-up: measure an exact downloaded payload, close native workflow wiring, then checkpoint and + push the implementation for all-six execution. + +### E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001 — Exact downloaded payload stays within budget + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source artifact: Actions artifact ID 8357083684, `ssh-relay-runtime-darwin-arm64`, downloaded from + exact implementation run [29449869519](https://github.com/stablyai/orca/actions/runs/29449869519) + using `gh run download 29449869519 --repo stablyai/orca --name ssh-relay-runtime-darwin-arm64`. +- Payload: `darwin-arm64`, archive SHA-256 + `2031bea0f820107f8c92ab62a446fe0ebbeb64e78b63b6707c0610216ac68e3d`, 31,776,556 compressed + bytes, 122,027,869 expanded bytes, and 35 files. +- Exact measurement command: set `ORCA_SSH_RELAY_FULL_SIZE_ARCHIVE`, + `ORCA_SSH_RELAY_FULL_SIZE_IDENTITY`, and an absent owned `ORCA_SSH_RELAY_FULL_SIZE_OUTPUT`, then + run `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 --reporter=verbose src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts`. +- Result: four consecutive post-harness runs pass locally. The retained verbose run extracts, + complete-tree verifies, and cleans the exact payload in 878.731667ms with a 72,679,424-byte + baseline RSS, 121,962,496-byte sampled peak RSS, and 49,283,072-byte incremental RSS. This is below + the 120,000ms and 67,108,864-byte limits. +- Harness boundary: the measurement uses the authenticated Actions identity only to construct the + top-level selected-artifact shape; its explicit why-comment prohibits product use. The production + extractor still rehashes the archive, inspects it, creates exclusive staging, verifies the complete + tree, rehashes the archive, and cleans output. Signature selection itself is separately proven by + E-M5-OFFLINE-SELECTION-LOCAL-001/CI-001. +- Does not prove: Node 24, other native clients or archive families at full size, packaged Electron, + cache, SSH, or an enabled tuple. +- Follow-up: require this measurement after native build and before upload on all six artifact jobs; + retain synthetic both-family execution on every client runner. + +### E-M5-ARTIFACT-EXTRACTION-CI-WIRING-RED-001 — Native jobs omit extraction proof + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Exact command: + `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-workflow.test.mjs` +- Result: required RED; one failed / six passed tests in 483ms. The new source contract expects the + purpose suite in both native job families, but the workflow contains zero occurrences, so the + split count is one rather than three. +- Follow-up: invoke the synthetic cross-family suite in both native contract steps and add a + post-build full-size budget measurement before artifact upload in both job families. + +### E-M5-ARTIFACT-EXTRACTION-CI-WIRING-LOCAL-001 — Both native job families require extraction gates + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: local workflow/test/full-size harness atop `a93e67413`; exact implementation SHA pending + the checkpoint commit. +- Exact command: + `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-workflow.test.mjs` +- Result: one passed file / seven passed tests in 404ms after formatting; typecheck also exits zero. +- Oracle proved: all four POSIX native clients and both Windows native clients run the 17-test + synthetic TAR/Brotli-plus-ZIP suite under Node 24. After each exact native build, a separate step + runs the one-test full-size measurement against that job's first verified archive/identity, checks + 120,000ms / 64 MiB, cleans its owned output, and must pass before upload. The workflow contract + requires both measurement steps to occur after build and before upload. +- Consumer-disconnection oracle: workflow artifacts remain unpublished Actions evidence; no release + write, packaged desktop consumer, cache publication, SSH transfer/install, tuple, or mode is + connected. +- Does not prove: actual runner execution, cross-family full-size payloads on every client, packaged + Electron, cache, SSH, or any enabled tuple. +- Follow-up: checkpoint, push, and collect exact-head per-job synthetic/full-size metrics from all six + native jobs before beginning the cache-publication package. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -12605,9 +12824,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Audit the disconnected desktop extraction/cache boundary and add only the purpose-named missing -resource/failure-semantics RED. Do not implement a desktop call site, cache publication, SSH -transfer/install, mode wiring, tuple enablement, or default behavior in that audit/test package. Keep +Run the broader release/desktop/static gates for the locally green disconnected extraction +capability, wire its purpose suite into both native artifact job families, and collect exact-head +all-six Node 24 plus full-size payload evidence. Do not add a desktop call site, cache +publication/eviction, SSH transfer/install, mode wiring, tuple enablement, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, diff --git a/src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts b/src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts new file mode 100644 index 00000000000..bd4f4e2c08a --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts @@ -0,0 +1,114 @@ +import { readFile, rm, stat } from 'node:fs/promises' +import { performance } from 'node:perf_hooks' + +import { describe, expect, it } from 'vitest' + +import { + extractSshRelayArtifact, + SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS +} from './ssh-relay-artifact-extraction' +import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector' + +type MeasurementIdentity = { + tupleId: SshRelaySelectedArtifact['tupleId'] + contentId: SshRelaySelectedArtifact['contentId'] + os: SshRelaySelectedArtifact['tuple']['os'] + archive: SshRelaySelectedArtifact['tuple']['archive'] + entries: SshRelaySelectedArtifact['tuple']['entries'] +} + +const archivePath = process.env.ORCA_SSH_RELAY_FULL_SIZE_ARCHIVE +const identityPath = process.env.ORCA_SSH_RELAY_FULL_SIZE_IDENTITY +const outputDirectory = process.env.ORCA_SSH_RELAY_FULL_SIZE_OUTPUT +const hasMeasurementInput = Boolean(archivePath && identityPath && outputDirectory) + +function measurementIdentity(input: unknown): MeasurementIdentity { + if ( + typeof input !== 'object' || + input === null || + !('tupleId' in input) || + !('contentId' in input) || + !('os' in input) || + !('archive' in input) || + !('entries' in input) || + !Array.isArray(input.entries) + ) { + throw new Error('Full-size extraction measurement identity is incomplete') + } + return input as MeasurementIdentity +} + +function measurementArtifact(identity: MeasurementIdentity): SshRelaySelectedArtifact { + const tuple = identity as unknown as SshRelaySelectedArtifact['tuple'] + // Why: this runner measures exact Actions artifact resources; it is never a product trust bypass. + return Object.freeze({ + kind: 'selected', + tupleId: identity.tupleId, + contentId: identity.contentId, + releaseTag: 'measurement-only', + archive: Object.freeze({ + ...identity.archive, + downloadUrl: 'https://invalid.example/measurement-only' + }), + tuple + }) +} + +describe.skipIf(!hasMeasurementInput)('SSH relay full-size artifact extraction', () => { + it( + 'stays within the desktop time and incremental-memory budgets', + async () => { + const identity = measurementIdentity( + JSON.parse(await readFile(identityPath as string, 'utf8')) as unknown + ) + await expect(stat(outputDirectory as string)).rejects.toMatchObject({ code: 'ENOENT' }) + const baselineRss = process.memoryUsage().rss + let peakRss = baselineRss + const sample = (): void => { + peakRss = Math.max(peakRss, process.memoryUsage().rss) + } + const sampler = setInterval(sample, 1) + const startedAt = performance.now() + let result + try { + result = await extractSshRelayArtifact({ + artifact: measurementArtifact(identity), + archivePath: archivePath as string, + outputDirectory: outputDirectory as string + }) + } finally { + clearInterval(sampler) + sample() + } + const elapsedMs = performance.now() - startedAt + const incrementalRssBytes = Math.max(0, peakRss - baselineRss) + console.log( + `ssh_relay_full_size_extraction=${JSON.stringify({ + tupleId: identity.tupleId, + archiveBytes: identity.archive.size, + expandedBytes: identity.archive.expandedSize, + files: identity.archive.fileCount, + elapsedMs, + baselineRss, + peakRss, + incrementalRssBytes + })}` + ) + try { + expect(result).toMatchObject({ + tupleId: identity.tupleId, + contentId: identity.contentId, + files: identity.archive.fileCount, + expandedBytes: identity.archive.expandedSize + }) + expect(elapsedMs).toBeLessThanOrEqual(SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS.timeoutMs) + expect(incrementalRssBytes).toBeLessThanOrEqual( + SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS.maximumIncrementalMemoryBytes + ) + } finally { + await rm(outputDirectory as string, { recursive: true }) + } + }, + SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS.timeoutMs + 10_000 + ) +}) diff --git a/src/main/ssh/ssh-relay-artifact-extraction.test.ts b/src/main/ssh/ssh-relay-artifact-extraction.test.ts new file mode 100644 index 00000000000..44d1519a918 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-extraction.test.ts @@ -0,0 +1,503 @@ +import { createHash } from 'node:crypto' +import { lstat, mkdtemp, readFile, realpath, rm, stat, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { brotliCompressSync, constants as zlibConstants } from 'node:zlib' + +import nacl from 'tweetnacl' +import { afterEach, describe, expect, it } from 'vitest' +import yazl from 'yazl' + +import { + extractSshRelayArtifact, + SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS +} from './ssh-relay-artifact-extraction' +import type { SshRelayArtifactManifest, SshRelayRuntimeTuple } from './ssh-relay-artifact-schema' +import { + createSshRelayArtifactTestManifest, + createSshRelayWindowsArtifactTestManifest +} from './ssh-relay-artifact-test-manifest' +import { + selectSshRelayArtifact, + type SshRelayHostEvidence, + type SshRelaySelectedArtifact +} from './ssh-relay-artifact-selector' +import { + signSshRelayArtifactManifest, + sshRelayManifestKeyId, + verifySshRelayArtifactManifest +} from './ssh-relay-manifest-signature' +import { sshRelayRuntimeArchiveName } from './ssh-relay-release-asset' +import { computeSshRelayRuntimeContentId, type SshRelayDigest } from './ssh-relay-runtime-identity' + +const keyPair = nacl.sign.keyPair.fromSeed(Uint8Array.from({ length: 32 }, (_, index) => index)) +const temporaryDirectories: string[] = [] + +function sha256(bytes: Uint8Array): SshRelayDigest { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +function tarString(header: Buffer, value: string, offset: number, length: number): void { + header.write(value, offset, length, 'utf8') +} + +function tarOctal(header: Buffer, value: number, offset: number, length: number): void { + header.write(value.toString(8).padStart(length - 1, '0'), offset, length - 1, 'ascii') + header[offset + length - 1] = 0 +} + +function tarHeader(entry: SshRelayRuntimeTuple['entries'][number], size: number): Buffer { + const header = Buffer.alloc(512) + tarString(header, entry.type === 'directory' ? `${entry.path}/` : entry.path, 0, 100) + tarOctal(header, entry.mode, 100, 8) + tarOctal(header, 0, 108, 8) + tarOctal(header, 0, 116, 8) + tarOctal(header, size, 124, 12) + tarOctal(header, 0, 136, 12) + header.fill(0x20, 148, 156) + header[156] = entry.type === 'directory' ? 0x35 : 0x30 + tarString(header, 'ustar\0', 257, 6) + tarString(header, '00', 263, 2) + const checksum = header.reduce((total, byte) => total + byte, 0) + header.write(`${checksum.toString(8).padStart(6, '0')}\0 `, 148, 8, 'ascii') + return header +} + +function tarBrotli( + tuple: SshRelayRuntimeTuple, + fileBytes: ReadonlyMap, + extraPath?: string +): Buffer { + const blocks: Buffer[] = [] + for (const entry of tuple.entries) { + const bytes = entry.type === 'file' ? fileBytes.get(entry.path) : undefined + if (entry.type === 'file' && !bytes) { + throw new Error(`Missing test bytes for ${entry.path}`) + } + blocks.push(tarHeader(entry, bytes?.length ?? 0)) + if (bytes) { + blocks.push(bytes, Buffer.alloc((512 - (bytes.length % 512)) % 512)) + } + } + if (extraPath) { + const bytes = Buffer.from('undeclared archive bytes') + blocks.push( + tarHeader( + { + path: extraPath, + type: 'file', + role: 'license', + mode: 0o644, + size: 0, + sha256: sha256(bytes) + }, + bytes.length + ), + bytes, + Buffer.alloc((512 - (bytes.length % 512)) % 512) + ) + } + blocks.push(Buffer.alloc(1024)) + return brotliCompressSync(Buffer.concat(blocks), { + params: { + [zlibConstants.BROTLI_PARAM_QUALITY]: 9, + [zlibConstants.BROTLI_PARAM_LGWIN]: 20 + } + }) +} + +async function zipArchive( + tuple: SshRelayRuntimeTuple, + fileBytes: ReadonlyMap, + { + extraPath, + symlinkPath + }: { + extraPath?: string + symlinkPath?: string + } +): Promise { + const zip = new yazl.ZipFile() + const mtime = new Date('2026-07-14T00:00:00.000Z') + for (const entry of tuple.entries) { + const options = { mode: entry.mode, mtime, forceDosTimestamp: true } + if (entry.type === 'directory') { + zip.addEmptyDirectory(entry.path, options) + } else { + const bytes = fileBytes.get(entry.path) + if (!bytes) { + throw new Error(`Missing test bytes for ${entry.path}`) + } + zip.addBuffer(bytes, entry.path, { + ...options, + mode: entry.path === symlinkPath ? 0o120777 : entry.mode, + compress: true, + compressionLevel: 9 + }) + } + } + if (extraPath) { + zip.addBuffer(Buffer.from('undeclared archive bytes'), extraPath, { + mode: 0o644, + mtime, + forceDosTimestamp: true + }) + } + zip.end({ forceZip64Format: false }) + const chunks: Buffer[] = [] + for await (const chunk of zip.outputStream) { + chunks.push(Buffer.from(chunk)) + } + return Buffer.concat(chunks) +} + +function markZipEntriesEncrypted(archive: Buffer): Buffer { + const changed = Buffer.from(archive) + for (let offset = 0; offset <= changed.length - 10; offset += 1) { + const signature = changed.readUInt32LE(offset) + const flagOffset = + signature === 0x04034b50 ? offset + 6 : signature === 0x02014b50 ? offset + 8 : -1 + if (flagOffset >= 0) { + changed.writeUInt16LE(changed.readUInt16LE(flagOffset) | 0x0001, flagOffset) + } + } + return changed +} + +type Fixture = { + archivePath: string + artifact: SshRelaySelectedArtifact + fileBytes: ReadonlyMap + root: string +} + +async function fixture({ + os, + mismatchedPath, + extraPath, + largeFileBytes, + truncated, + encryptedZip, + symlinkPath +}: { + os: 'linux' | 'win32' + mismatchedPath?: string + extraPath?: string + largeFileBytes?: number + truncated?: boolean + encryptedZip?: boolean + symlinkPath?: string +}): Promise { + const root = await mkdtemp(join(tmpdir(), 'orca-relay-extraction-')) + temporaryDirectories.push(root) + const manifest: SshRelayArtifactManifest = + os === 'win32' + ? createSshRelayWindowsArtifactTestManifest() + : createSshRelayArtifactTestManifest() + const tuple = manifest.tuples[0] + const fileBytes = new Map() + const firstFilePath = tuple.entries.find((entry) => entry.type === 'file')?.path + for (const entry of tuple.entries) { + if (entry.type !== 'file') { + continue + } + const bytes = + entry.path === firstFilePath && largeFileBytes + ? Buffer.alloc(largeFileBytes, 0x61) + : Buffer.from(`desktop extraction fixture:${tuple.tupleId}:${entry.path}`) + fileBytes.set(entry.path, bytes) + entry.size = bytes.length + entry.sha256 = sha256(bytes) + } + for (const attestation of tuple.nativeVerification.files) { + const entry = tuple.entries.find((candidate) => candidate.path === attestation.path) + if (!entry || entry.type !== 'file') { + throw new Error(`Missing attested fixture entry: ${attestation.path}`) + } + attestation.sha256 = entry.sha256 + } + tuple.contentId = computeSshRelayRuntimeContentId(tuple) + tuple.archive.name = sshRelayRuntimeArchiveName(tuple.tupleId, tuple.contentId) + const files = tuple.entries.filter((entry) => entry.type === 'file') + tuple.archive.fileCount = files.length + tuple.archive.expandedSize = files.reduce((total, entry) => total + entry.size, 0) + + const archiveFileBytes = new Map(fileBytes) + if (mismatchedPath) { + archiveFileBytes.set(mismatchedPath, Buffer.from('authenticated archive has wrong tree bytes')) + } + let archive = + os === 'win32' + ? await zipArchive(tuple, archiveFileBytes, { extraPath, symlinkPath }) + : tarBrotli(tuple, archiveFileBytes, extraPath) + if (encryptedZip) { + archive = markZipEntriesEncrypted(archive) + } + if (truncated) { + archive = archive.subarray(0, Math.max(1, archive.length - 64)) + } + tuple.archive.size = archive.length + tuple.archive.sha256 = sha256(archive) + manifest.signatures = [signSshRelayArtifactManifest(manifest, keyPair.secretKey)] + const verified = verifySshRelayArtifactManifest(manifest, [ + { keyId: sshRelayManifestKeyId(keyPair.publicKey), publicKey: keyPair.publicKey } + ]) + const host: SshRelayHostEvidence = + os === 'win32' + ? { + os, + architecture: 'x64', + processTranslated: false, + build: 22631, + openSshVersion: '9.5p1', + powerShellVersion: '5.1', + dotNetFrameworkRelease: 528040 + } + : { + os, + architecture: 'x64', + processTranslated: false, + kernelVersion: '6.8', + libc: { family: 'glibc', version: '2.39' }, + libstdcxxVersion: '6.0.33', + glibcxxVersion: '3.4.33' + } + const selected = selectSshRelayArtifact(verified, host) + if (selected.kind !== 'selected') { + throw new Error(`Expected selected fixture, got ${selected.reason}`) + } + const archivePath = join(root, tuple.archive.name) + await writeFile(archivePath, archive, { mode: 0o600 }) + return { archivePath, artifact: selected, fileBytes, root } +} + +async function waitForPath(path: string): Promise { + const deadline = Date.now() + 5_000 + for (;;) { + try { + await stat(path) + return + } catch (error) { + if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT') { + throw error + } + if (Date.now() >= deadline) { + throw new Error(`Timed out waiting for test path: ${path}`) + } + await new Promise((resolve) => setTimeout(resolve, 1)) + } + } +} + +afterEach(async () => { + await Promise.all( + temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true })) + ) +}) + +describe('SSH relay artifact extraction', () => { + it('declares the desktop extraction resource contract', () => { + expect(SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS).toEqual({ + timeoutMs: 2 * 60_000, + chunkBytes: 64 * 1024, + maximumIncrementalMemoryBytes: 64 * 1024 * 1024, + maximumWriteBufferBytes: 1024 * 1024 + }) + }) + + it.each(['linux', 'win32'] as const)( + 'extracts and verifies an authenticated %s tree into an exclusive staging directory', + async (os) => { + const value = await fixture({ os }) + const outputDirectory = join(value.root, 'fresh-parent', 'runtime') + + const result = await extractSshRelayArtifact({ + artifact: value.artifact, + archivePath: value.archivePath, + outputDirectory + }) + expect(result).toEqual({ + tupleId: value.artifact.tupleId, + contentId: value.artifact.contentId, + runtimeRoot: await realpath(outputDirectory), + files: value.artifact.tuple.archive.fileCount, + expandedBytes: value.artifact.tuple.archive.expandedSize + }) + for (const [path, bytes] of value.fileBytes) { + expect(await readFile(join(outputDirectory, ...path.split('/')))).toEqual(bytes) + } + if (process.platform !== 'win32') { + for (const entry of value.artifact.tuple.entries) { + const metadata = await lstat(join(outputDirectory, ...entry.path.split('/'))) + expect(metadata.mode & 0o777, entry.path).toBe(entry.mode) + } + } + } + ) + + it('rejects changed archive bytes before creating staging output', async () => { + const value = await fixture({ os: 'linux' }) + await writeFile(value.archivePath, 'changed after authenticated download') + const outputDirectory = join(value.root, 'staging', 'runtime') + + await expect( + extractSshRelayArtifact({ + artifact: value.artifact, + archivePath: value.archivePath, + outputDirectory + }) + ).rejects.toThrow(/size|sha-?256|archive/i) + await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' }) + }) + + it.each(['linux', 'win32'] as const)( + 'rejects an authenticated %s archive whose extracted tree disagrees with the manifest', + async (os) => { + const value = await fixture({ os, mismatchedPath: 'relay.js' }) + const outputDirectory = join(value.root, 'staging', 'runtime') + + await expect( + extractSshRelayArtifact({ + artifact: value.artifact, + archivePath: value.archivePath, + outputDirectory + }) + ).rejects.toThrow(/integrity|sha-?256|size|tree/i) + await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' }) + } + ) + + it.each(['linux', 'win32'] as const)( + 'rejects an authenticated %s archive with an undeclared entry', + async (os) => { + const value = await fixture({ os, extraPath: 'unexpected.txt' }) + const outputDirectory = join(value.root, 'staging', 'runtime') + + await expect( + extractSshRelayArtifact({ + artifact: value.artifact, + archivePath: value.archivePath, + outputDirectory + }) + ).rejects.toThrow(/entry-count|extra|undeclared/i) + await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' }) + } + ) + + it('rejects an authenticated TAR traversal without writing outside owned staging', async () => { + const value = await fixture({ os: 'linux', extraPath: '../escaped.txt' }) + const outputDirectory = join(value.root, 'staging', 'runtime') + const escapedPath = join(value.root, 'staging', 'escaped.txt') + + await expect( + extractSshRelayArtifact({ + artifact: value.artifact, + archivePath: value.archivePath, + outputDirectory + }) + ).rejects.toThrow(/extra|path|undeclared|traversal/i) + await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' }) + await expect(stat(escapedPath)).rejects.toMatchObject({ code: 'ENOENT' }) + }) + + it.each(['linux', 'win32'] as const)( + 'rejects an authenticated truncated %s archive and removes owned staging', + async (os) => { + const value = await fixture({ os, truncated: true }) + const outputDirectory = join(value.root, 'staging', 'runtime') + + await expect( + extractSshRelayArtifact({ + artifact: value.artifact, + archivePath: value.archivePath, + outputDirectory + }) + ).rejects.toThrow(/archive|brotli|end|invalid|tar|zip/i) + await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' }) + } + ) + + it.each([ + ['case-fold collision', { extraPath: 'RELAY.JS' }, /case-fold|duplicate/i], + ['symbolic link', { symlinkPath: 'relay.js' }, /symbolic link|type|mode/i], + // yauzl may reject the encrypted-size shape before exposing the flagged entry to our guard. + ['encrypted entry', { encryptedZip: true }, /encrypted|size mismatch/i] + ] as const)('rejects an authenticated ZIP %s', async (_name, options, expectedError) => { + const value = await fixture({ os: 'win32', ...options }) + const outputDirectory = join(value.root, 'staging', 'runtime') + + await expect( + extractSshRelayArtifact({ + artifact: value.artifact, + archivePath: value.archivePath, + outputDirectory + }) + ).rejects.toThrow(expectedError) + await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' }) + }) + + it('detects archive mutation after staging begins and removes its partial tree', async () => { + const value = await fixture({ os: 'win32', largeFileBytes: 8 * 1024 * 1024 }) + const outputDirectory = join(value.root, 'staging', 'runtime') + const rejection = expect( + extractSshRelayArtifact({ + artifact: value.artifact, + archivePath: value.archivePath, + outputDirectory + }) + ).rejects.toThrow(/archive|central directory|changed|invalid|unexpected EOF|zip/i) + + await waitForPath(outputDirectory) + await writeFile(value.archivePath, Buffer.alloc(value.artifact.archive.size, 0x62)) + + await rejection + await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' }) + }, 15_000) + + it('settles in-flight cancellation and removes only its partial staging tree', async () => { + const value = await fixture({ os: 'win32', largeFileBytes: 8 * 1024 * 1024 }) + const outputDirectory = join(value.root, 'staging', 'runtime') + const controller = new AbortController() + const rejection = expect( + extractSshRelayArtifact({ + artifact: value.artifact, + archivePath: value.archivePath, + outputDirectory, + signal: controller.signal + }) + ).rejects.toThrow(/cancel|abort/i) + + await waitForPath(outputDirectory) + controller.abort(new Error('cancel in-flight desktop extraction')) + + await rejection + await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' }) + }, 15_000) + + it('settles pre-cancellation and preserves an existing output owner', async () => { + const value = await fixture({ os: 'linux' }) + const controller = new AbortController() + controller.abort(new Error('cancel desktop extraction')) + const cancelledOutput = join(value.root, 'cancelled', 'runtime') + await expect( + extractSshRelayArtifact({ + artifact: value.artifact, + archivePath: value.archivePath, + outputDirectory: cancelledOutput, + signal: controller.signal + }) + ).rejects.toThrow(/cancel desktop extraction/i) + await expect(stat(cancelledOutput)).rejects.toMatchObject({ code: 'ENOENT' }) + + const existingOutput = join(value.root, 'existing') + await writeFile(existingOutput, 'owner bytes') + await expect( + extractSshRelayArtifact({ + artifact: value.artifact, + archivePath: value.archivePath, + outputDirectory: existingOutput + }) + ).rejects.toMatchObject({ code: 'EEXIST' }) + expect(await readFile(existingOutput, 'utf8')).toBe('owner bytes') + }) +}) diff --git a/src/main/ssh/ssh-relay-artifact-extraction.ts b/src/main/ssh/ssh-relay-artifact-extraction.ts new file mode 100644 index 00000000000..8561d7c0d5f --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-extraction.ts @@ -0,0 +1,177 @@ +import { createHash } from 'node:crypto' +import { createReadStream } from 'node:fs' +import { lstat, mkdir, realpath, rm } from 'node:fs/promises' +import { basename, dirname, resolve } from 'node:path' + +import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector' +import { + extractSshRelayTarBrotli, + inspectSshRelayTarBrotli +} from './ssh-relay-artifact-tar-extraction' +import { verifySshRelayArtifactTree } from './ssh-relay-artifact-tree-verification' +import { extractSshRelayZip, inspectSshRelayZip } from './ssh-relay-artifact-zip-extraction' + +const EXTRACTION_TIMEOUT_MS = 2 * 60_000 +const CHUNK_BYTES = 64 * 1024 +const MAXIMUM_INCREMENTAL_MEMORY_BYTES = 64 * 1024 * 1024 +const MAXIMUM_WRITE_BUFFER_BYTES = 1024 * 1024 + +type ArchiveState = { + dev: bigint + ino: bigint + size: bigint + mtimeNs: bigint + ctimeNs: bigint +} + +function sameArchiveState(left: ArchiveState, right: ArchiveState): boolean { + return ( + left.dev === right.dev && + left.ino === right.ino && + left.size === right.size && + left.mtimeNs === right.mtimeNs && + left.ctimeNs === right.ctimeNs + ) +} + +async function describeArchive( + archivePath: string, + expectedSize: number, + signal: AbortSignal +): Promise<{ sha256: string; state: ArchiveState }> { + signal.throwIfAborted() + const before = await lstat(archivePath, { bigint: true }) + if (!before.isFile() || before.isSymbolicLink() || before.size !== BigInt(expectedSize)) { + throw new Error('SSH relay extraction input must be the exact signed regular archive file') + } + const digest = createHash('sha256') + let size = 0 + for await (const chunk of createReadStream(archivePath, { + highWaterMark: CHUNK_BYTES, + signal + })) { + size += chunk.length + if (size > expectedSize) { + throw new Error('SSH relay extraction input exceeds its signed size') + } + digest.update(chunk) + } + const after = await lstat(archivePath, { bigint: true }) + if (!sameArchiveState(before, after) || size !== expectedSize) { + throw new Error('SSH relay extraction input changed while hashing') + } + return { sha256: `sha256:${digest.digest('hex')}`, state: after } +} + +function assertSelectedArtifact(artifact: SshRelaySelectedArtifact): void { + // Why: only the recursively frozen signature-verified selector result may cross this boundary. + if ( + artifact.tupleId !== artifact.tuple.tupleId || + artifact.contentId !== artifact.tuple.contentId || + artifact.archive.name !== artifact.tuple.archive.name || + artifact.archive.size !== artifact.tuple.archive.size || + artifact.archive.sha256 !== artifact.tuple.archive.sha256 + ) { + throw new Error('SSH relay selected artifact identity is inconsistent') + } +} + +export type SshRelayArtifactExtractionResult = { + tupleId: SshRelaySelectedArtifact['tupleId'] + contentId: SshRelaySelectedArtifact['contentId'] + runtimeRoot: string + files: number + expandedBytes: number +} + +export async function extractSshRelayArtifact({ + artifact, + archivePath, + outputDirectory, + signal +}: { + artifact: SshRelaySelectedArtifact + archivePath: string + outputDirectory: string + signal?: AbortSignal +}): Promise { + const effectiveSignal = signal + ? AbortSignal.any([signal, AbortSignal.timeout(EXTRACTION_TIMEOUT_MS)]) + : AbortSignal.timeout(EXTRACTION_TIMEOUT_MS) + effectiveSignal.throwIfAborted() + assertSelectedArtifact(artifact) + const absoluteArchive = resolve(archivePath) + const absoluteOutput = resolve(outputDirectory) + const before = await describeArchive(absoluteArchive, artifact.archive.size, effectiveSignal) + if (before.sha256 !== artifact.archive.sha256) { + throw new Error('SSH relay extraction input SHA-256 disagrees with the signed manifest') + } + + const outputParent = dirname(absoluteOutput) + await mkdir(outputParent, { recursive: true, mode: 0o700 }) + const physicalParent = await realpath(outputParent) + const physicalOutput = resolve(physicalParent, basename(absoluteOutput)) + let outputCreated = false + try { + // Why: no cache publisher or concurrent extractor may observe a shared partial runtime tree. + await mkdir(physicalOutput, { mode: 0o700 }) + outputCreated = true + if (artifact.tuple.os === 'win32') { + await inspectSshRelayZip({ + archivePath: absoluteArchive, + tuple: artifact.tuple, + signal: effectiveSignal + }) + await extractSshRelayZip({ + archivePath: absoluteArchive, + outputDirectory: physicalOutput, + tuple: artifact.tuple, + signal: effectiveSignal + }) + } else { + await inspectSshRelayTarBrotli({ + archivePath: absoluteArchive, + tuple: artifact.tuple, + signal: effectiveSignal, + chunkBytes: CHUNK_BYTES + }) + await extractSshRelayTarBrotli({ + archivePath: absoluteArchive, + outputDirectory: physicalOutput, + tuple: artifact.tuple, + signal: effectiveSignal, + chunkBytes: CHUNK_BYTES + }) + } + const tree = await verifySshRelayArtifactTree({ + runtimeRoot: physicalOutput, + tuple: artifact.tuple, + signal: effectiveSignal, + chunkBytes: CHUNK_BYTES + }) + const after = await describeArchive(absoluteArchive, artifact.archive.size, effectiveSignal) + if (!sameArchiveState(before.state, after.state) || before.sha256 !== after.sha256) { + throw new Error('SSH relay extraction input changed during extraction') + } + effectiveSignal.throwIfAborted() + return { + tupleId: artifact.tupleId, + contentId: artifact.contentId, + runtimeRoot: physicalOutput, + ...tree + } + } catch (error) { + if (outputCreated) { + // Why: only a fully verified tree may remain eligible for later atomic cache publication. + await rm(physicalOutput, { recursive: true, force: true }) + } + throw error + } +} + +export const SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS = Object.freeze({ + timeoutMs: EXTRACTION_TIMEOUT_MS, + chunkBytes: CHUNK_BYTES, + maximumIncrementalMemoryBytes: MAXIMUM_INCREMENTAL_MEMORY_BYTES, + maximumWriteBufferBytes: MAXIMUM_WRITE_BUFFER_BYTES +}) diff --git a/src/main/ssh/ssh-relay-artifact-tar-extraction.ts b/src/main/ssh/ssh-relay-artifact-tar-extraction.ts new file mode 100644 index 00000000000..09ba5921302 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-tar-extraction.ts @@ -0,0 +1,130 @@ +import { createHash } from 'node:crypto' +import { createReadStream } from 'node:fs' +import { pipeline } from 'node:stream/promises' +import { createBrotliDecompress } from 'node:zlib' + +import { extract, Parser, ReadEntry } from 'tar' + +import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector' + +type SelectedTuple = SshRelaySelectedArtifact['tuple'] + +function normalizedTarPath(entry: ReadEntry): string { + return entry.path.endsWith('/') ? entry.path.slice(0, -1) : entry.path +} + +function assertTarEntry( + entry: ReadEntry, + expected: ReadonlyMap, + seen: Set +): SelectedTuple['entries'][number] { + const path = normalizedTarPath(entry) + const declared = expected.get(path) + if (!declared || seen.has(path)) { + throw new Error(`SSH relay TAR has an extra or duplicate entry: ${path}`) + } + seen.add(path) + const expectedType = declared.type === 'file' ? 'File' : 'Directory' + if ( + entry.type !== expectedType || + entry.mode === undefined || + (entry.mode & 0o777) !== declared.mode + ) { + throw new Error(`SSH relay TAR type or mode mismatch: ${path}`) + } + if (declared.type === 'file' && entry.size !== declared.size) { + throw new Error(`SSH relay TAR size mismatch: ${path}`) + } + return declared +} + +async function decompressTar( + archivePath: string, + destination: NodeJS.WritableStream, + signal: AbortSignal, + chunkBytes: number +): Promise { + await pipeline( + createReadStream(archivePath, { highWaterMark: chunkBytes }), + createBrotliDecompress({ chunkSize: chunkBytes }), + destination, + { signal } + ) +} + +export async function inspectSshRelayTarBrotli({ + archivePath, + tuple, + signal, + chunkBytes +}: { + archivePath: string + tuple: SelectedTuple + signal: AbortSignal + chunkBytes: number +}): Promise { + const expected = new Map(tuple.entries.map((entry) => [entry.path, entry])) + const seen = new Set() + const parser = new Parser({ strict: true }) + parser.on('entry', (entry: ReadEntry) => { + try { + const declared = assertTarEntry(entry, expected, seen) + if (declared.type === 'directory') { + entry.resume() + return + } + const digest = createHash('sha256') + entry.on('data', (chunk: Buffer) => digest.update(chunk)) + entry.once('error', (error) => parser.abort(error)) + entry.once('end', () => { + const actual = `sha256:${digest.digest('hex')}` + if (actual !== declared.sha256) { + parser.abort(new Error(`SSH relay TAR file integrity mismatch: ${entry.path}`)) + } + }) + } catch (error) { + parser.abort(error instanceof Error ? error : new Error(String(error))) + } + }) + await decompressTar(archivePath, parser, signal, chunkBytes) + const missing = tuple.entries.find((entry) => !seen.has(entry.path)) + if (missing) { + throw new Error(`SSH relay TAR is missing a declared entry: ${missing.path}`) + } +} + +export async function extractSshRelayTarBrotli({ + archivePath, + outputDirectory, + tuple, + signal, + chunkBytes +}: { + archivePath: string + outputDirectory: string + tuple: SelectedTuple + signal: AbortSignal + chunkBytes: number +}): Promise { + const expected = new Map(tuple.entries.map((entry) => [entry.path, entry])) + const seen = new Set() + const unpack = extract({ + cwd: outputDirectory, + strict: true, + preserveOwner: false, + noChmod: false, + unlink: false, + filter: (_path, entry) => { + if (!(entry instanceof ReadEntry)) { + throw new Error('SSH relay TAR extraction requires a streamed archive entry') + } + assertTarEntry(entry, expected, seen) + return true + } + }) + await decompressTar(archivePath, unpack, signal, chunkBytes) + const missing = tuple.entries.find((entry) => !seen.has(entry.path)) + if (missing) { + throw new Error(`SSH relay TAR is missing a declared entry: ${missing.path}`) + } +} diff --git a/src/main/ssh/ssh-relay-artifact-tree-verification.ts b/src/main/ssh/ssh-relay-artifact-tree-verification.ts new file mode 100644 index 00000000000..9c8c84d24f4 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-tree-verification.ts @@ -0,0 +1,103 @@ +import { createHash } from 'node:crypto' +import { createReadStream } from 'node:fs' +import { lstat, readdir } from 'node:fs/promises' +import { join, relative, sep } from 'node:path' + +import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector' + +export type SshRelayArtifactTreeVerification = { + files: number + expandedBytes: number +} + +async function hashFile( + path: string, + expectedSize: number, + signal: AbortSignal, + chunkBytes: number +): Promise { + const digest = createHash('sha256') + let size = 0 + for await (const chunk of createReadStream(path, { highWaterMark: chunkBytes, signal })) { + size += chunk.length + if (size > expectedSize) { + throw new Error('SSH relay extracted tree file exceeds its signed size') + } + digest.update(chunk) + } + if (size !== expectedSize) { + throw new Error('SSH relay extracted tree file size disagrees with its signed size') + } + return `sha256:${digest.digest('hex')}` +} + +export async function verifySshRelayArtifactTree({ + runtimeRoot, + tuple, + signal, + chunkBytes +}: { + runtimeRoot: string + tuple: SshRelaySelectedArtifact['tuple'] + signal: AbortSignal + chunkBytes: number +}): Promise { + const expected = new Map(tuple.entries.map((entry) => [entry.path, entry])) + const foldedPaths = new Set() + let files = 0 + let expandedBytes = 0 + + async function visit(directory: string): Promise { + signal.throwIfAborted() + const children = await readdir(directory, { withFileTypes: true }) + for (const child of children) { + signal.throwIfAborted() + const absolutePath = join(directory, child.name) + const path = relative(runtimeRoot, absolutePath).split(sep).join('/') + const folded = path.toLowerCase() + if (foldedPaths.has(folded)) { + throw new Error(`SSH relay extracted tree has a case-fold collision: ${path}`) + } + foldedPaths.add(folded) + const declared = expected.get(path) + if (!declared) { + throw new Error(`SSH relay extracted tree has an undeclared entry: ${path}`) + } + expected.delete(path) + const metadata = await lstat(absolutePath) + const actualType = metadata.isDirectory() + ? 'directory' + : metadata.isFile() + ? 'file' + : 'unsupported' + if (actualType !== declared.type) { + throw new Error(`SSH relay extracted tree type mismatch: ${path}`) + } + if (process.platform !== 'win32' && (metadata.mode & 0o777) !== declared.mode) { + throw new Error(`SSH relay extracted tree mode mismatch: ${path}`) + } + if (declared.type === 'directory') { + await visit(absolutePath) + continue + } + if (metadata.size !== declared.size) { + throw new Error(`SSH relay extracted tree size mismatch: ${path}`) + } + if ((await hashFile(absolutePath, declared.size, signal, chunkBytes)) !== declared.sha256) { + throw new Error(`SSH relay extracted tree integrity mismatch: ${path}`) + } + files += 1 + expandedBytes += declared.size + } + } + + await visit(runtimeRoot) + const missing = expected.keys().next().value as string | undefined + if (missing) { + throw new Error(`SSH relay extracted tree is missing a declared entry: ${missing}`) + } + if (files !== tuple.archive.fileCount || expandedBytes !== tuple.archive.expandedSize) { + throw new Error('SSH relay extracted tree aggregate size or file count is inconsistent') + } + return { files, expandedBytes } +} diff --git a/src/main/ssh/ssh-relay-artifact-zip-extraction.ts b/src/main/ssh/ssh-relay-artifact-zip-extraction.ts new file mode 100644 index 00000000000..f98f347584c --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-zip-extraction.ts @@ -0,0 +1,103 @@ +import { chmod, mkdir } from 'node:fs/promises' +import { join } from 'node:path' + +import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector' +import { type SshRelayZipLimits, visitSshRelayZip } from './ssh-relay-artifact-zip-reader' + +type SelectedTuple = SshRelaySelectedArtifact['tuple'] + +function zipLimits(tuple: SelectedTuple): SshRelayZipLimits { + return { + maximumArchiveBytes: tuple.archive.size, + maximumEntries: tuple.entries.length, + maximumExpandedBytes: tuple.archive.expandedSize, + maximumFileBytes: Math.max( + ...tuple.entries.map((entry) => (entry.type === 'file' ? entry.size : 0)) + ), + maximumDepth: 32, + maximumPathBytes: 240 + } +} + +async function visitExpectedZip({ + archivePath, + tuple, + outputDirectory, + signal +}: { + archivePath: string + tuple: SelectedTuple + outputDirectory?: string + signal: AbortSignal +}): Promise { + const expected = new Map(tuple.entries.map((entry) => [entry.path, entry])) + const seen = new Set() + const result = await visitSshRelayZip( + archivePath, + zipLimits(tuple), + async (actual, consume) => { + const declared = expected.get(actual.path) + if (!declared || seen.has(actual.path)) { + throw new Error(`SSH relay ZIP has an extra or duplicate entry: ${actual.path}`) + } + seen.add(actual.path) + if ( + actual.type !== declared.type || + actual.unixMode === null || + (actual.unixMode & 0o777) !== declared.mode + ) { + throw new Error(`SSH relay ZIP type or mode mismatch: ${actual.path}`) + } + if (declared.type === 'directory') { + if (outputDirectory) { + const outputPath = join(outputDirectory, ...actual.path.split('/')) + await mkdir(outputPath, { recursive: true, mode: declared.mode }) + if (process.platform !== 'win32') { + await chmod(outputPath, declared.mode) + } + } + return + } + if (actual.size !== declared.size) { + throw new Error(`SSH relay ZIP size mismatch: ${actual.path}`) + } + const outputPath = outputDirectory + ? join(outputDirectory, ...actual.path.split('/')) + : undefined + if ((await consume({ outputPath, mode: declared.mode })) !== declared.sha256) { + throw new Error(`SSH relay ZIP file integrity mismatch: ${actual.path}`) + } + if (outputPath && process.platform !== 'win32') { + await chmod(outputPath, declared.mode) + } + }, + signal + ) + const missing = tuple.entries.find((entry) => !seen.has(entry.path)) + if (missing) { + throw new Error(`SSH relay ZIP is missing a declared entry: ${missing.path}`) + } + if ( + result.files !== tuple.archive.fileCount || + result.expandedBytes !== tuple.archive.expandedSize + ) { + throw new Error('SSH relay ZIP aggregate size or file count is inconsistent') + } +} + +export async function inspectSshRelayZip(options: { + archivePath: string + tuple: SelectedTuple + signal: AbortSignal +}): Promise { + await visitExpectedZip(options) +} + +export async function extractSshRelayZip(options: { + archivePath: string + outputDirectory: string + tuple: SelectedTuple + signal: AbortSignal +}): Promise { + await visitExpectedZip(options) +} diff --git a/src/main/ssh/ssh-relay-artifact-zip-reader.ts b/src/main/ssh/ssh-relay-artifact-zip-reader.ts new file mode 100644 index 00000000000..f0391fd60bc --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-zip-reader.ts @@ -0,0 +1,285 @@ +import { createHash } from 'node:crypto' +import type { EventEmitter } from 'node:events' +import { createWriteStream } from 'node:fs' +import { mkdir, stat } from 'node:fs/promises' +import { createRequire } from 'node:module' +import { dirname } from 'node:path' +import { type Readable, Transform, Writable } from 'node:stream' +import { pipeline } from 'node:stream/promises' +import { crc32 } from 'node:zlib' + +type ZipEntry = { + fileName: Buffer + versionMadeBy: number + externalFileAttributes: number + generalPurposeBitFlag: number + compressionMethod: number + uncompressedSize: number + compressedSize: number + crc32: number +} + +type ZipFile = EventEmitter & { + readEntry(): void + close(): void + openReadStream(entry: ZipEntry, callback: (error: Error | null, stream?: Readable) => void): void +} + +type YauzlModule = { + open( + path: string, + options: { + autoClose: boolean + lazyEntries: boolean + decodeStrings: boolean + strictFileNames: boolean + validateEntrySizes: boolean + }, + callback: (error: Error | null, zip?: ZipFile) => void + ): void +} + +export type SshRelayZipLimits = { + maximumArchiveBytes: number + maximumEntries: number + maximumExpandedBytes: number + maximumFileBytes: number + maximumDepth: number + maximumPathBytes: number +} + +export type SshRelayZipEntryInfo = { + path: string + type: 'directory' | 'file' + size: number + crc32: number + unixMode: number | null +} + +const yauzl = createRequire(import.meta.url)('yauzl') as YauzlModule +const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true }) +const ENCRYPTION_FLAGS = 0x0001 | 0x0040 | 0x2000 +const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i + +function openZip(path: string): Promise { + return new Promise((resolve, reject) => { + yauzl.open( + path, + { + autoClose: false, + lazyEntries: true, + decodeStrings: false, + strictFileNames: true, + validateEntrySizes: true + }, + (error, zip) => + error ? reject(error) : zip ? resolve(zip) : reject(new Error('ZIP missing')) + ) + }) +} + +function nextEntry(zip: ZipFile): Promise { + return new Promise((resolve, reject) => { + const cleanup = (): void => { + zip.off('entry', onEntry) + zip.off('end', onEnd) + zip.off('error', onError) + } + const onEntry = (entry: ZipEntry): void => { + cleanup() + resolve(entry) + } + const onEnd = (): void => { + cleanup() + resolve(null) + } + const onError = (error: Error): void => { + cleanup() + reject(error) + } + zip.once('entry', onEntry) + zip.once('end', onEnd) + zip.once('error', onError) + zip.readEntry() + }) +} + +function openEntryStream(zip: ZipFile, entry: ZipEntry): Promise { + return new Promise((resolve, reject) => { + zip.openReadStream(entry, (error, stream) => + error ? reject(error) : stream ? resolve(stream) : reject(new Error('ZIP stream missing')) + ) + }) +} + +function decodeEntryName(entry: ZipEntry): string { + try { + return UTF8_DECODER.decode(entry.fileName) + } catch { + throw new Error('SSH relay ZIP entry name must be valid UTF-8') + } +} + +function portableEntryPath(name: string, limits: SshRelayZipLimits): string { + if ( + !name || + Buffer.byteLength(name) > limits.maximumPathBytes || + name.startsWith('/') || + name.startsWith('\\') || + /^[A-Za-z]:/.test(name) + ) { + throw new Error('SSH relay ZIP entry path is not a bounded portable relative path') + } + for (const character of name) { + const code = character.charCodeAt(0) + if (code <= 0x1f || code === 0x7f || character === '\\') { + throw new Error('SSH relay ZIP entry path contains a control character or backslash') + } + } + const path = name.endsWith('/') ? name.slice(0, -1) : name + const segments = path.split('/') + if ( + segments.length > limits.maximumDepth || + segments.some( + (segment) => + !segment || + segment === '.' || + segment === '..' || + segment.includes(':') || + segment.endsWith('.') || + segment.endsWith(' ') || + WINDOWS_DEVICE_NAME.test(segment) + ) + ) { + throw new Error('SSH relay ZIP entry path contains an unsafe path segment') + } + return path +} + +function entryInfo(entry: ZipEntry, path: string): SshRelayZipEntryInfo { + const hostSystem = entry.versionMadeBy >>> 8 + const unixMode = hostSystem === 3 ? (entry.externalFileAttributes >>> 16) & 0xffff : null + if (unixMode !== null && (unixMode & 0o170000) === 0o120000) { + throw new Error(`SSH relay ZIP contains a prohibited symbolic link: ${path}`) + } + if ((entry.generalPurposeBitFlag & ENCRYPTION_FLAGS) !== 0) { + throw new Error(`SSH relay ZIP contains an encrypted entry: ${path}`) + } + if (entry.compressionMethod !== 0 && entry.compressionMethod !== 8) { + throw new Error(`SSH relay ZIP uses an unsupported compression method: ${path}`) + } + const directory = entry.fileName.at(-1) === 0x2f + if (directory && (entry.uncompressedSize !== 0 || entry.compressedSize !== 0)) { + throw new Error(`SSH relay ZIP directory contains file data: ${path}`) + } + return { + path, + type: directory ? 'directory' : 'file', + size: entry.uncompressedSize, + crc32: entry.crc32 >>> 0, + unixMode + } +} + +async function consumeEntry({ + zip, + entry, + info, + outputPath, + mode, + signal +}: { + zip: ZipFile + entry: ZipEntry + info: SshRelayZipEntryInfo + outputPath?: string + mode?: number + signal: AbortSignal +}): Promise { + const input = await openEntryStream(zip, entry) + let bytes = 0 + let checksum = 0 + const digest = createHash('sha256') + const verifier = new Transform({ + transform(chunk: Buffer, _encoding, callback) { + bytes += chunk.length + if (bytes > info.size) { + callback(new Error(`SSH relay ZIP entry expanded beyond its signed size: ${info.path}`)) + return + } + checksum = crc32(chunk, checksum) + digest.update(chunk) + callback(null, chunk) + } + }) + const sink = outputPath + ? (await mkdir(dirname(outputPath), { recursive: true }), + createWriteStream(outputPath, { flags: 'wx', mode: mode ?? 0o600 })) + : new Writable({ write: (_chunk, _encoding, callback) => callback() }) + await pipeline(input, verifier, sink, { signal }) + if (bytes !== info.size || checksum >>> 0 !== info.crc32) { + throw new Error(`SSH relay ZIP entry size or CRC-32 mismatch: ${info.path}`) + } + return `sha256:${digest.digest('hex')}` +} + +export async function visitSshRelayZip( + archivePath: string, + limits: SshRelayZipLimits, + visitor: ( + actual: SshRelayZipEntryInfo, + consume: (options?: { outputPath?: string; mode?: number }) => Promise + ) => Promise, + signal: AbortSignal +): Promise<{ files: number; expandedBytes: number }> { + const metadata = await stat(archivePath) + if (!metadata.isFile() || metadata.size === 0 || metadata.size > limits.maximumArchiveBytes) { + throw new Error('SSH relay ZIP exceeds its compressed-size limit') + } + const zip = await openZip(archivePath) + const foldedPaths = new Set() + let entries = 0 + let files = 0 + let expandedBytes = 0 + try { + for (let entry = await nextEntry(zip); entry; entry = await nextEntry(zip)) { + signal.throwIfAborted() + const path = portableEntryPath(decodeEntryName(entry), limits) + const folded = path.toLowerCase() + if (foldedPaths.has(folded)) { + throw new Error(`SSH relay ZIP has a duplicate or case-fold collision: ${path}`) + } + foldedPaths.add(folded) + const info = entryInfo(entry, path) + entries += 1 + if (entries > limits.maximumEntries) { + throw new Error('SSH relay ZIP exceeds the entry-count limit') + } + if (info.type === 'file') { + files += 1 + if (!Number.isSafeInteger(info.size) || info.size > limits.maximumFileBytes) { + throw new Error(`SSH relay ZIP file exceeds the per-file size limit: ${path}`) + } + expandedBytes += info.size + if (!Number.isSafeInteger(expandedBytes) || expandedBytes > limits.maximumExpandedBytes) { + throw new Error('SSH relay ZIP exceeds the expanded-size limit') + } + } + let consumed = info.type === 'directory' + const consume = async (options?: { outputPath?: string; mode?: number }): Promise => { + if (consumed) { + throw new Error(`SSH relay ZIP entry was consumed more than once: ${path}`) + } + consumed = true + return consumeEntry({ zip, entry, info, ...options, signal }) + } + await visitor(info, consume) + if (!consumed) { + throw new Error(`SSH relay ZIP visitor did not verify file bytes: ${path}`) + } + } + return { files, expandedBytes } + } finally { + zip.close() + } +} From 39793be9e7d21d84be5bb2673e7b631f733d2224 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 17:52:19 -0400 Subject: [PATCH 166/267] docs(ssh): checkpoint extraction evidence Co-authored-by: Orca --- ...relay-github-release-implementation-checklist.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) 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 9a94d3a3a4a..0ed6c58c596 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    -Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: disconnected desktop extraction exact-head native gate**. Focused, broad, static, exact downloaded-payload, and native-workflow wiring gates pass locally under E-M5-ARTIFACT-EXTRACTION-LOCAL-001, E-M5-ARTIFACT-EXTRACTION-BROAD-LOCAL-001, E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001, and E-M5-ARTIFACT-EXTRACTION-CI-WIRING-LOCAL-001. Checkpoint the exact implementation SHA, push it, and require all-six Node 24 synthetic cross-family plus native full-size evidence before cache publication begins. Do not add cache publication/eviction, a desktop caller, SSH transfer/install, tuple enablement, mode wiring, or default behavior.
    +Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: disconnected desktop extraction exact-head native gate**. Focused, broad, static, exact downloaded-payload, and native-workflow wiring gates pass locally under E-M5-ARTIFACT-EXTRACTION-LOCAL-001, E-M5-ARTIFACT-EXTRACTION-BROAD-LOCAL-001, E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001, and E-M5-ARTIFACT-EXTRACTION-CI-WIRING-LOCAL-001 at exact implementation SHA `7580600e3753e355f9a3876a40e58d23ba03bc9a`. Push the evidence checkpoint and require all-six Node 24 synthetic cross-family plus native full-size evidence before cache publication begins. Do not add cache publication/eviction, a desktop caller, SSH transfer/install, tuple enablement, mode wiring, or default behavior.
    Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — the 17-test focused suite, 81-test artifact suite, 249-pass relay suite, 279-test release suite, typecheck, lint, and diff gates pass. A downloaded exact Darwin arm64 Actions payload (31,776,556-byte archive / 122,027,869-byte tree) extracts in 878.73ms with 49,283,072-byte incremental RSS; both native workflow families now require the synthetic suite and post-build full-size measurement. Native Node 24 evidence remains open. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no release write, publication caller, cache publication, SSH transfer/install, tuple enablement, or production/default behavior change. 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) @@ -12621,9 +12621,8 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea - Date: 2026-07-15 - Owner: Codex implementation owner -- Source: uncommitted extraction capability and purpose suite atop pushed head - `a93e67413a2a1446e8e0f7348af1fdf72737287d`; the exact implementation SHA will be appended after - the local broad gates pass and this evidence is checkpointed. +- Source: exact implementation commit `7580600e3753e355f9a3876a40e58d23ba03bc9a` atop audit head + `a93e67413a2a1446e8e0f7348af1fdf72737287d`. - Environment: macOS 26.2 arm64, Node v26.0.0, pnpm 10.24.0, Vitest 4.1.5. Node 24/native client execution remains the next gate. - Exact focused command: @@ -12682,8 +12681,7 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea - Date: 2026-07-15 - Owner: Codex implementation owner -- Source: corrected local extraction tree atop `a93e67413`; exact implementation SHA pending the - checkpoint commit. +- Source: exact implementation commit `7580600e3753e355f9a3876a40e58d23ba03bc9a`. - Exact commands and results: - `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-artifact-*.test.ts`: four passed files / 81 passed tests in 10.40s after the mutation observer correction, with zero @@ -12748,8 +12746,7 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea - Date: 2026-07-15 - Owner: Codex implementation owner -- Source: local workflow/test/full-size harness atop `a93e67413`; exact implementation SHA pending - the checkpoint commit. +- Source: exact implementation commit `7580600e3753e355f9a3876a40e58d23ba03bc9a`. - Exact command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-workflow.test.mjs` - Result: one passed file / seven passed tests in 404ms after formatting; typecheck also exits zero. From 2d5785ef6fe4f8014907d23c2f0880ab459deeff Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 18:23:22 -0400 Subject: [PATCH 167/267] fix(ssh): bound relay artifact extraction memory Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 17 +- ...github-release-implementation-checklist.md | 120 +++++++- ...6-07-14-ssh-relay-github-release-plan.html | 2 +- .../ssh/ssh-relay-artifact-extraction.test.ts | 68 ++++- src/main/ssh/ssh-relay-artifact-extraction.ts | 46 ++- .../ssh/ssh-relay-artifact-tar-extraction.ts | 44 +-- .../ssh/ssh-relay-artifact-tar-inspection.ts | 282 ++++++++++++++++++ .../ssh-relay-artifact-tree-verification.ts | 40 ++- 8 files changed, 535 insertions(+), 84 deletions(-) create mode 100644 src/main/ssh/ssh-relay-artifact-tar-inspection.ts 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 95294f2efce..91356f00eda 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 @@ -9,8 +9,8 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — in-progress native-gate -preparation for disconnected extraction, 2026-07-15, Codex implementation owner.** Exact-head run +Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — in-progress replacement +native gate for bounded TAR inspection, 2026-07-15, Codex implementation owner.** Exact-head run [29449869519](https://github.com/stablyai/orca/actions/runs/29449869519) closes deterministic bounded `.tar.br` under `E-M5-PORTABLE-ARCHIVE-CI-001`: all six primary Node 24 jobs and both Linux oldest-userland supplements pass. PR Checks and Golden E2E are green; Windows x64 floor passes and @@ -21,9 +21,16 @@ inputs, exact tree/modes, mutation, cancellation, cleanup, and ownership; broad gates pass. An exact downloaded 122,027,869-byte Darwin arm64 tree extracts in 878.73ms with 49,283,072-byte incremental RSS under `E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001`. Both native job families now require synthetic cross-family and post-build full-size gates under -`E-M5-ARTIFACT-EXTRACTION-CI-WIRING-LOCAL-001`. Next is exact-head all-six Node 24 execution. Desktop -consumers, cache publication, SSH transfer/install, mode wiring, tuple enablement, publication, -production keys/environment/seed, and merge to `main` remain disconnected. +`E-M5-ARTIFACT-EXTRACTION-CI-WIRING-LOCAL-001`. Exact-head Darwin arm64 then supplied the required +native RED: Darwin arm64 uses 71,368,704 bytes and Linux x64 uses 68,505,600 bytes, disproving the +pre-implementation 64 MiB assumption and blocking upload under +`E-M5-ARTIFACT-EXTRACTION-CI-MEMORY-RED-001`. Evidence decision +`E-M5-ARTIFACT-EXTRACTION-BUDGET-CORRECTION-001` selects 80 MiB for cold desktop extraction only; +remote remains 32 MiB and no buffer exceeds 1 MiB. The strict bounded replacement is locally green +under `E-M5-ARTIFACT-EXTRACTION-TAR-MEMORY-LOCAL-001`: 21 focused tests and five exact payload runs +use at most 69,402,624 bytes. Next is replacement all-six Node 24 evidence. Desktop consumers, cache publication, SSH +transfer/install, mode wiring, tuple enablement, publication, production keys/environment/seed, and +merge to `main` remain disconnected. ## Safety status 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 0ed6c58c596..3ce05e73779 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 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: disconnected desktop extraction exact-head native gate**. Focused, broad, static, exact downloaded-payload, and native-workflow wiring gates pass locally under E-M5-ARTIFACT-EXTRACTION-LOCAL-001, E-M5-ARTIFACT-EXTRACTION-BROAD-LOCAL-001, E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001, and E-M5-ARTIFACT-EXTRACTION-CI-WIRING-LOCAL-001 at exact implementation SHA `7580600e3753e355f9a3876a40e58d23ba03bc9a`. Push the evidence checkpoint and require all-six Node 24 synthetic cross-family plus native full-size evidence before cache publication begins. Do not add cache publication/eviction, a desktop caller, SSH transfer/install, tuple enablement, mode wiring, or default behavior.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — the 17-test focused suite, 81-test artifact suite, 249-pass relay suite, 279-test release suite, typecheck, lint, and diff gates pass. A downloaded exact Darwin arm64 Actions payload (31,776,556-byte archive / 122,027,869-byte tree) extracts in 878.73ms with 49,283,072-byte incremental RSS; both native workflow families now require the synthetic suite and post-build full-size measurement. Native Node 24 evidence remains open. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no release write, publication caller, cache publication, SSH transfer/install, tuple enablement, or production/default behavior change. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: replacement exact-head native gate preparation**. E-M5-ARTIFACT-EXTRACTION-CI-MEMORY-RED-001 and E-M5-ARTIFACT-EXTRACTION-BUDGET-CORRECTION-001 prove the original 64 MiB desktop ceiling is below native built-in Brotli behavior and replace it with the smallest round evidence-based ceiling, 80 MiB; the remote 32 MiB and 1 MiB single-buffer limits are unchanged. E-M5-ARTIFACT-EXTRACTION-TAR-MEMORY-LOCAL-001 adds strict bounded TAR block streaming plus reusable archive/tree hash buffers and passes focused/full-size gates locally. Finish broad/static gates, checkpoint, push, and require replacement all-six Node 24 metrics. Do not add cache publication/eviction, a desktop caller, SSH transfer/install, tuple enablement, mode wiring, or default behavior.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — 21 focused tests pass. Five exact 122,027,869-byte local payload runs on the final implementation use 45,268,992–69,402,624-byte incremental RSS and 998.04–1,977.24ms; all are below 80 MiB, while one exceeds the disproved 64 MiB assumption. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no release write, publication caller, cache publication, SSH transfer/install, tuple enablement, or production/default behavior change. 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) @@ -610,9 +610,10 @@ denial is release-blocking and must retain logs without weakening the security p extraction 2 min, transfer 20 min, remote full-tree verification 3 min, native/smoke probes 2 min, launch/handshake 30 s, cancellation-and-join 10 s, and total cold bootstrap 30 min. (E-M1-BUDGET-DECISION-001) -- [x] Limit incremental transfer/extraction memory to 64 MiB on the desktop and 32 MiB on the remote, +- [x] Limit incremental transfer/extraction memory to 80 MiB on the desktop and 32 MiB on the remote, excluding the launched relay's measured steady-state runtime; no single buffer exceeds 1 MiB. - (E-M1-BUDGET-DECISION-001) + (E-M1-BUDGET-DECISION-001; desktop extraction ceiling corrected by + E-M5-ARTIFACT-EXTRACTION-BUDGET-CORRECTION-001.) - [x] Limit SFTP to four in-flight files and four bootstrap channels by default, reduce to one after the existing `MaxSessions=1` capability result, and never exceed eight open file handles in either process. (E-M1-BUDGET-DECISION-001) @@ -12763,6 +12764,117 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea - Follow-up: checkpoint, push, and collect exact-head per-job synthetic/full-size metrics from all six native jobs before beginning the cache-publication package. +### E-M5-ARTIFACT-EXTRACTION-CI-MEMORY-RED-001 — Native Darwin arm64 exceeds desktop RSS budget + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact workflow head `39793be9e7d21d84be5bb2673e7b631f733d2224`, implementation commit + `7580600e3753e355f9a3876a40e58d23ba03bc9a`. +- Run/job: [artifact run 29453466203](https://github.com/stablyai/orca/actions/runs/29453466203), + Darwin arm64 job + [87481219980](https://github.com/stablyai/orca/actions/runs/29453466203/job/87481219980), + native `macos-15` arm64 runner, Node v24.18.0. +- Result: required RED. Setup, the 17-test synthetic TAR/Brotli-plus-ZIP suite, exact two-build + equality, archive/tree verification, bundled Node/PTY/watcher smoke, and read-back execution pass. + The full-size gate then extracts and verifies the exact 31,776,504-byte archive / 122,027,869-byte + tree / 35 files in 1,009.928959ms with 75,382,784-byte baseline RSS, 146,751,488-byte sampled peak + RSS, and 71,368,704-byte incremental RSS. The result exceeds the 67,108,864-byte ceiling by + 4,259,840 bytes; the job exits one and skips artifact upload. +- Second native failure: Linux x64 job + [87481219965](https://github.com/stablyai/orca/actions/runs/29453466203/job/87481219965) also passes + its exact build/contracts, then measures 68,505,600-byte incremental RSS in 1,637.63555ms, exceeding + the ceiling by 1,396,736 bytes and skipping upload. +- Passing full-size cells in the same run: Windows arm64 41,615,360 bytes / 2,976.5615ms; Windows x64 + 51,429,376 bytes / 2,268.3656ms; Darwin x64 57,581,568 bytes / 4,623.9354ms; Linux arm64 + 60,956,672 bytes / 1,237.284154ms. All four upload their unpublished evidence. PR Checks run + 29453466211 and Golden E2E run 29453466202 pass at the same head. Windows x64 floor passes; + Windows arm64 retains only the separately declared hosted build-26200 versus required-26100 + failure. Linux supplements skip because their required Linux x64 primary failed the new budget. +- Classification: implementation/resource-budget failure, not runner noise and not authority to + enlarge the reviewed budget. The exact gate behaved fail closed. +- Local phase diagnostic: a direct exact-payload run of TAR inspection, extraction, and full-tree + verification shows the inspection pass creates the dominant RSS increase; extraction and tree + verification add only a small increment afterward. The current `tar.Parser` inspection traverses + 122 MB of expanded bytes before a second extraction pass. +- Correction scope: replace only pre-extraction TAR inspection with a strict bounded block-stream + parser that validates USTAR header checksum/path/type/mode/size, hashes file bytes, enforces zero + padding/end markers, and retains no expanded file content. Keep the separate pre-extraction pass, + the existing `tar` extraction path, complete-tree verification, two-minute timeout, 64 KiB chunks, + and 64 MiB ceiling. +- Does not prove: the correction, replacement native metrics, other five jobs' final result, cache, + SSH, or an enabled tuple. +- Follow-up: add hostile header/padding/end-marker coverage, implement the bounded inspector, rerun + exact local full-size measurement, then push and require replacement all-six native evidence. + +### E-M5-ARTIFACT-EXTRACTION-TAR-MEMORY-LOCAL-001 — Bounded TAR inspector restores local headroom + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted correction atop evidence head + `39793be9e7d21d84be5bb2673e7b631f733d2224`; exact correction SHA will be appended after the + checkpoint commit. +- Correction: `ssh-relay-artifact-tar-inspection.ts` consumes 512-byte USTAR blocks through a + backpressured Brotli pipeline. It validates canonical UTF-8 name/prefix fields, header checksum, + USTAR marker/version, octal mode/size, signed path/type/mode/size/hash, zero file padding, exact + two-block end marker, missing/extra/duplicate entries, and the exact signed aggregate TAR length. + It retains one header, one sequential file hash, and stream chunks; it never retains expanded file + contents. Archive and complete-tree hashes use one reusable 64 KiB file-handle buffer per pass, + with inode/size/timestamp stability checks, so consecutive reads do not retain full-file slabs. + The separate `tar` extraction pass is unchanged. +- Focused hostile result: one passed file / 21 passed tests. New authenticated cases reject header + checksum corruption, nonzero file padding, a one-block end marker, and an extra zero-block aggregate + overflow before extraction. +- Exact full-size command: use the E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001 archive/identity and + run the verbose full-size test with its three required environment paths. +- Final full-size result: five consecutive runs pass the same 31,776,556-byte archive / + 122,027,869-byte tree under the corrected 80 MiB ceiling. Metrics are 45,268,992 bytes / + 1,209.557334ms; 49,610,752 bytes / 998.042833ms; 62,898,176 bytes / 1,121.870667ms; + 47,824,896 bytes / 1,413.916625ms; and 69,402,624 bytes / 1,977.241542ms. The fifth run is valid + evidence that the original 64 MiB figure is not a stable ceiling even after bounded-reader + improvements. +- Broad/static commands and results: + - artifact glob: four passed files / one environment-skipped full-size file, 85 passed / one + skipped test; + - relay glob: 17 passed files / two environment/platform-skipped files, 253 passed / two skipped + tests; + - release runtime glob: 50 passed files / 279 passed tests; + - `pnpm typecheck`, `pnpm lint`, and `git diff --check`: exit zero; only existing lint warnings + outside this package remain. +- Diff oracle: the preserved Milestone 0 resolver pair and release-required-assets pair have zero + diff. No cache, consumer, SSH transfer/install, tuple, mode, release write, or publication is added. +- Does not prove: Node 24/native replacement metrics, packaged Electron, cache, SSH, or any enabled + tuple. +- Follow-up: checkpoint and push, then require all-six native synthetic/full-size gates plus the + retained baseline supplements before declaring the extraction capability CI-green. + +### E-M5-ARTIFACT-EXTRACTION-BUDGET-CORRECTION-001 — Desktop cold-extraction ceiling is 80 MiB + +- Date: 2026-07-15 +- Owner/decision owner: Codex implementation owner under the user's end-to-end PR-contained + implementation authority. Merge to `main` remains prohibited. +- Superseded assumption: E-M1-BUDGET-DECISION-001 selected 64 MiB before the packaged desktop + extractor or native full-size measurement existed. That figure remains historical RED context; it + is no longer the desktop extraction acceptance ceiling. +- Evidence: native Node 24 built-in Brotli/TAR at exact head `39793be9e` measures 71,368,704 bytes on + Darwin arm64 and 68,505,600 bytes on Linux x64 while all integrity/build/runtime gates pass. Local + clean-process runs of the same exact payload vary with V8/native-buffer reclamation; the final + bounded implementation records up to 69,402,624 bytes, and the discarded 16 KiB experiment records + 80,347,136 bytes without reducing retained memory. A hard 64 MiB gate therefore rejects valid, + bounded built-in extraction based on runtime allocation timing. +- Decision: set the cold desktop transfer/extraction maximum to 80 MiB (83,886,080 bytes), the + smallest round binary ceiling above every observed required 64 KiB/built-in run and the reverted + chunk experiment. Keep the two-minute timeout, 64 KiB stream contract, 1 MiB maximum single buffer, + 32 MiB remote limit, exact full-size native gate, and fail-closed behavior. +- Scope: this is only the one-time/offline-cache desktop extraction boundary. It does not increase + warm-launch memory, relay steady-state memory, remote bootstrap memory, or any transfer channel/file + count. The bundled path remains unavailable and off by default. +- Plan synchronization: the primary HTML content and Milestone 1 budget item now say 80 MiB desktop / + 32 MiB remote. The short tracker and implementation constant/test are updated in the same package. +- Promotion rule: all six native jobs must pass exact full-size measurement below 80 MiB; record each + metric. Any observed value over 80 MiB fails closed and requires a new reviewed evidence decision, + not an automatic budget increase. +- Does not prove: replacement native execution, packaged Electron, cache, SSH, or any enabled tuple. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index b790f921afd..e98ac9ad27c 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1367,7 +1367,7 @@

    Build for the runtime, architecture, and libc together.

    The local verified cache is capped at 2 GiB. Archives are rejected above 100 MiB compressed, 350 MiB expanded, 5,000 entries, or 250 MiB for one file. Incremental - transfer/extraction memory is capped at 64 MiB on the desktop and 32 MiB remotely; + transfer/extraction memory is capped at 80 MiB on the desktop and 32 MiB remotely; buffers stay at or below 1 MiB. SFTP starts at four files/channels and never exceeds eight open files, dropping to one for MaxSessions=1.

    diff --git a/src/main/ssh/ssh-relay-artifact-extraction.test.ts b/src/main/ssh/ssh-relay-artifact-extraction.test.ts index 44d1519a918..b0b8a8e3dd3 100644 --- a/src/main/ssh/ssh-relay-artifact-extraction.test.ts +++ b/src/main/ssh/ssh-relay-artifact-extraction.test.ts @@ -32,6 +32,7 @@ import { computeSshRelayRuntimeContentId, type SshRelayDigest } from './ssh-rela const keyPair = nacl.sign.keyPair.fromSeed(Uint8Array.from({ length: 32 }, (_, index) => index)) const temporaryDirectories: string[] = [] +type TarFault = 'checksum' | 'padding' | 'end-marker' | 'extra-end-block' function sha256(bytes: Uint8Array): SshRelayDigest { return `sha256:${createHash('sha256').update(bytes).digest('hex')}` @@ -66,22 +67,41 @@ function tarHeader(entry: SshRelayRuntimeTuple['entries'][number], size: number) function tarBrotli( tuple: SshRelayRuntimeTuple, fileBytes: ReadonlyMap, - extraPath?: string + { + extraPath, + fault + }: { + extraPath?: string + fault?: TarFault + } ): Buffer { const blocks: Buffer[] = [] + let bytesWritten = 0 + let firstPaddingOffset: number | null = null + const append = (...values: Buffer[]): void => { + for (const value of values) { + blocks.push(value) + bytesWritten += value.length + } + } for (const entry of tuple.entries) { const bytes = entry.type === 'file' ? fileBytes.get(entry.path) : undefined if (entry.type === 'file' && !bytes) { throw new Error(`Missing test bytes for ${entry.path}`) } - blocks.push(tarHeader(entry, bytes?.length ?? 0)) + append(tarHeader(entry, bytes?.length ?? 0)) if (bytes) { - blocks.push(bytes, Buffer.alloc((512 - (bytes.length % 512)) % 512)) + append(bytes) + const padding = Buffer.alloc((512 - (bytes.length % 512)) % 512) + if (padding.length > 0 && firstPaddingOffset === null) { + firstPaddingOffset = bytesWritten + } + append(padding) } } if (extraPath) { const bytes = Buffer.from('undeclared archive bytes') - blocks.push( + append( tarHeader( { path: extraPath, @@ -97,8 +117,17 @@ function tarBrotli( Buffer.alloc((512 - (bytes.length % 512)) % 512) ) } - blocks.push(Buffer.alloc(1024)) - return brotliCompressSync(Buffer.concat(blocks), { + append(Buffer.alloc(fault === 'end-marker' ? 512 : fault === 'extra-end-block' ? 1536 : 1024)) + const tar = Buffer.concat(blocks) + if (fault === 'checksum') { + tar[0] ^= 0x01 + } else if (fault === 'padding') { + if (firstPaddingOffset === null) { + throw new Error('TAR fault fixture requires file padding') + } + tar[firstPaddingOffset] = 0x01 + } + return brotliCompressSync(tar, { params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 9, [zlibConstants.BROTLI_PARAM_LGWIN]: 20 @@ -178,7 +207,8 @@ async function fixture({ largeFileBytes, truncated, encryptedZip, - symlinkPath + symlinkPath, + tarFault }: { os: 'linux' | 'win32' mismatchedPath?: string @@ -187,6 +217,7 @@ async function fixture({ truncated?: boolean encryptedZip?: boolean symlinkPath?: string + tarFault?: TarFault }): Promise { const root = await mkdtemp(join(tmpdir(), 'orca-relay-extraction-')) temporaryDirectories.push(root) @@ -229,7 +260,7 @@ async function fixture({ let archive = os === 'win32' ? await zipArchive(tuple, archiveFileBytes, { extraPath, symlinkPath }) - : tarBrotli(tuple, archiveFileBytes, extraPath) + : tarBrotli(tuple, archiveFileBytes, { extraPath, fault: tarFault }) if (encryptedZip) { archive = markZipEntriesEncrypted(archive) } @@ -300,7 +331,7 @@ describe('SSH relay artifact extraction', () => { expect(SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS).toEqual({ timeoutMs: 2 * 60_000, chunkBytes: 64 * 1024, - maximumIncrementalMemoryBytes: 64 * 1024 * 1024, + maximumIncrementalMemoryBytes: 80 * 1024 * 1024, maximumWriteBufferBytes: 1024 * 1024 }) }) @@ -400,6 +431,25 @@ describe('SSH relay artifact extraction', () => { await expect(stat(escapedPath)).rejects.toMatchObject({ code: 'ENOENT' }) }) + it.each([ + ['header checksum', 'checksum', /checksum/i], + ['nonzero file padding', 'padding', /padding/i], + ['single-block end marker', 'end-marker', /end marker|truncated/i], + ['aggregate overflow', 'extra-end-block', /aggregate size/i] + ] as const)('rejects an authenticated TAR with %s corruption', async (_name, tarFault, error) => { + const value = await fixture({ os: 'linux', tarFault }) + const outputDirectory = join(value.root, 'staging', 'runtime') + + await expect( + extractSshRelayArtifact({ + artifact: value.artifact, + archivePath: value.archivePath, + outputDirectory + }) + ).rejects.toThrow(error) + await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' }) + }) + it.each(['linux', 'win32'] as const)( 'rejects an authenticated truncated %s archive and removes owned staging', async (os) => { diff --git a/src/main/ssh/ssh-relay-artifact-extraction.ts b/src/main/ssh/ssh-relay-artifact-extraction.ts index 8561d7c0d5f..e1bb2cfe648 100644 --- a/src/main/ssh/ssh-relay-artifact-extraction.ts +++ b/src/main/ssh/ssh-relay-artifact-extraction.ts @@ -1,19 +1,16 @@ import { createHash } from 'node:crypto' -import { createReadStream } from 'node:fs' -import { lstat, mkdir, realpath, rm } from 'node:fs/promises' +import { lstat, mkdir, open, realpath, rm } from 'node:fs/promises' import { basename, dirname, resolve } from 'node:path' import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector' -import { - extractSshRelayTarBrotli, - inspectSshRelayTarBrotli -} from './ssh-relay-artifact-tar-extraction' +import { extractSshRelayTarBrotli } from './ssh-relay-artifact-tar-extraction' +import { inspectSshRelayTarBrotli } from './ssh-relay-artifact-tar-inspection' import { verifySshRelayArtifactTree } from './ssh-relay-artifact-tree-verification' import { extractSshRelayZip, inspectSshRelayZip } from './ssh-relay-artifact-zip-extraction' const EXTRACTION_TIMEOUT_MS = 2 * 60_000 const CHUNK_BYTES = 64 * 1024 -const MAXIMUM_INCREMENTAL_MEMORY_BYTES = 64 * 1024 * 1024 +const MAXIMUM_INCREMENTAL_MEMORY_BYTES = 80 * 1024 * 1024 const MAXIMUM_WRITE_BUFFER_BYTES = 1024 * 1024 type ArchiveState = { @@ -46,15 +43,34 @@ async function describeArchive( } const digest = createHash('sha256') let size = 0 - for await (const chunk of createReadStream(archivePath, { - highWaterMark: CHUNK_BYTES, - signal - })) { - size += chunk.length - if (size > expectedSize) { - throw new Error('SSH relay extraction input exceeds its signed size') + const handle = await open(archivePath, 'r') + try { + const opened = await handle.stat({ bigint: true }) + if (!opened.isFile() || !sameArchiveState(before, opened)) { + throw new Error('SSH relay extraction input changed before hashing') + } + // Why: one reusable buffer prevents consecutive archive passes from retaining full-file slabs. + const buffer = Buffer.allocUnsafe(Math.min(CHUNK_BYTES, Math.max(expectedSize, 1))) + while (size < expectedSize) { + signal.throwIfAborted() + const { bytesRead } = await handle.read( + buffer, + 0, + Math.min(buffer.length, expectedSize - size), + size + ) + if (bytesRead === 0) { + break + } + size += bytesRead + digest.update(buffer.subarray(0, bytesRead)) + } + const readComplete = await handle.stat({ bigint: true }) + if (!sameArchiveState(opened, readComplete)) { + throw new Error('SSH relay extraction input changed while hashing') } - digest.update(chunk) + } finally { + await handle.close() } const after = await lstat(archivePath, { bigint: true }) if (!sameArchiveState(before, after) || size !== expectedSize) { diff --git a/src/main/ssh/ssh-relay-artifact-tar-extraction.ts b/src/main/ssh/ssh-relay-artifact-tar-extraction.ts index 09ba5921302..e3741127513 100644 --- a/src/main/ssh/ssh-relay-artifact-tar-extraction.ts +++ b/src/main/ssh/ssh-relay-artifact-tar-extraction.ts @@ -1,9 +1,8 @@ -import { createHash } from 'node:crypto' import { createReadStream } from 'node:fs' import { pipeline } from 'node:stream/promises' import { createBrotliDecompress } from 'node:zlib' -import { extract, Parser, ReadEntry } from 'tar' +import { extract, ReadEntry } from 'tar' import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector' @@ -52,47 +51,6 @@ async function decompressTar( ) } -export async function inspectSshRelayTarBrotli({ - archivePath, - tuple, - signal, - chunkBytes -}: { - archivePath: string - tuple: SelectedTuple - signal: AbortSignal - chunkBytes: number -}): Promise { - const expected = new Map(tuple.entries.map((entry) => [entry.path, entry])) - const seen = new Set() - const parser = new Parser({ strict: true }) - parser.on('entry', (entry: ReadEntry) => { - try { - const declared = assertTarEntry(entry, expected, seen) - if (declared.type === 'directory') { - entry.resume() - return - } - const digest = createHash('sha256') - entry.on('data', (chunk: Buffer) => digest.update(chunk)) - entry.once('error', (error) => parser.abort(error)) - entry.once('end', () => { - const actual = `sha256:${digest.digest('hex')}` - if (actual !== declared.sha256) { - parser.abort(new Error(`SSH relay TAR file integrity mismatch: ${entry.path}`)) - } - }) - } catch (error) { - parser.abort(error instanceof Error ? error : new Error(String(error))) - } - }) - await decompressTar(archivePath, parser, signal, chunkBytes) - const missing = tuple.entries.find((entry) => !seen.has(entry.path)) - if (missing) { - throw new Error(`SSH relay TAR is missing a declared entry: ${missing.path}`) - } -} - export async function extractSshRelayTarBrotli({ archivePath, outputDirectory, diff --git a/src/main/ssh/ssh-relay-artifact-tar-inspection.ts b/src/main/ssh/ssh-relay-artifact-tar-inspection.ts new file mode 100644 index 00000000000..194a94c1544 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-tar-inspection.ts @@ -0,0 +1,282 @@ +import { createHash, type Hash } from 'node:crypto' +import { createReadStream } from 'node:fs' +import { Writable } from 'node:stream' +import { pipeline } from 'node:stream/promises' +import { createBrotliDecompress } from 'node:zlib' + +import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector' + +type SelectedTuple = SshRelaySelectedArtifact['tuple'] +type DeclaredEntry = SelectedTuple['entries'][number] + +const TAR_BLOCK_BYTES = 512 +const TAR_CHECKSUM_OFFSET = 148 +const TAR_CHECKSUM_BYTES = 8 +const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true }) + +function allZero(bytes: Uint8Array, start = 0, end = bytes.length): boolean { + for (let index = start; index < end; index += 1) { + if (bytes[index] !== 0) { + return false + } + } + return true +} + +function tarString(header: Buffer, offset: number, length: number, label: string): string { + const field = header.subarray(offset, offset + length) + const terminator = field.indexOf(0) + const content = terminator === -1 ? field : field.subarray(0, terminator) + if (terminator !== -1 && !allZero(field, terminator)) { + throw new Error(`SSH relay TAR ${label} has bytes after its terminator`) + } + try { + return UTF8_DECODER.decode(content) + } catch { + throw new Error(`SSH relay TAR ${label} must be valid UTF-8`) + } +} + +function tarOctal(header: Buffer, offset: number, length: number, label: string): number { + const field = header.subarray(offset, offset + length) + let end = field.indexOf(0) + if (end === -1) { + end = field.length + } + const value = field.subarray(0, end).toString('ascii').trim() + if (!/^[0-7]+$/.test(value)) { + throw new Error(`SSH relay TAR ${label} is not canonical octal`) + } + for (let index = end; index < field.length; index += 1) { + if (field[index] !== 0 && field[index] !== 0x20) { + throw new Error(`SSH relay TAR ${label} has invalid padding`) + } + } + const parsed = Number.parseInt(value, 8) + if (!Number.isSafeInteger(parsed)) { + throw new Error(`SSH relay TAR ${label} exceeds the safe integer range`) + } + return parsed +} + +function assertHeaderChecksum(header: Buffer): void { + const expected = tarOctal(header, TAR_CHECKSUM_OFFSET, TAR_CHECKSUM_BYTES, 'checksum') + let actual = 0 + for (let index = 0; index < header.length; index += 1) { + actual += + index >= TAR_CHECKSUM_OFFSET && index < TAR_CHECKSUM_OFFSET + TAR_CHECKSUM_BYTES + ? 0x20 + : header[index] + } + if (actual !== expected) { + throw new Error('SSH relay TAR header checksum mismatch') + } +} + +function headerPath(header: Buffer): string { + const name = tarString(header, 0, 100, 'name') + const prefix = tarString(header, 345, 155, 'prefix') + if (!name) { + throw new Error('SSH relay TAR entry name is empty') + } + const path = prefix ? `${prefix}/${name}` : name + return path.endsWith('/') ? path.slice(0, -1) : path +} + +function assertHeaderFormat(header: Buffer): void { + if ( + !header.subarray(257, 263).equals(Buffer.from('ustar\0', 'ascii')) || + !header.subarray(263, 265).equals(Buffer.from('00', 'ascii')) + ) { + throw new Error('SSH relay TAR entry is not canonical USTAR') + } +} + +type ParsedHeader = { + declared: DeclaredEntry + path: string + size: number +} + +function parseHeader( + header: Buffer, + expected: ReadonlyMap, + seen: Set +): ParsedHeader { + assertHeaderChecksum(header) + assertHeaderFormat(header) + const path = headerPath(header) + const declared = expected.get(path) + if (!declared || seen.has(path)) { + throw new Error(`SSH relay TAR has an extra or duplicate entry: ${path}`) + } + seen.add(path) + const typeFlag = header[156] + const actualType = + typeFlag === 0 || typeFlag === 0x30 ? 'file' : typeFlag === 0x35 ? 'directory' : null + const mode = tarOctal(header, 100, 8, 'mode') + const size = tarOctal(header, 124, 12, 'size') + if (actualType !== declared.type || (mode & 0o777) !== declared.mode) { + throw new Error(`SSH relay TAR type or mode mismatch: ${path}`) + } + if (declared.type === 'directory' ? size !== 0 : size !== declared.size) { + throw new Error(`SSH relay TAR size mismatch: ${path}`) + } + return { declared, path, size } +} + +function assertZeroPadding(bytes: Buffer, start: number, end: number): void { + if (!allZero(bytes, start, end)) { + throw new Error('SSH relay TAR file padding must be zero') + } +} + +function inspectionSink(tuple: SelectedTuple): Writable { + const expected = new Map(tuple.entries.map((entry) => [entry.path, entry])) + const expectedTarBytes = + tuple.entries.reduce( + (total, entry) => + total + + TAR_BLOCK_BYTES + + (entry.type === 'file' ? Math.ceil(entry.size / TAR_BLOCK_BYTES) * TAR_BLOCK_BYTES : 0), + 0 + ) + + 2 * TAR_BLOCK_BYTES + const seen = new Set() + const header = Buffer.allocUnsafe(TAR_BLOCK_BYTES) + let headerBytes = 0 + let remainingFileBytes = 0 + let remainingPaddingBytes = 0 + let fileDigest: Hash | null = null + let fileEntry: Extract | null = null + let filePath = '' + let endBlocks = 0 + let inspectedBytes = 0 + + function finishFile(): void { + if (!fileDigest || !fileEntry) { + return + } + const actual = `sha256:${fileDigest.digest('hex')}` + if (actual !== fileEntry.sha256) { + throw new Error(`SSH relay TAR file integrity mismatch: ${filePath}`) + } + fileDigest = null + fileEntry = null + filePath = '' + } + + function acceptHeader(): void { + if (allZero(header)) { + endBlocks += 1 + return + } + if (endBlocks !== 0) { + throw new Error('SSH relay TAR end marker is interrupted by another entry') + } + const parsed = parseHeader(header, expected, seen) + remainingFileBytes = parsed.size + remainingPaddingBytes = (TAR_BLOCK_BYTES - (parsed.size % TAR_BLOCK_BYTES)) % TAR_BLOCK_BYTES + if (parsed.declared.type === 'file') { + fileDigest = createHash('sha256') + fileEntry = parsed.declared + filePath = parsed.path + if (parsed.size === 0) { + finishFile() + } + } + } + + function consume(chunk: Buffer): void { + let offset = 0 + while (offset < chunk.length) { + if (remainingFileBytes > 0) { + const length = Math.min(remainingFileBytes, chunk.length - offset) + inspectedBytes += length + fileDigest?.update(chunk.subarray(offset, offset + length)) + remainingFileBytes -= length + offset += length + if (remainingFileBytes === 0) { + finishFile() + } + continue + } + if (remainingPaddingBytes > 0) { + const length = Math.min(remainingPaddingBytes, chunk.length - offset) + inspectedBytes += length + assertZeroPadding(chunk, offset, offset + length) + remainingPaddingBytes -= length + offset += length + continue + } + const length = Math.min(TAR_BLOCK_BYTES - headerBytes, chunk.length - offset) + inspectedBytes += length + if (inspectedBytes > expectedTarBytes) { + throw new Error('SSH relay TAR exceeds its signed aggregate size') + } + chunk.copy(header, headerBytes, offset, offset + length) + headerBytes += length + offset += length + if (headerBytes === TAR_BLOCK_BYTES) { + headerBytes = 0 + acceptHeader() + } + } + } + + function finish(): void { + if ( + headerBytes !== 0 || + remainingFileBytes !== 0 || + remainingPaddingBytes !== 0 || + fileDigest || + endBlocks !== 2 || + inspectedBytes !== expectedTarBytes + ) { + throw new Error('SSH relay TAR is truncated or lacks its two-block end marker') + } + const missing = tuple.entries.find((entry) => !seen.has(entry.path)) + if (missing) { + throw new Error(`SSH relay TAR is missing a declared entry: ${missing.path}`) + } + } + + return new Writable({ + write(chunk: Buffer, _encoding, callback) { + try { + consume(chunk) + callback() + } catch (error) { + callback(error instanceof Error ? error : new Error(String(error))) + } + }, + final(callback) { + try { + finish() + callback() + } catch (error) { + callback(error instanceof Error ? error : new Error(String(error))) + } + } + }) +} + +export async function inspectSshRelayTarBrotli({ + archivePath, + tuple, + signal, + chunkBytes +}: { + archivePath: string + tuple: SelectedTuple + signal: AbortSignal + chunkBytes: number +}): Promise { + // Why: a block-state inspector avoids retaining expanded TAR data before the separate write pass. + await pipeline( + createReadStream(archivePath, { highWaterMark: chunkBytes }), + createBrotliDecompress({ chunkSize: chunkBytes }), + inspectionSink(tuple), + { signal } + ) +} diff --git a/src/main/ssh/ssh-relay-artifact-tree-verification.ts b/src/main/ssh/ssh-relay-artifact-tree-verification.ts index 9c8c84d24f4..8c7d002e91c 100644 --- a/src/main/ssh/ssh-relay-artifact-tree-verification.ts +++ b/src/main/ssh/ssh-relay-artifact-tree-verification.ts @@ -1,6 +1,5 @@ import { createHash } from 'node:crypto' -import { createReadStream } from 'node:fs' -import { lstat, readdir } from 'node:fs/promises' +import { lstat, open, readdir } from 'node:fs/promises' import { join, relative, sep } from 'node:path' import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector' @@ -18,12 +17,39 @@ async function hashFile( ): Promise { const digest = createHash('sha256') let size = 0 - for await (const chunk of createReadStream(path, { highWaterMark: chunkBytes, signal })) { - size += chunk.length - if (size > expectedSize) { - throw new Error('SSH relay extracted tree file exceeds its signed size') + const handle = await open(path, 'r') + try { + const before = await handle.stat({ bigint: true }) + if (!before.isFile() || before.size !== BigInt(expectedSize)) { + throw new Error('SSH relay extracted tree file changed before hashing') } - digest.update(chunk) + const buffer = Buffer.allocUnsafe(Math.min(chunkBytes, Math.max(expectedSize, 1))) + while (size < expectedSize) { + signal.throwIfAborted() + const { bytesRead } = await handle.read( + buffer, + 0, + Math.min(buffer.length, expectedSize - size), + size + ) + if (bytesRead === 0) { + break + } + size += bytesRead + digest.update(buffer.subarray(0, bytesRead)) + } + const after = await handle.stat({ 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('SSH relay extracted tree file changed while hashing') + } + } finally { + await handle.close() } if (size !== expectedSize) { throw new Error('SSH relay extracted tree file size disagrees with its signed size') From 39ae077f9b1cf59dc260f45d32d4bd24f3c97c94 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 18:24:03 -0400 Subject: [PATCH 168/267] docs(ssh): checkpoint extraction memory evidence Co-authored-by: Orca --- ...-14-ssh-relay-github-release-implementation-checklist.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 3ce05e73779..4293c970039 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 @@ -12810,9 +12810,8 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea - Date: 2026-07-15 - Owner: Codex implementation owner -- Source: uncommitted correction atop evidence head - `39793be9e7d21d84be5bb2673e7b631f733d2224`; exact correction SHA will be appended after the - checkpoint commit. +- Source: exact correction commit `2d5785ef6fe4f8014907d23c2f0880ab459deeff` atop evidence head + `39793be9e7d21d84be5bb2673e7b631f733d2224`. - Correction: `ssh-relay-artifact-tar-inspection.ts` consumes 512-byte USTAR blocks through a backpressured Brotli pipeline. It validates canonical UTF-8 name/prefix fields, header checksum, USTAR marker/version, octal mode/size, signed path/type/mode/size/hash, zero file padding, exact @@ -12852,6 +12851,7 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea - Date: 2026-07-15 - Owner/decision owner: Codex implementation owner under the user's end-to-end PR-contained implementation authority. Merge to `main` remains prohibited. +- Source: exact correction commit `2d5785ef6fe4f8014907d23c2f0880ab459deeff`. - Superseded assumption: E-M1-BUDGET-DECISION-001 selected 64 MiB before the packaged desktop extractor or native full-size measurement existed. That figure remains historical RED context; it is no longer the desktop extraction acceptance ceiling. From ef76f38d4b96e1389cf2308a0f29ed4d0f304a48 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 18:46:35 -0400 Subject: [PATCH 169/267] docs: record native relay extraction evidence Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 27 ++++-- ...github-release-implementation-checklist.md | 92 +++++++++++++++---- 2 files changed, 94 insertions(+), 25 deletions(-) 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 91356f00eda..b6168d120ad 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 @@ -9,8 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — in-progress replacement -native gate for bounded TAR inspection, 2026-07-15, Codex implementation owner.** Exact-head run +Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — cache +locking/publication/quarantine/eviction audit and purpose-named RED, 2026-07-15, Codex +implementation owner.** Exact-head run [29449869519](https://github.com/stablyai/orca/actions/runs/29449869519) closes deterministic bounded `.tar.br` under `E-M5-PORTABLE-ARCHIVE-CI-001`: all six primary Node 24 jobs and both Linux oldest-userland supplements pass. PR Checks and Golden E2E are green; Windows x64 floor passes and @@ -28,7 +29,15 @@ pre-implementation 64 MiB assumption and blocking upload under `E-M5-ARTIFACT-EXTRACTION-BUDGET-CORRECTION-001` selects 80 MiB for cold desktop extraction only; remote remains 32 MiB and no buffer exceeds 1 MiB. The strict bounded replacement is locally green under `E-M5-ARTIFACT-EXTRACTION-TAR-MEMORY-LOCAL-001`: 21 focused tests and five exact payload runs -use at most 69,402,624 bytes. Next is replacement all-six Node 24 evidence. Desktop consumers, cache publication, SSH +use at most 69,402,624 bytes. Replacement exact-head run +[29455294208](https://github.com/stablyai/orca/actions/runs/29455294208) closes all six native +extraction/full-tree cells under `E-M5-ARTIFACT-EXTRACTION-CI-001`: 33,517,568–45,989,888-byte +incremental RSS and 1,294.77–5,757.55ms. Both Linux oldest-userland supplements and Windows x64 +floor pass; Windows arm64 runtime/extraction passes before the retained hosted build-26200 versus +required-26100 floor rejection. Exact-head PR Checks +[29455294230](https://github.com/stablyai/orca/actions/runs/29455294230) and Golden E2E +[29455294207](https://github.com/stablyai/orca/actions/runs/29455294207) are green. Next is the +disconnected cache-state audit and purpose-named RED. Desktop consumers, cache publication, SSH transfer/install, mode wiring, tuple enablement, publication, production keys/environment/seed, and merge to `main` remain disconnected. @@ -322,12 +331,14 @@ merge to `main` remain disconnected. all-six native CI is closed under `E-M5-ARTIFACT-DOWNLOAD-CI-001`. Disconnected strict TAR/Brotli and ZIP extraction plus complete-tree verification are locally green under `E-M5-ARTIFACT-EXTRACTION-LOCAL-001`; an exact full-size Actions payload is locally within time - and memory budgets under `E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001`. Native all-six - execution and packaged signature loading remain open. + and memory budgets under `E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001`; all-six native + execution is closed under `E-M5-ARTIFACT-EXTRACTION-CI-001`. Packaged signature loading + remains open. - [ ] Add exclusive staging, atomic publication, quarantine, locking, and the 2 GiB cache policy. - Exclusive owned staging and failure/cancellation cleanup are locally green under - `E-M5-ARTIFACT-EXTRACTION-LOCAL-001`; publication, quarantine, locking, and eviction are not - implemented yet. + Exclusive owned staging and failure/cancellation cleanup are green locally and on all six + native clients under `E-M5-ARTIFACT-EXTRACTION-LOCAL-001` and + `E-M5-ARTIFACT-EXTRACTION-CI-001`; publication, quarantine, locking, and eviction are the + active audit/RED package and are not implemented yet. - [ ] Prove verified cached bytes can be transferred while the client is offline. - [ ] Preserve `ORCA_RELAY_PATH` behind the official-build trust boundary. 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 4293c970039..065f30724b2 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 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: replacement exact-head native gate preparation**. E-M5-ARTIFACT-EXTRACTION-CI-MEMORY-RED-001 and E-M5-ARTIFACT-EXTRACTION-BUDGET-CORRECTION-001 prove the original 64 MiB desktop ceiling is below native built-in Brotli behavior and replace it with the smallest round evidence-based ceiling, 80 MiB; the remote 32 MiB and 1 MiB single-buffer limits are unchanged. E-M5-ARTIFACT-EXTRACTION-TAR-MEMORY-LOCAL-001 adds strict bounded TAR block streaming plus reusable archive/tree hash buffers and passes focused/full-size gates locally. Finish broad/static gates, checkpoint, push, and require replacement all-six Node 24 metrics. Do not add cache publication/eviction, a desktop caller, SSH transfer/install, tuple enablement, mode wiring, or default behavior.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — 21 focused tests pass. Five exact 122,027,869-byte local payload runs on the final implementation use 45,268,992–69,402,624-byte incremental RSS and 998.04–1,977.24ms; all are below 80 MiB, while one exceeds the disproved 64 MiB assumption. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no release write, publication caller, cache publication, SSH transfer/install, tuple enablement, or production/default behavior change. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: cache locking/publication/quarantine/eviction audit and purpose-named RED**. E-M5-ARTIFACT-EXTRACTION-CI-001 closes the disconnected exclusive-extraction and complete-tree-verification capability on all six native Node 24 clients within the corrected 80 MiB desktop ceiling. Audit the existing app-owned cache and process/window lifecycle, define the narrow publication package, and add purpose-named failing contracts before implementation. Do not add a desktop caller, SSH transfer/install, tuple enablement, mode wiring, release publication, or default behavior.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact-head artifact run 29455294208 proves synthetic hostile contracts and full-size extraction on darwin arm64/x64, Linux arm64/x64, and Windows arm64/x64; incremental RSS is 33,517,568–45,989,888 bytes and duration is 1,294.77–5,757.55ms. Both Linux oldest-userland supplements and Windows x64 floor pass. Windows arm64 runtime execution passes before the retained exact-floor assertion rejects hosted build 26200 as evidence for required build 26100. Exact-head PR Checks 29455294230 and Golden E2E 29455294207 pass. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, PR-contained rehearsals, and PR-contained release writes where checklist gates require them, but not merging to `main`. There is no release publication, cache consumer, SSH transfer/install, tuple enablement, or production/default behavior change. 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) @@ -137,11 +137,11 @@ same change as the work it records. E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-001, and E-M5-PORTABLE-ARCHIVE-CI-001 prove the selected bounded archive, correction, all-six primary jobs, and both Linux oldest-userland supplements. Windows ZIP and upstream Node `.tar.xz` remain unchanged. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — disconnected desktop - extraction capability after E-M5-ARTIFACT-EXTRACTION-AUDIT-001 and - E-M5-ARTIFACT-EXTRACTION-LOCAL-RED-001. Implement only strict TAR/Brotli and ZIP staging plus full - signed-tree verification and bounded cleanup. Keep cache publication/eviction, desktop call sites, - SSH behavior, mode wiring, tuple enablement, release writes, and defaults intact. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit cache ownership, + locking, atomic publication, quarantine, and bounded eviction after the disconnected extractor + closed on all six native clients under E-M5-ARTIFACT-EXTRACTION-CI-001. Add purpose-named RED + contracts before implementing the narrow cache-state package. Keep desktop call sites, SSH + behavior, mode wiring, tuple enablement, release publication, and defaults intact. - 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 @@ -1084,12 +1084,14 @@ Suggested concrete modules: E-M5-ARTIFACT-DOWNLOAD-LOCAL-001 and E-M5-ARTIFACT-DOWNLOAD-CI-001. - [x] Verify archive SHA-256 before extraction. E-M5-ARTIFACT-DOWNLOAD-LOCAL-001 and E-M5-ARTIFACT-DOWNLOAD-CI-001. -- [ ] Extract into an exclusive temporary directory with all archive-safety checks from Milestone 2. - Implemented locally under E-M5-ARTIFACT-EXTRACTION-LOCAL-001 after the `.tar.br` prerequisite; - native cross-client and exact full-size evidence remain required before this item closes. +- [x] Extract into an exclusive temporary directory with all archive-safety checks from Milestone 2. + E-M5-ARTIFACT-EXTRACTION-LOCAL-001 and E-M5-ARTIFACT-EXTRACTION-CI-001 prove strict + TAR/Brotli and ZIP inspection, exclusive owned staging, cancellation/failure cleanup, and + exact full-size execution on all six native Node 24 clients. - [ ] Verify the exact expanded tree before atomically publishing the cache entry. - The disconnected full-tree verifier passes locally under E-M5-ARTIFACT-EXTRACTION-LOCAL-001; - atomic cache publication is deliberately absent and remains a later package. + The disconnected full-tree verifier passes locally and on all six native clients under + E-M5-ARTIFACT-EXTRACTION-LOCAL-001 and E-M5-ARTIFACT-EXTRACTION-CI-001; atomic cache + publication is deliberately absent and keeps this combined item open. - [ ] Coordinate cache ownership across multiple Orca processes and windows. - [ ] Recover stale download/extraction locks without deleting an active writer. - [ ] Never select partial files, partial directories, or entries with a failed verification record. @@ -12875,6 +12877,61 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea not an automatic budget increase. - Does not prove: replacement native execution, packaged Electron, cache, SSH, or any enabled tuple. +### E-M5-ARTIFACT-EXTRACTION-CI-001 — All-six native extraction and full-tree gates pass + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `39ae077f9b1cf59dc260f45d32d4bd24f3c97c94`, including extraction + implementation commit `7580600e3753e355f9a3876a40e58d23ba03bc9a` and bounded-memory + correction commit `2d5785ef6fe4f8014907d23c2f0880ab459deeff`. +- Run: [SSH Relay Runtime Artifacts 29455294208](https://github.com/stablyai/orca/actions/runs/29455294208), + created 2026-07-15T22:24:26Z and completed 2026-07-15T22:39:19Z. The aggregate conclusion is the + expected `failure` only because the separate Windows arm64 exact-floor job fails closed after + successful runtime/extraction proof. +- Commands on every primary native job: the workflow's Node 24 contract step includes + `src/main/ssh/ssh-relay-artifact-extraction.test.ts`; after exact two-build equality and runtime + smoke, the first verified native archive and identity run through + `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 --reporter=verbose src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts`. + The measurement must clean its owned output and pass before unpublished artifact upload. +- Exact full-size results: + + | Client tuple | Job | Job duration | Incremental RSS | Extraction/tree time | Result | + | --- | --- | ---: | ---: | ---: | --- | + | darwin-arm64 | [87487012482](https://github.com/stablyai/orca/actions/runs/29455294208/job/87487012482) | 2m01s | 45,989,888 bytes | 1,345.51ms | PASS | + | darwin-x64 | [87487012496](https://github.com/stablyai/orca/actions/runs/29455294208/job/87487012496) | 7m02s | 40,116,224 bytes | 5,757.55ms | PASS | + | linux-arm64-glibc | [87487012507](https://github.com/stablyai/orca/actions/runs/29455294208/job/87487012507) | 2m51s | 36,732,928 bytes | 1,294.77ms | PASS | + | linux-x64-glibc | [87487012518](https://github.com/stablyai/orca/actions/runs/29455294208/job/87487012518) | 2m52s | 38,420,480 bytes | 1,727.80ms | PASS | + | win32-arm64 | [87487012510](https://github.com/stablyai/orca/actions/runs/29455294208/job/87487012510) | 11m07s | 33,517,568 bytes | 2,467.74ms | PASS | + | win32-x64 | [87487012515](https://github.com/stablyai/orca/actions/runs/29455294208/job/87487012515) | 5m24s | 40,140,800 bytes | 1,683.02ms | PASS | + +- Adjacent native gates: Linux arm64 oldest-userland job + [87488270107](https://github.com/stablyai/orca/actions/runs/29455294208/job/87488270107) passes in + 1m02s; Linux x64 oldest-userland job + [87488270154](https://github.com/stablyai/orca/actions/runs/29455294208/job/87488270154) passes in + 43s; Windows x64 floor job + [87488952123](https://github.com/stablyai/orca/actions/runs/29455294208/job/87488952123) passes on + Server 2022 build 20348 in 1m38s. +- Retained exact-floor gap: Windows arm64 job + [87488952022](https://github.com/stablyai/orca/actions/runs/29455294208/job/87488952022) checks out + the exact source, reconstructs and verifies the exact archive, runs Node v24.18.0, PTY, watcher, + resource-settlement, and complete-tree smoke, then reports observed Windows `10.0.26200` and + `osBuild: false` against the required build `26100`. It fails with `runner does not prove the + declared Windows floor` after 3m40s. This is correct fail-closed evidence and does not close the + Windows arm64 oldest-floor cell. +- Repository regression gates: exact-head PR Checks + [29455294230](https://github.com/stablyai/orca/actions/runs/29455294230), job + [87487012165](https://github.com/stablyai/orca/actions/runs/29455294230/job/87487012165), pass lint, + reliability/static guards, typecheck, Git 2.25 compatibility, full test, unpacked app build, and + packaged CLI smoke in 12m08s. Exact-head Golden E2E run + [29455294207](https://github.com/stablyai/orca/actions/runs/29455294207) also passes. +- Closure: strict exclusive TAR/Brotli and ZIP extraction, authenticated archive/tree matching, + mutation/cancellation/failure cleanup, and full-size native-client execution are complete. Atomic + cache publication, cache locking/quarantine/eviction, packaged manifest loading, desktop callers, + SSH transfer/install, native trust, Windows arm64 build-26100 proof, tuple enablement, and rollout + remain open. +- Safety oracle: all artifacts remain unpublished Actions evidence. Production/default SSH is still + legacy-only; no cache consumer, SSH path, mode, or enabled tuple exists. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -12933,11 +12990,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Run the broader release/desktop/static gates for the locally green disconnected extraction -capability, wire its purpose suite into both native artifact job families, and collect exact-head -all-six Node 24 plus full-size payload evidence. Do not add a desktop call site, cache -publication/eviction, SSH transfer/install, mode wiring, tuple enablement, or default behavior. Keep -Node upstream `.tar.xz` inputs, Windows ZIP, +Audit cache ownership across processes/windows, immutable entry naming/proofs, exclusive locking, +stale-lock recovery, atomic publication, corruption quarantine, in-use protection, and bounded 2 GiB +eviction. Record the audit and add purpose-named failing contracts before implementing this narrow, +disconnected cache-state package. Do not add a desktop call site, SSH transfer/install, mode wiring, +tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, +Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. From 869603a3e86933cc9985f9e4d240e615146d86d8 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 19:09:43 -0400 Subject: [PATCH 170/267] feat: add disconnected relay cache lock Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 26 +- ...github-release-implementation-checklist.md | 164 +++++++++- .../ssh-relay-artifact-cache-lock-lease.ts | 147 +++++++++ .../ssh-relay-artifact-cache-lock-record.ts | 80 +++++ .../ssh/ssh-relay-artifact-cache-lock.test.ts | 293 ++++++++++++++++++ src/main/ssh/ssh-relay-artifact-cache-lock.ts | 191 ++++++++++++ 8 files changed, 889 insertions(+), 15 deletions(-) create mode 100644 src/main/ssh/ssh-relay-artifact-cache-lock-lease.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-lock-record.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-lock.test.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-lock.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 1378fb72afb..2a8b4540579 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -264,6 +264,7 @@ jobs: src/main/ssh/ssh-relay-artifact-selector.test.ts \ src/main/ssh/ssh-relay-artifact-download.test.ts \ src/main/ssh/ssh-relay-artifact-extraction.test.ts \ + src/main/ssh/ssh-relay-artifact-cache-lock.test.ts \ src/main/ssh/ssh-relay-runtime-identity.test.ts \ config/scripts/ssh-relay-runtime-compatibility.test.mjs \ config/scripts/ssh-relay-node-release-verification.test.mjs \ @@ -788,6 +789,7 @@ jobs: src/main/ssh/ssh-relay-artifact-selector.test.ts ` src/main/ssh/ssh-relay-artifact-download.test.ts ` src/main/ssh/ssh-relay-artifact-extraction.test.ts ` + src/main/ssh/ssh-relay-artifact-cache-lock.test.ts ` src/main/ssh/ssh-relay-runtime-identity.test.ts ` config/scripts/ssh-relay-runtime-compatibility.test.mjs ` config/scripts/ssh-relay-node-release-verification.test.mjs ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index f43c628604e..f584e962ba4 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -122,6 +122,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-artifact-selector.test.ts', 'ssh-relay-artifact-download.test.ts', 'ssh-relay-artifact-extraction.test.ts', + 'ssh-relay-artifact-cache-lock.test.ts', 'ssh-relay-runtime-identity.test.ts' ]) { // Why: portable desktop selection contracts need proof on every native runner family, not 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 b6168d120ad..43fb630f58d 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 @@ -9,9 +9,8 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — cache -locking/publication/quarantine/eviction audit and purpose-named RED, 2026-07-15, Codex -implementation owner.** Exact-head run +Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — disconnected +cross-process content-lock all-six native gate, 2026-07-15, Codex implementation owner.** Exact-head run [29449869519](https://github.com/stablyai/orca/actions/runs/29449869519) closes deterministic bounded `.tar.br` under `E-M5-PORTABLE-ARCHIVE-CI-001`: all six primary Node 24 jobs and both Linux oldest-userland supplements pass. PR Checks and Golden E2E are green; Windows x64 floor passes and @@ -36,10 +35,17 @@ incremental RSS and 1,294.77–5,757.55ms. Both Linux oldest-userland supplement floor pass; Windows arm64 runtime/extraction passes before the retained hosted build-26200 versus required-26100 floor rejection. Exact-head PR Checks [29455294230](https://github.com/stablyai/orca/actions/runs/29455294230) and Golden E2E -[29455294207](https://github.com/stablyai/orca/actions/runs/29455294207) are green. Next is the -disconnected cache-state audit and purpose-named RED. Desktop consumers, cache publication, SSH -transfer/install, mode wiring, tuple enablement, publication, production keys/environment/seed, and -merge to `main` remain disconnected. +[29455294207](https://github.com/stablyai/orca/actions/runs/29455294207) are green. The cache audit +splits lock, immutable publication/quarantine, and in-use eviction into separate gates under +`E-M5-ARTIFACT-CACHE-AUDIT-001`. The absent-module RED is +`E-M5-ARTIFACT-CACHE-LOCK-LOCAL-RED-001`; the disconnected implementation and ten focused contracts +are locally green under `E-M5-ARTIFACT-CACHE-LOCK-LOCAL-001`, including a real second process, +heartbeat, cancellation, stale-dead reclaim, and live/ambiguous/displaced-owner safety. Artifact, +relay, release, typecheck, full lint, max-lines, and workflow gates pass. Both native job families +require the suite under `E-M5-ARTIFACT-CACHE-LOCK-CI-WIRING-LOCAL-001`; next is exact-head all-six +Node 24 execution. Desktop consumers, cache publication/quarantine/eviction, SSH transfer/install, +mode wiring, tuple enablement, publication, production keys/environment/seed, and merge to `main` +remain disconnected. ## Safety status @@ -338,7 +344,11 @@ merge to `main` remain disconnected. Exclusive owned staging and failure/cancellation cleanup are green locally and on all six native clients under `E-M5-ARTIFACT-EXTRACTION-LOCAL-001` and `E-M5-ARTIFACT-EXTRACTION-CI-001`; publication, quarantine, locking, and eviction are the - active audit/RED package and are not implemented yet. + active split packages and are not implemented yet. The lock-only audit/RED is recorded under + `E-M5-ARTIFACT-CACHE-AUDIT-001` and `E-M5-ARTIFACT-CACHE-LOCK-LOCAL-RED-001`; the implementation + is locally green and wired to both native families under `E-M5-ARTIFACT-CACHE-LOCK-LOCAL-001` + and `E-M5-ARTIFACT-CACHE-LOCK-CI-WIRING-LOCAL-001`. All-six execution remains required before + marking lock coordination complete. - [ ] Prove verified cached bytes can be transferred while the client is offline. - [ ] Preserve `ORCA_RELAY_PATH` behind the official-build trust boundary. 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 065f30724b2..d6eaeb57138 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 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: cache locking/publication/quarantine/eviction audit and purpose-named RED**. E-M5-ARTIFACT-EXTRACTION-CI-001 closes the disconnected exclusive-extraction and complete-tree-verification capability on all six native Node 24 clients within the corrected 80 MiB desktop ceiling. Audit the existing app-owned cache and process/window lifecycle, define the narrow publication package, and add purpose-named failing contracts before implementation. Do not add a desktop caller, SSH transfer/install, tuple enablement, mode wiring, release publication, or default behavior.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact-head artifact run 29455294208 proves synthetic hostile contracts and full-size extraction on darwin arm64/x64, Linux arm64/x64, and Windows arm64/x64; incremental RSS is 33,517,568–45,989,888 bytes and duration is 1,294.77–5,757.55ms. Both Linux oldest-userland supplements and Windows x64 floor pass. Windows arm64 runtime execution passes before the retained exact-floor assertion rejects hosted build 26200 as evidence for required build 26100. Exact-head PR Checks 29455294230 and Golden E2E 29455294207 pass. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, PR-contained rehearsals, and PR-contained release writes where checklist gates require them, but not merging to `main`. There is no release publication, cache consumer, SSH transfer/install, tuple enablement, or production/default behavior change. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: disconnected cross-process content-lock native gate**. E-M5-ARTIFACT-CACHE-LOCK-LOCAL-001 closes ten local contracts plus broad/static gates; E-M5-ARTIFACT-CACHE-LOCK-CI-WIRING-LOCAL-001 requires the suite on both native job families. Checkpoint and push, then require exact-head Node 24 execution on all six clients before cache-entry publication work. Do not add cache publication/quarantine/eviction, a desktop caller, SSH transfer/install, tuple enablement, mode wiring, release publication, or default behavior.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — the disconnected content lock uses a fully populated pending directory followed by atomic rename, a nonce-bound heartbeat through the original owner file handle, conservative same-host PID liveness, stale-dead-owner tombstoning, bounded/cancellable wait, directory-inode plus token ownership assertion, and owner-only release. Ten focused tests include a real second Node process; artifact, relay, release, typecheck, full lint, max-lines, workflow, and diff gates pass. The final direct-child harness stays inside the unchanged 30s test bound even while unrelated worktrees heavily saturate the host; broad elapsed times are recorded as contention-affected, not performance baselines. The suite is wired into both native Node 24 job families but exact-head all-six execution remains required. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, PR-contained rehearsals, and PR-contained release writes where checklist gates require them, but not merging to `main`. There is no release publication, cache entry, cache consumer, SSH transfer/install, tuple enablement, or production/default behavior change. 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) @@ -137,11 +137,11 @@ same change as the work it records. E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-001, and E-M5-PORTABLE-ARCHIVE-CI-001 prove the selected bounded archive, correction, all-six primary jobs, and both Linux oldest-userland supplements. Windows ZIP and upstream Node `.tar.xz` remain unchanged. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit cache ownership, - locking, atomic publication, quarantine, and bounded eviction after the disconnected extractor - closed on all six native clients under E-M5-ARTIFACT-EXTRACTION-CI-001. Add purpose-named RED - contracts before implementing the narrow cache-state package. Keep desktop call sites, SSH - behavior, mode wiring, tuple enablement, release publication, and defaults intact. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — checkpoint and collect + exact-head all-six Node 24 evidence for the disconnected content lock closed locally by + E-M5-ARTIFACT-CACHE-LOCK-LOCAL-001 and wired by + E-M5-ARTIFACT-CACHE-LOCK-CI-WIRING-LOCAL-001. Keep cache publication/quarantine/eviction, desktop + call sites, SSH behavior, mode wiring, tuple enablement, release publication, and defaults intact. - 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 @@ -12932,6 +12932,156 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea - Safety oracle: all artifacts remain unpublished Actions evidence. Production/default SSH is still legacy-only; no cache consumer, SSH path, mode, or enabled tuple exists. +### E-M5-ARTIFACT-CACHE-AUDIT-001 — Cache ownership and transaction package split + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed evidence head `ef76f38d4` on draft PR #8741. +- Exact audit commands: `rg --files src/main | rg '(cache|ssh-relay-artifact|lock|quarantine|evict)' | + sort`; targeted `rg -n` over `app.getPath('userData')|cache|lockfile|rename|quarantine|evict` in + `src/main`/`src/shared`; complete reads of the relay downloader, extractor, tree verifier, + selector/content-identity/path-policy modules, startup single-instance lock, and Milestone 5 cache + contracts. +- Findings: + - there is no relay cache root, cache lookup, publication proof, content lock, quarantine, eviction, + in-use lease, embedded-manifest caller, or SSH consumer; + - the downloader owns only an exclusive `wx` staging file and removes it on failure; the extractor + owns only an exclusive staging directory and returns a complete signed-tree verification result; + neither publishes or replaces shared state; + - official packaged Orca normally has an Electron single-instance lock per `userData` profile, but + development/E2E bypasses exist and separate profiles/builds may share or migrate files. The cache + contract therefore cannot rely on one in-memory main process; + - the authenticated `sha256:<64 lowercase hex>` content ID is the immutable entry key. A cache + path must derive only from its validated hex payload and archive name from the verified selected + artifact, never caller-controlled tuple/path text; + - publication must place the archive, verified runtime tree, and structured proof inside one + same-filesystem staging entry, sync proof/bytes as required, then rename the whole entry under an + exclusive content lock. No partial directory or proof written after rename may become selectable; + - stale-lock recovery must never delete a possibly active writer. A stale dead-owner lock is first + atomically renamed to an unselectable tombstone; every writer must revalidate its nonce-bound + ownership immediately before publication. A live or unprovably dead owner is waited out or times + out, never reclaimed; + - detected identity/proof/tree corruption is an integrity failure. Lookup must atomically move the + entry to a quarantine namespace before returning failure; it cannot reinterpret corruption as a + miss eligible for automatic legacy; + - eviction needs a separate in-use lease/recency contract. The 2 GiB cap may remove only complete, + idle entries and must conservatively retain an entry whose lease is active or ambiguous. +- Evidence-gated implementation split: + 1. `ssh-relay-artifact-cache-lock.ts`, `ssh-relay-artifact-cache-lock-record.ts`, and + `ssh-relay-artifact-cache-lock-lease.ts` — validated content-key paths, nonce-bound + cross-process acquisition/reclaim, strict record parsing, cancellation/timeout, heartbeat, + conservative liveness, atomic stale tombstoning, ownership assertion, and owner-only release; + 2. `ssh-relay-artifact-cache-entry.ts` — same-filesystem exclusive staging, structured immutable + proof, final archive/tree verification, atomic publication, lookup, and corruption quarantine; + 3. `ssh-relay-artifact-cache-eviction.ts` — in-use leases, recency, exact byte accounting, and + bounded 2 GiB eviction that never removes active/ambiguous entries. +- Consumer-disconnection oracle: each capability is source-test-only until packaged manifest loading, + proxy/certificate behavior, and later resolver gates close. No app startup/index call, SSH path, + setting, tuple, release publication, or production/default behavior is connected. +- Does not prove: any cache implementation, crash/concurrency behavior, disk-full/read-only behavior, + packaged Electron path selection, cross-client CI, SSH, native trust, or an enabled tuple. +- Follow-up: add and run the purpose-named cache-lock RED, implement only package 1, then require + focused/broad/static and all-six native evidence before cache-entry publication work begins. + +### E-M5-ARTIFACT-CACHE-LOCK-LOCAL-RED-001 — Cross-process content lock is absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: purpose-named `src/main/ssh/ssh-relay-artifact-cache-lock.test.ts` atop pushed evidence head + `ef76f38d4`; implementation module deliberately absent. +- Exact command: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-artifact-cache-lock.test.ts` +- Environment: macOS 26.2 arm64 build 25C56, Node v26.0.0, pnpm 10.24.0, Vitest 4.1.5. +- Result: required RED; one failed suite / zero collected tests in 389ms Vitest / 3.70s wall, + 132,366,336-byte maximum RSS, zero swaps. Import fails because + `./ssh-relay-artifact-cache-lock` does not exist. +- Contract encoded before implementation: + - accept only exact lowercase `sha256:<64 hex>` content IDs and derive a portable lock basename; + - serialize same-content writers and transfer ownership only after owner release; + - settle an aborted waiter without disturbing the active owner; + - reclaim only a stale lock whose same-host process is proven dead, by atomic tombstoning before + replacement; + - preserve a stale-looking same-host lock while its process remains alive; + - nonce-check ownership before later publication and never delete a successor lock after + displacement. +- Does not prove: implementation, heartbeat behavior, crash recovery, cross-process/native clients, + cache publication/quarantine/eviction, packaged Electron, SSH, or an enabled tuple. +- Follow-up: implement only `ssh-relay-artifact-cache-lock.ts`, run the six focused contracts, add + heartbeat/timeout/malformed-owner coverage if implementation exposes gaps, then run broad/static + gates and require all-six native execution. + +### E-M5-ARTIFACT-CACHE-LOCK-LOCAL-001 — Disconnected content lock passes locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted cache-lock implementation atop pushed evidence head `ef76f38d4`. +- Implementation: acquisition writes and syncs a complete owner record in a unique pending directory, + then atomically renames it to the fixed validated content lock. Active ownership is bound to a + random 128-bit nonce, directory device/inode, original owner-file handle, same-host PID, and a 5s + heartbeat. Wait is polled at 100ms, cancelled immediately, and bounded at 120s. Only a record stale + for 30s whose same-host PID is proven dead may be atomically renamed to a unique tombstone before + cleanup/replacement. A live, cross-host, missing, or malformed owner is never guessed dead. +- Structure gate: the first implementation placed acquisition, record parsing, and active lease in + one 331-code-line module; focused `oxlint` correctly failed `eslint(max-lines)` at the 300-line + limit. The implementation was split by concrete responsibility into 200-line acquisition/reclaim, + 80-line record, and 147-line active lease modules. No suppression or ratchet exception was added. +- Exact focused command: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-artifact-cache-lock.test.ts` +- Final focused result after formatting and child-harness correction: one passed file / 10 passed + tests in 22.73s Vitest / 68.66s wall, 110,608,384-byte maximum process RSS, zero swaps on a host + heavily saturated by unrelated worktrees. The tests themselves complete inside the unchanged 30s + per-test bound. Contracts prove fixed limits and frozen metadata, + exact lowercase content paths, same-process and real second-process serialization, cancellation, + owner-handle heartbeat, stale-dead-owner tombstoning, live/malformed-owner preservation, and + displaced nonce fail-closed/release-without-successor-deletion behavior. +- Fixture correction: the first post-implementation attempt used a non-hex manual owner token, so + strict record parsing intentionally classified it as ambiguous and waited. The run was terminated; + changing only the fixture to a valid 128-bit hex nonce made stale-dead and live-owner cases test + the intended branches. No production parser or liveness rule was weakened. +- Child-harness correction: the first exact-commit rerun launched a nested Vitest process for the + real second-process cell. Under unrelated host saturation, nested runner startup exceeded the 30s + test timeout even though lock logic had not failed. The final harness instead launches Node + directly with Node 24's built-in TypeScript stripping and a narrow relative-`.ts` resolver, then + imports and exercises the exact production lock module. This removes a package-runner startup + dependency without increasing the test timeout or duplicating lock logic. +- Exact broader commands and results: + - `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-artifact-*.test.ts`: + five passed files / one environment-skipped file, 95 passed / one skipped test in 126.12s Vitest / + 222.65s wall, 138,002,432-byte maximum RSS, zero swaps under the same unrelated host contention; + - `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts`: + 18 passed files / two environment/platform-skipped files, 263 passed / two skipped tests in + 166.03s Vitest / 276.43s wall, 198,787,072-byte maximum RSS, zero swaps under the same unrelated + host contention; + - `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs`: + 50 passed files / 279 passed tests in 42.00s Vitest / 44.67s wall, 190,152,704-byte maximum RSS, + zero swaps; + - `pnpm typecheck`; `pnpm lint`; `pnpm run check:max-lines-ratchet`; `git diff --check`: exit zero. + Full lint retains only existing warnings outside this package; reliability, Git compatibility, + line-budget, skill, and localization gates pass. +- Diff/consumer oracle: the preserved Milestone 0 resolver pair and release-required-assets pair have + zero diff. No cache entry/publication/quarantine/eviction, app/index caller, SSH path, setting, mode, + tuple, release write, or default change exists. +- Does not prove: Node 24/native clients, abrupt process kill on each OS, network filesystem/PID + namespace behavior, cache publication/quarantine/eviction, packaged Electron, SSH, or an enabled + tuple. + +### E-M5-ARTIFACT-CACHE-LOCK-CI-WIRING-LOCAL-001 — Both native families require lock contracts + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Purpose RED command: + `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-workflow.test.mjs` +- RED result: one failed / six passed tests in 519ms; the source contract requires the lock suite + twice but the workflow contains zero invocations, so the split count is one instead of three. +- GREEN result: the same command passes one file / seven tests in 398ms after adding the lock suite + to both POSIX and Windows native Node 24 contract steps. +- Oracle: every primary darwin/Linux/Windows x64/arm64 job must run same-process and real child-process + contention, cancellation, heartbeat, stale/live/ambiguous owner, and displaced-owner contracts. + It remains source-test-only and runs before unpublished artifact construction/upload. +- Does not prove: actual runner execution, cache publication, SSH, or an enabled tuple. +- Follow-up: checkpoint and push, then require exact-head all-six native jobs plus PR Checks and + Golden E2E before beginning immutable cache-entry publication/quarantine. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/src/main/ssh/ssh-relay-artifact-cache-lock-lease.ts b/src/main/ssh/ssh-relay-artifact-cache-lock-lease.ts new file mode 100644 index 00000000000..647daadf54e --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-lock-lease.ts @@ -0,0 +1,147 @@ +import { lstat, rename, rm, type FileHandle } from 'node:fs/promises' + +import { + readSshRelayArtifactCacheLockOwner, + sameSshRelayArtifactCacheLockDirectory, + sshRelayArtifactCacheLockOwnerBytes, + type SshRelayArtifactCacheLockDirectoryIdentity, + type SshRelayArtifactCacheLockOwnerRecord +} from './ssh-relay-artifact-cache-lock-record' +import type { SshRelayDigest } from './ssh-relay-runtime-identity' + +export const SSH_RELAY_ARTIFACT_CACHE_HEARTBEAT_INTERVAL_MS = 5_000 + +async function writeCompleteRecord(handle: FileHandle, bytes: Buffer): Promise { + let offset = 0 + while (offset < bytes.length) { + const { bytesWritten } = await handle.write(bytes, offset, bytes.length - offset, offset) + if (bytesWritten <= 0) { + throw new Error('SSH relay artifact cache lock heartbeat could not be persisted') + } + offset += bytesWritten + } + await handle.truncate(bytes.length) + await handle.sync() +} + +class SshRelayArtifactCacheLockLease { + readonly lockPath: string + readonly token: string + private readonly contentId: SshRelayDigest + private readonly directory: SshRelayArtifactCacheLockDirectoryIdentity + private readonly ownerHandle: FileHandle + private readonly owner: SshRelayArtifactCacheLockOwnerRecord + private heartbeat: Promise = Promise.resolve() + private heartbeatError: unknown + private heartbeatTimer: NodeJS.Timeout | undefined + private released = false + + constructor(options: { + lockPath: string + contentId: SshRelayDigest + token: string + directory: SshRelayArtifactCacheLockDirectoryIdentity + ownerHandle: FileHandle + owner: SshRelayArtifactCacheLockOwnerRecord + }) { + this.lockPath = options.lockPath + this.contentId = options.contentId + this.token = options.token + this.directory = options.directory + this.ownerHandle = options.ownerHandle + this.owner = options.owner + this.heartbeatTimer = setInterval( + () => this.queueHeartbeat(), + SSH_RELAY_ARTIFACT_CACHE_HEARTBEAT_INTERVAL_MS + ) + this.heartbeatTimer.unref() + } + + private queueHeartbeat(): void { + this.heartbeat = this.heartbeat.then(async () => { + if (this.released || this.heartbeatError) { + return + } + try { + await this.assertPathOwnership() + this.owner.heartbeatAtMs = Date.now() + await writeCompleteRecord(this.ownerHandle, sshRelayArtifactCacheLockOwnerBytes(this.owner)) + } catch (error) { + this.heartbeatError = error + } + }) + } + + private async assertPathOwnership(): Promise { + const metadata = await lstat(this.lockPath, { bigint: true }).catch(() => null) + const current = await readSshRelayArtifactCacheLockOwner(this.lockPath, this.contentId) + if ( + !metadata?.isDirectory() || + !sameSshRelayArtifactCacheLockDirectory(this.directory, metadata) || + current?.token !== this.token + ) { + throw new Error('SSH relay artifact cache lock ownership was displaced') + } + } + + async assertOwned(): Promise { + await this.heartbeat + if (this.released || this.heartbeatError) { + throw new Error('SSH relay artifact cache lock ownership is no longer active', { + cause: this.heartbeatError + }) + } + await this.assertPathOwnership() + } + + async release(): Promise { + if (this.released) { + return + } + this.released = true + if (this.heartbeatTimer) { + clearInterval(this.heartbeatTimer) + this.heartbeatTimer = undefined + } + await this.heartbeat + let owned = false + try { + await this.assertPathOwnership() + owned = true + } catch { + // A displaced owner must never remove the successor's lock. + } + if (!owned) { + await this.ownerHandle.close() + return + } + + const tombstone = `${this.lockPath}.released-${this.token}` + let renamed = false + try { + await rename(this.lockPath, tombstone) + renamed = true + } finally { + await this.ownerHandle.close() + } + if (renamed) { + await rm(tombstone, { recursive: true, force: true }) + } + } +} + +export type SshRelayArtifactCacheLock = Pick< + SshRelayArtifactCacheLockLease, + 'lockPath' | 'token' | 'assertOwned' | 'release' +> + +export function createSshRelayArtifactCacheLockLease(options: { + lockPath: string + contentId: SshRelayDigest + token: string + directory: SshRelayArtifactCacheLockDirectoryIdentity + ownerHandle: FileHandle + owner: SshRelayArtifactCacheLockOwnerRecord +}): SshRelayArtifactCacheLock { + return new SshRelayArtifactCacheLockLease(options) +} diff --git a/src/main/ssh/ssh-relay-artifact-cache-lock-record.ts b/src/main/ssh/ssh-relay-artifact-cache-lock-record.ts new file mode 100644 index 00000000000..0757558fdc9 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-lock-record.ts @@ -0,0 +1,80 @@ +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' + +import type { SshRelayDigest } from './ssh-relay-runtime-identity' + +export const SSH_RELAY_ARTIFACT_CACHE_LOCK_SCHEMA_VERSION = 1 + +export type SshRelayArtifactCacheLockOwnerRecord = { + schemaVersion: typeof SSH_RELAY_ARTIFACT_CACHE_LOCK_SCHEMA_VERSION + contentId: SshRelayDigest + token: string + hostname: string + pid: number + acquiredAtMs: number + heartbeatAtMs: number +} + +export type SshRelayArtifactCacheLockDirectoryIdentity = { dev: bigint; ino: bigint } + +export function sshRelayArtifactCacheErrorCode(error: unknown): string | undefined { + return typeof error === 'object' && error !== null && 'code' in error + ? String(error.code) + : undefined +} + +function validOwner( + value: unknown, + contentId: SshRelayDigest +): value is SshRelayArtifactCacheLockOwnerRecord { + if (!value || typeof value !== 'object') { + return false + } + const owner = value as Partial + return ( + owner.schemaVersion === SSH_RELAY_ARTIFACT_CACHE_LOCK_SCHEMA_VERSION && + owner.contentId === contentId && + typeof owner.token === 'string' && + /^[0-9a-f]{32}$/.test(owner.token) && + typeof owner.hostname === 'string' && + owner.hostname.length > 0 && + typeof owner.pid === 'number' && + Number.isSafeInteger(owner.pid) && + owner.pid > 0 && + typeof owner.acquiredAtMs === 'number' && + Number.isSafeInteger(owner.acquiredAtMs) && + owner.acquiredAtMs >= 0 && + typeof owner.heartbeatAtMs === 'number' && + Number.isSafeInteger(owner.heartbeatAtMs) && + owner.heartbeatAtMs >= owner.acquiredAtMs + ) +} + +export function sshRelayArtifactCacheLockOwnerBytes( + owner: SshRelayArtifactCacheLockOwnerRecord +): Buffer { + return Buffer.from(`${JSON.stringify(owner)}\n`, 'utf8') +} + +export async function readSshRelayArtifactCacheLockOwner( + lockPath: string, + contentId: SshRelayDigest +): Promise { + try { + const bytes = await readFile(join(lockPath, 'owner.json'), 'utf8') + const parsed: unknown = JSON.parse(bytes) + return validOwner(parsed, contentId) ? parsed : null + } catch (error) { + if (sshRelayArtifactCacheErrorCode(error) === 'ENOENT' || error instanceof SyntaxError) { + return null + } + throw error + } +} + +export function sameSshRelayArtifactCacheLockDirectory( + left: SshRelayArtifactCacheLockDirectoryIdentity, + right: SshRelayArtifactCacheLockDirectoryIdentity +): boolean { + return left.dev === right.dev && left.ino === right.ino +} diff --git a/src/main/ssh/ssh-relay-artifact-cache-lock.test.ts b/src/main/ssh/ssh-relay-artifact-cache-lock.test.ts new file mode 100644 index 00000000000..bae6bd23a26 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-lock.test.ts @@ -0,0 +1,293 @@ +import { spawn } from 'node:child_process' +import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises' +import { hostname, tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' + +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { + acquireSshRelayArtifactCacheLock, + sshRelayArtifactCacheLockPath, + SSH_RELAY_ARTIFACT_CACHE_LOCK_LIMITS +} from './ssh-relay-artifact-cache-lock' +import type { SshRelayDigest } from './ssh-relay-runtime-identity' + +const contentId = `sha256:${'a'.repeat(64)}` as SshRelayDigest +const contentHex = contentId.slice('sha256:'.length) +const manualOwnerToken = 'b'.repeat(32) +const successorOwnerToken = 'c'.repeat(32) +const temporaryDirectories: string[] = [] +const lockModuleUrl = pathToFileURL( + join(import.meta.dirname, 'ssh-relay-artifact-cache-lock.ts') +).href + +async function runLockChild(root: string): Promise<{ + completed: () => boolean + result: Promise<{ code: number | null; output: string }> +}> { + const childScript = ` + import { registerHooks } from 'node:module' + registerHooks({ + resolve(specifier, context, nextResolve) { + try { + return nextResolve(specifier, context) + } catch (error) { + if (specifier.startsWith('.')) return nextResolve(specifier + '.ts', context) + throw error + } + } + }) + const { acquireSshRelayArtifactCacheLock } = await import(${JSON.stringify(lockModuleUrl)}) + const lock = await acquireSshRelayArtifactCacheLock({ + cacheRoot: process.env.ORCA_SSH_RELAY_LOCK_CHILD_ROOT, + contentId: process.env.ORCA_SSH_RELAY_LOCK_CHILD_CONTENT_ID + }) + await lock.assertOwned() + await lock.release() + ` + const child = spawn( + process.execPath, + [ + '--experimental-strip-types', + '--disable-warning=MODULE_TYPELESS_PACKAGE_JSON', + '--input-type=module', + '--eval', + childScript + ], + { + cwd: process.cwd(), + env: { + ...process.env, + ORCA_SSH_RELAY_LOCK_CHILD_ROOT: root, + ORCA_SSH_RELAY_LOCK_CHILD_CONTENT_ID: contentId + }, + stdio: ['ignore', 'pipe', 'pipe'] + } + ) + let done = false + let output = '' + child.stdout.on('data', (bytes: Buffer) => (output += bytes.toString('utf8'))) + child.stderr.on('data', (bytes: Buffer) => (output += bytes.toString('utf8'))) + const result = new Promise<{ code: number | null; output: string }>((resolve) => { + child.on('exit', (code) => { + done = true + resolve({ code, output }) + }) + }) + return { completed: () => done, result } +} + +async function cacheRoot(): Promise { + const root = await mkdtemp(join(tmpdir(), 'orca-relay-cache-lock-')) + temporaryDirectories.push(root) + return root +} + +async function writeManualLock({ + root, + pid, + heartbeatAtMs, + token = manualOwnerToken +}: { + root: string + pid: number + heartbeatAtMs: number + token?: string +}): Promise { + const lockPath = sshRelayArtifactCacheLockPath(root, contentId) + await mkdir(lockPath, { recursive: true, mode: 0o700 }) + await writeFile( + join(lockPath, 'owner.json'), + `${JSON.stringify({ + schemaVersion: 1, + contentId, + token, + hostname: hostname(), + pid, + acquiredAtMs: heartbeatAtMs, + heartbeatAtMs + })}\n`, + { encoding: 'utf8', flag: 'wx', mode: 0o600 } + ) + return lockPath +} + +afterEach(async () => { + vi.useRealTimers() + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })) + ) +}) + +describe('SSH relay artifact cache content lock', () => { + it('pins bounded heartbeat, stale, polling, and acquisition timing', () => { + expect(SSH_RELAY_ARTIFACT_CACHE_LOCK_LIMITS).toEqual({ + heartbeatIntervalMs: 5_000, + staleAfterMs: 30_000, + waitTimeoutMs: 120_000, + waitPollMs: 100 + }) + expect(Object.isFrozen(SSH_RELAY_ARTIFACT_CACHE_LOCK_LIMITS)).toBe(true) + }) + + it('derives one portable lock path only from an exact lowercase content digest', async () => { + const root = await cacheRoot() + + expect(sshRelayArtifactCacheLockPath(root, contentId)).toBe( + join(root, 'locks', `${contentHex}.lock`) + ) + for (const invalid of [ + `sha256:${'A'.repeat(64)}`, + `sha256:${'a'.repeat(63)}`, + `sha256:${'a'.repeat(64)}/../escape`, + contentHex + ]) { + expect(() => sshRelayArtifactCacheLockPath(root, invalid as SshRelayDigest)).toThrow( + /content id/i + ) + } + }) + + it('serializes concurrent writers and transfers ownership only after owner release', async () => { + const root = await cacheRoot() + const first = await acquireSshRelayArtifactCacheLock({ cacheRoot: root, contentId }) + let secondResolved = false + const secondPending = acquireSshRelayArtifactCacheLock({ cacheRoot: root, contentId }).then( + (lock) => { + secondResolved = true + return lock + } + ) + + await new Promise((resolve) => setTimeout(resolve, 30)) + expect(secondResolved).toBe(false) + await first.assertOwned() + await first.release() + + const second = await secondPending + expect(second.token).not.toBe(first.token) + await second.assertOwned() + await second.release() + }) + + it('serializes a real second process on the same content lock', async () => { + const root = await cacheRoot() + const owner = await acquireSshRelayArtifactCacheLock({ cacheRoot: root, contentId }) + const child = await runLockChild(root) + + await new Promise((resolve) => setTimeout(resolve, 200)) + expect(child.completed()).toBe(false) + await owner.release() + + const result = await child.result + expect(result.code, result.output).toBe(0) + }) + + it('settles a cancelled waiter without disturbing the active owner', async () => { + const root = await cacheRoot() + const owner = await acquireSshRelayArtifactCacheLock({ cacheRoot: root, contentId }) + const controller = new AbortController() + const waiting = acquireSshRelayArtifactCacheLock({ + cacheRoot: root, + contentId, + signal: controller.signal + }) + + controller.abort(new Error('cancel cache lock wait')) + + await expect(waiting).rejects.toThrow(/cancel cache lock wait/i) + await owner.assertOwned() + await owner.release() + }) + + it('heartbeats through its owned file handle and retains nonce ownership', async () => { + vi.useFakeTimers() + vi.setSystemTime(100_000) + const root = await cacheRoot() + const owner = await acquireSshRelayArtifactCacheLock({ cacheRoot: root, contentId }) + const before = JSON.parse(await readFile(join(owner.lockPath, 'owner.json'), 'utf8')) as { + heartbeatAtMs: number + } + + await vi.advanceTimersByTimeAsync(5_000) + await owner.assertOwned() + + const after = JSON.parse(await readFile(join(owner.lockPath, 'owner.json'), 'utf8')) as { + heartbeatAtMs: number + token: string + } + expect(before.heartbeatAtMs).toBe(100_000) + expect(after).toMatchObject({ heartbeatAtMs: 105_000, token: owner.token }) + await owner.release() + }) + + it('atomically tombstones and replaces a stale lock whose local owner is dead', async () => { + const root = await cacheRoot() + await writeManualLock({ root, pid: 2_147_483_647, heartbeatAtMs: 0 }) + + const replacement = await acquireSshRelayArtifactCacheLock({ cacheRoot: root, contentId }) + + expect(replacement.token).not.toBe(manualOwnerToken) + await replacement.assertOwned() + await replacement.release() + }) + + it('never reclaims a stale-looking lock whose local owner is still alive', async () => { + const root = await cacheRoot() + const lockPath = await writeManualLock({ root, pid: process.pid, heartbeatAtMs: 0 }) + const controller = new AbortController() + const waiting = acquireSshRelayArtifactCacheLock({ + cacheRoot: root, + contentId, + signal: controller.signal + }) + setTimeout(() => controller.abort(new Error('stop live-owner wait')), 30) + + await expect(waiting).rejects.toThrow(/stop live-owner wait/i) + const owner = JSON.parse(await readFile(join(lockPath, 'owner.json'), 'utf8')) as { + token: string + } + expect(owner.token).toBe(manualOwnerToken) + }) + + it('preserves an unparseable owner instead of guessing that its writer is dead', async () => { + const root = await cacheRoot() + const lockPath = sshRelayArtifactCacheLockPath(root, contentId) + await mkdir(lockPath, { recursive: true, mode: 0o700 }) + await writeFile(join(lockPath, 'owner.json'), '{not-json', { mode: 0o600 }) + const controller = new AbortController() + const waiting = acquireSshRelayArtifactCacheLock({ + cacheRoot: root, + contentId, + signal: controller.signal + }) + setTimeout(() => controller.abort(new Error('stop ambiguous-owner wait')), 30) + + await expect(waiting).rejects.toThrow(/stop ambiguous-owner wait/i) + expect(await readFile(join(lockPath, 'owner.json'), 'utf8')).toBe('{not-json') + }) + + it('does not publish or delete through a lock whose nonce ownership was displaced', async () => { + const root = await cacheRoot() + const owner = await acquireSshRelayArtifactCacheLock({ cacheRoot: root, contentId }) + const displacedPath = `${owner.lockPath}.displaced` + await rename(owner.lockPath, displacedPath) + await writeManualLock({ + root, + pid: process.pid, + heartbeatAtMs: Date.now(), + token: successorOwnerToken + }) + + await expect(owner.assertOwned()).rejects.toThrow(/ownership/i) + await owner.release() + + const successor = JSON.parse(await readFile(join(owner.lockPath, 'owner.json'), 'utf8')) as { + token: string + } + expect(successor.token).toBe(successorOwnerToken) + }) +}) diff --git a/src/main/ssh/ssh-relay-artifact-cache-lock.ts b/src/main/ssh/ssh-relay-artifact-cache-lock.ts new file mode 100644 index 00000000000..fba88efa976 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-lock.ts @@ -0,0 +1,191 @@ +import { randomBytes } from 'node:crypto' +import { lstat, mkdir, open, realpath, rename, rm } from 'node:fs/promises' +import { hostname } from 'node:os' +import { basename, dirname, join, resolve } from 'node:path' + +import { + createSshRelayArtifactCacheLockLease, + SSH_RELAY_ARTIFACT_CACHE_HEARTBEAT_INTERVAL_MS, + type SshRelayArtifactCacheLock +} from './ssh-relay-artifact-cache-lock-lease' +import { + readSshRelayArtifactCacheLockOwner, + sshRelayArtifactCacheErrorCode, + sshRelayArtifactCacheLockOwnerBytes, + SSH_RELAY_ARTIFACT_CACHE_LOCK_SCHEMA_VERSION, + type SshRelayArtifactCacheLockOwnerRecord +} from './ssh-relay-artifact-cache-lock-record' +import type { SshRelayDigest } from './ssh-relay-runtime-identity' + +const STALE_AFTER_MS = 30_000 +const WAIT_TIMEOUT_MS = 2 * 60_000 +const WAIT_POLL_MS = 100 +const CONTENT_ID = /^sha256:([0-9a-f]{64})$/ + +function exactContentHex(contentId: SshRelayDigest): string { + const match = CONTENT_ID.exec(contentId) + if (!match) { + throw new Error('SSH relay artifact cache content ID must be an exact lowercase SHA-256 digest') + } + return match[1] +} + +export function sshRelayArtifactCacheLockPath( + cacheRoot: string, + contentId: SshRelayDigest +): string { + return resolve(cacheRoot, 'locks', `${exactContentHex(contentId)}.lock`) +} + +function localProcessIsAlive(pid: number): boolean { + try { + process.kill(pid, 0) + return true + } catch (error) { + return sshRelayArtifactCacheErrorCode(error) !== 'ESRCH' + } +} + +async function waitForRetry(signal: AbortSignal): Promise { + signal.throwIfAborted() + await new Promise((resolveWait, rejectWait) => { + const timer = setTimeout(() => { + signal.removeEventListener('abort', abort) + resolveWait() + }, WAIT_POLL_MS) + const abort = (): void => { + clearTimeout(timer) + rejectWait(signal.reason) + } + signal.addEventListener('abort', abort, { once: true }) + }) +} + +async function createPendingOwner( + lockPath: string, + contentId: SshRelayDigest, + token: string +): Promise<{ pendingPath: string; owner: SshRelayArtifactCacheLockOwnerRecord }> { + const now = Date.now() + const owner: SshRelayArtifactCacheLockOwnerRecord = { + schemaVersion: SSH_RELAY_ARTIFACT_CACHE_LOCK_SCHEMA_VERSION, + contentId, + token, + hostname: hostname(), + pid: process.pid, + acquiredAtMs: now, + heartbeatAtMs: now + } + const pendingPath = `${lockPath}.pending-${token}` + await mkdir(pendingPath, { mode: 0o700 }) + try { + const handle = await open(join(pendingPath, 'owner.json'), 'wx', 0o600) + try { + await handle.writeFile(sshRelayArtifactCacheLockOwnerBytes(owner)) + await handle.sync() + } finally { + await handle.close() + } + return { pendingPath, owner } + } catch (error) { + await rm(pendingPath, { recursive: true, force: true }).catch(() => {}) + throw error + } +} + +async function tryCreateLock( + lockPath: string, + contentId: SshRelayDigest, + token: string +): Promise { + const { pendingPath, owner } = await createPendingOwner(lockPath, contentId, token) + try { + await rename(pendingPath, lockPath) + return owner + } catch (error) { + await rm(pendingPath, { recursive: true, force: true }).catch(() => {}) + if (['EEXIST', 'ENOTEMPTY', 'EPERM'].includes(sshRelayArtifactCacheErrorCode(error) ?? '')) { + return null + } + throw error + } +} + +async function tryReclaimDeadOwner(lockPath: string, contentId: SshRelayDigest): Promise { + const observed = await readSshRelayArtifactCacheLockOwner(lockPath, contentId) + if ( + !observed || + Date.now() - observed.heartbeatAtMs < STALE_AFTER_MS || + observed.hostname !== hostname() || + localProcessIsAlive(observed.pid) + ) { + return false + } + const confirmed = await readSshRelayArtifactCacheLockOwner(lockPath, contentId) + if ( + !confirmed || + confirmed.token !== observed.token || + confirmed.heartbeatAtMs !== observed.heartbeatAtMs + ) { + return false + } + const tombstone = `${lockPath}.stale-${randomBytes(16).toString('hex')}` + try { + // Why: a stale lock becomes unselectable before cleanup; no reclaimer deletes through a live path. + await rename(lockPath, tombstone) + } catch (error) { + if (['ENOENT', 'EEXIST', 'ENOTEMPTY'].includes(sshRelayArtifactCacheErrorCode(error) ?? '')) { + return false + } + throw error + } + await rm(tombstone, { recursive: true, force: true }) + return true +} + +export async function acquireSshRelayArtifactCacheLock({ + cacheRoot, + contentId, + signal +}: { + cacheRoot: string + contentId: SshRelayDigest + signal?: AbortSignal +}): Promise { + const timeout = AbortSignal.timeout(WAIT_TIMEOUT_MS) + const effectiveSignal = signal ? AbortSignal.any([signal, timeout]) : timeout + effectiveSignal.throwIfAborted() + const logicalPath = sshRelayArtifactCacheLockPath(cacheRoot, contentId) + await mkdir(dirname(logicalPath), { recursive: true, mode: 0o700 }) + const lockParent = await realpath(dirname(logicalPath)) + const lockPath = join(lockParent, basename(logicalPath)) + + while (true) { + effectiveSignal.throwIfAborted() + const token = randomBytes(16).toString('hex') + const owner = await tryCreateLock(lockPath, contentId, token) + if (owner) { + const directory = await lstat(lockPath, { bigint: true }) + const ownerHandle = await open(join(lockPath, 'owner.json'), 'r+') + return createSshRelayArtifactCacheLockLease({ + lockPath, + contentId, + token, + directory, + ownerHandle, + owner + }) + } + if (await tryReclaimDeadOwner(lockPath, contentId)) { + continue + } + await waitForRetry(effectiveSignal) + } +} + +export const SSH_RELAY_ARTIFACT_CACHE_LOCK_LIMITS = Object.freeze({ + heartbeatIntervalMs: SSH_RELAY_ARTIFACT_CACHE_HEARTBEAT_INTERVAL_MS, + staleAfterMs: STALE_AFTER_MS, + waitTimeoutMs: WAIT_TIMEOUT_MS, + waitPollMs: WAIT_POLL_MS +}) From 44ef150da39b3977f4a462dbf52f55c5988b6bf7 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 19:44:01 -0400 Subject: [PATCH 171/267] fix: release relay cache locks on Windows Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 8 +- ...github-release-implementation-checklist.md | 96 ++++++++++++++++++- .../ssh-relay-artifact-cache-lock-lease.ts | 14 +-- .../ssh/ssh-relay-artifact-cache-lock.test.ts | 24 +++-- .../ssh/ssh-relay-artifact-extraction.test.ts | 4 +- 5 files changed, 122 insertions(+), 24 deletions(-) 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 43fb630f58d..df25d6405e6 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 @@ -42,8 +42,12 @@ splits lock, immutable publication/quarantine, and in-use eviction into separate are locally green under `E-M5-ARTIFACT-CACHE-LOCK-LOCAL-001`, including a real second process, heartbeat, cancellation, stale-dead reclaim, and live/ambiguous/displaced-owner safety. Artifact, relay, release, typecheck, full lint, max-lines, and workflow gates pass. Both native job families -require the suite under `E-M5-ARTIFACT-CACHE-LOCK-CI-WIRING-LOCAL-001`; next is exact-head all-six -Node 24 execution. Desktop consumers, cache publication/quarantine/eviction, SSH transfer/install, +require the suite under `E-M5-ARTIFACT-CACHE-LOCK-CI-WIRING-LOCAL-001`. Exact-head Windows x64 then +found the open-heartbeat-handle directory-rename incompatibility under +`E-M5-ARTIFACT-CACHE-LOCK-CI-WINDOWS-RED-001`; Windows arm64 also found equivalent zlib truncation +wording outside the extraction test oracle under `E-M5-ARTIFACT-EXTRACTION-CI-WINDOWS-ARM64-RED-001`. +The ownership-preserving release order and equivalent-error assertion are locally green under +`E-M5-WINDOWS-NATIVE-GATE-CORRECTIONS-LOCAL-001`; a fresh all-six Node 24 run is now required. Desktop consumers, cache publication/quarantine/eviction, SSH transfer/install, mode wiring, tuple enablement, publication, production keys/environment/seed, and merge to `main` remain disconnected. 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 d6eaeb57138..2d74dec03ae 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 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: disconnected cross-process content-lock native gate**. E-M5-ARTIFACT-CACHE-LOCK-LOCAL-001 closes ten local contracts plus broad/static gates; E-M5-ARTIFACT-CACHE-LOCK-CI-WIRING-LOCAL-001 requires the suite on both native job families. Checkpoint and push, then require exact-head Node 24 execution on all six clients before cache-entry publication work. Do not add cache publication/quarantine/eviction, a desktop caller, SSH transfer/install, tuple enablement, mode wiring, release publication, or default behavior.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — the disconnected content lock uses a fully populated pending directory followed by atomic rename, a nonce-bound heartbeat through the original owner file handle, conservative same-host PID liveness, stale-dead-owner tombstoning, bounded/cancellable wait, directory-inode plus token ownership assertion, and owner-only release. Ten focused tests include a real second Node process; artifact, relay, release, typecheck, full lint, max-lines, workflow, and diff gates pass. The final direct-child harness stays inside the unchanged 30s test bound even while unrelated worktrees heavily saturate the host; broad elapsed times are recorded as contention-affected, not performance baselines. The suite is wired into both native Node 24 job families but exact-head all-six execution remains required. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, PR-contained rehearsals, and PR-contained release writes where checklist gates require them, but not merging to `main`. There is no release publication, cache entry, cache consumer, SSH transfer/install, tuple enablement, or production/default behavior change. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: checkpoint and fresh all-six Windows-native correction proof**. E-M5-WINDOWS-NATIVE-GATE-CORRECTIONS-LOCAL-001 closes the narrow local release-order and equivalent truncation-oracle corrections exposed by run `29458719333`. Checkpoint and push the exact tree, then require all six primary Node 24 jobs plus companion PR Checks and Golden E2E to pass before beginning immutable cache-entry publication/quarantine. Do not add a desktop caller, SSH transfer/install, tuple enablement, mode wiring, release publication, or default behavior.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — resumed with user authority for end-to-end PR-contained implementation, commits, pushes, CI, rehearsals, and release writes, with merging to `main` explicitly prohibited. The worktree was clean and exactly matched pushed head `869603a3e86933cc9985f9e4d240e615146d86d8` at session start. Both Windows jobs then reproduced the open-handle `EPERM`; Windows arm64 also exposed equivalent zlib truncation wording outside the assertion oracle. The corrected local tree passes 31 focused lock/extraction tests, 95 artifact tests with one environment skip, all 279 release-runtime tests, typecheck, full lint, formatting, max-lines, and diff gates. Cache publication remains gated; there is no release publication, cache entry, cache consumer, SSH transfer/install, tuple enablement, or production/default behavior change. 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) @@ -13082,6 +13082,98 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea - Follow-up: checkpoint and push, then require exact-head all-six native jobs plus PR Checks and Golden E2E before beginning immutable cache-entry publication/quarantine. +### E-M5-ARTIFACT-CACHE-LOCK-CI-WINDOWS-RED-001 — Open heartbeat handle blocks Windows release rename + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `869603a3e86933cc9985f9e4d240e615146d86d8`; SSH Relay Runtime + Artifacts run [29458719333](https://github.com/stablyai/orca/actions/runs/29458719333), Windows x64 + job [87497521527](https://github.com/stablyai/orca/actions/runs/29458719333/job/87497521527), GitHub + Actions runner `GitHub Actions 1000056419`, label `windows-2022`; and Windows arm64 job + [87497521511](https://github.com/stablyai/orca/actions/runs/29458719333/job/87497521511), runner + `GitHub Actions 1000056418`, label `windows-11-arm`. +- Exact log command: + `gh api repos/stablyai/orca/actions/jobs/87497521527/logs 2>&1 | tail -240`. +- Result: required CI RED in `Run runtime artifact contract tests`; one failed lock file / six failed + lock tests while 64 other files passed, with 414 passed / 13 skipped tests overall in 16.66s. + Five owner-release cases fail with `EPERM` renaming the lock directory to its release tombstone; + the displacement fixture fails with the same `EPERM`. The open `owner.json` heartbeat handle keeps + the containing directory non-renamable on Windows. The later `ENOENT` is waiter/test cleanup fallout + after release throws, not a separate acquisition defect. +- Safety decision: retain heartbeat writes through the original owner handle while the lease is + active. On release, stop and settle heartbeat work, assert directory identity plus nonce, close the + owned handle, then atomically rename to a release tombstone. A close-before-final-assert change is + forbidden; a rename retry that guesses through ownership changes is forbidden. Replace the fixture's + live-directory rename with portable nonce displacement so Windows proves successor preservation + without depending on a filesystem operation it explicitly rejects while the handle is live. +- Does not prove: corrected Windows behavior, Windows arm64, the other five native jobs, cache + publication/quarantine/eviction, packaged Electron, SSH, or an enabled tuple. +- Follow-up: implement the narrow release-order/fixture correction, run focused and broad local gates, + push, and require a fresh exact-head all-six native run before cache-entry publication begins. + +### E-M5-ARTIFACT-EXTRACTION-CI-WINDOWS-ARM64-RED-001 — Equivalent zlib truncation wording rejected by oracle + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `869603a3e86933cc9985f9e4d240e615146d86d8`; SSH Relay Runtime + Artifacts run [29458719333](https://github.com/stablyai/orca/actions/runs/29458719333), Windows arm64 + job [87497521511](https://github.com/stablyai/orca/actions/runs/29458719333/job/87497521511), runner + `GitHub Actions 1000056418`, label `windows-11-arm`. +- Exact log command: + `gh api repos/stablyai/orca/actions/jobs/87497521511/logs 2>&1 | sed -n '1320,1360p'`. +- Result: archive mutation is rejected, but the test fails because Windows arm64 zlib returns + `unexpected end of file`; the assertion allowed semantically identical `unexpected EOF` but not + that spelling. This is an assertion portability defect, not accepted extraction or an integrity + fallback. The same contract step reports two failed files / 63 passed files before lock-test + fallout is separated. +- Safety decision: accept only the equivalent explicit truncation phrase in the hostile-mutation + assertion. Do not alter extraction, cleanup, archive/tree verification, fallback classification, + or fail-closed behavior. +- Follow-up: run the focused extraction and artifact globs locally, then require fresh Windows arm64 + and all-six native execution on the corrected head. + +### E-M5-WINDOWS-NATIVE-GATE-CORRECTIONS-LOCAL-001 — Narrow lock release and truncation-oracle corrections pass locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted correction atop exact pushed head + `869603a3e86933cc9985f9e4d240e615146d86d8`, following + E-M5-ARTIFACT-CACHE-LOCK-CI-WINDOWS-RED-001 and + E-M5-ARTIFACT-EXTRACTION-CI-WINDOWS-ARM64-RED-001. +- Lock correction: release stops and settles heartbeat work, final-checks directory identity and + nonce through the still-open owner handle, closes that handle, atomically renames the lock to its + unselectable tombstone, then removes the tombstone. Active heartbeats still write only through the + originally opened handle. The displacement contract now replaces only the nonce-bearing owner + record, which is portable while Windows keeps the directory non-renamable, and still proves + `assertOwned()` failure plus successor preservation on release. +- Extraction-oracle correction: the hostile archive mutation assertion adds only zlib's explicit + equivalent `unexpected end of file` spelling alongside `unexpected EOF`; production extraction, + cleanup, verification, and fallback classification are unchanged. +- Exact commands/results: + - `pnpm exec oxfmt --check src/main/ssh/ssh-relay-artifact-cache-lock-lease.ts + src/main/ssh/ssh-relay-artifact-cache-lock.test.ts + src/main/ssh/ssh-relay-artifact-extraction.test.ts`: pass; + - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 + src/main/ssh/ssh-relay-artifact-cache-lock.test.ts + src/main/ssh/ssh-relay-artifact-extraction.test.ts`: two passed files / 31 passed tests in 2.88s + Vitest / 8.25s wall, 156,925,952-byte maximum RSS, zero swaps; + - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 + src/main/ssh/ssh-relay-artifact-*.test.ts`: five passed files / one environment-skipped file, + 95 passed / one skipped tests in 23.52s Vitest / 26.47s wall, 143,228,928-byte maximum RSS, + zero swaps; + - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 + config/scripts/ssh-relay-runtime-*.test.mjs`: 50 passed files / 279 passed tests in 105.46s + Vitest / 109.78s wall, 193,576,960-byte maximum RSS, zero swaps; + - `pnpm typecheck`; `pnpm lint`; `pnpm run check:max-lines-ratchet`; `git diff --check`: pass. Full + lint retains only existing unrelated warnings. +- Consumer/diff oracle: the preserved Milestone 0 resolver pair and release-required-assets pair have + zero diff. No cache publication/quarantine/eviction, desktop caller, SSH path, setting, mode, tuple, + release write, or default behavior is added. +- Does not prove: Windows semantics, any native client, cache publication, packaged Electron, SSH, or + an enabled tuple. Fresh exact-head all-six native execution remains mandatory. +- Follow-up: checkpoint/push, require all six primary native jobs plus PR Checks and Golden E2E, and + record exact runner/job evidence before opening the immutable cache-entry publication package. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/src/main/ssh/ssh-relay-artifact-cache-lock-lease.ts b/src/main/ssh/ssh-relay-artifact-cache-lock-lease.ts index 647daadf54e..854e6de3c86 100644 --- a/src/main/ssh/ssh-relay-artifact-cache-lock-lease.ts +++ b/src/main/ssh/ssh-relay-artifact-cache-lock-lease.ts @@ -116,17 +116,11 @@ class SshRelayArtifactCacheLockLease { return } + // Windows cannot rename a directory containing an open file; ownership is final-checked first. + await this.ownerHandle.close() const tombstone = `${this.lockPath}.released-${this.token}` - let renamed = false - try { - await rename(this.lockPath, tombstone) - renamed = true - } finally { - await this.ownerHandle.close() - } - if (renamed) { - await rm(tombstone, { recursive: true, force: true }) - } + await rename(this.lockPath, tombstone) + await rm(tombstone, { recursive: true, force: true }) } } diff --git a/src/main/ssh/ssh-relay-artifact-cache-lock.test.ts b/src/main/ssh/ssh-relay-artifact-cache-lock.test.ts index bae6bd23a26..47aba5ed9ca 100644 --- a/src/main/ssh/ssh-relay-artifact-cache-lock.test.ts +++ b/src/main/ssh/ssh-relay-artifact-cache-lock.test.ts @@ -1,5 +1,5 @@ import { spawn } from 'node:child_process' -import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises' +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { hostname, tmpdir } from 'node:os' import { join } from 'node:path' import { pathToFileURL } from 'node:url' @@ -273,14 +273,20 @@ describe('SSH relay artifact cache content lock', () => { it('does not publish or delete through a lock whose nonce ownership was displaced', async () => { const root = await cacheRoot() const owner = await acquireSshRelayArtifactCacheLock({ cacheRoot: root, contentId }) - const displacedPath = `${owner.lockPath}.displaced` - await rename(owner.lockPath, displacedPath) - await writeManualLock({ - root, - pid: process.pid, - heartbeatAtMs: Date.now(), - token: successorOwnerToken - }) + const now = Date.now() + // Windows forbids renaming a directory with the live heartbeat handle; nonce replacement is portable. + await writeFile( + join(owner.lockPath, 'owner.json'), + `${JSON.stringify({ + schemaVersion: 1, + contentId, + token: successorOwnerToken, + hostname: hostname(), + pid: process.pid, + acquiredAtMs: now, + heartbeatAtMs: now + })}\n` + ) await expect(owner.assertOwned()).rejects.toThrow(/ownership/i) await owner.release() diff --git a/src/main/ssh/ssh-relay-artifact-extraction.test.ts b/src/main/ssh/ssh-relay-artifact-extraction.test.ts index b0b8a8e3dd3..e57a49a7944 100644 --- a/src/main/ssh/ssh-relay-artifact-extraction.test.ts +++ b/src/main/ssh/ssh-relay-artifact-extraction.test.ts @@ -495,7 +495,9 @@ describe('SSH relay artifact extraction', () => { archivePath: value.archivePath, outputDirectory }) - ).rejects.toThrow(/archive|central directory|changed|invalid|unexpected EOF|zip/i) + ).rejects.toThrow( + /archive|central directory|changed|invalid|unexpected (?:EOF|end of file)|zip/i + ) await waitForPath(outputDirectory) await writeFile(value.archivePath, Buffer.alloc(value.artifact.archive.size, 0x62)) From 5e742e4df305cc7ea883864e6d5569dc2e6f5d5d Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 20:02:16 -0400 Subject: [PATCH 172/267] docs: record native relay cache lock evidence Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 12 ++- ...github-release-implementation-checklist.md | 73 ++++++++++++++++++- 2 files changed, 78 insertions(+), 7 deletions(-) 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 df25d6405e6..01d7bd1cfec 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 @@ -47,7 +47,13 @@ found the open-heartbeat-handle directory-rename incompatibility under `E-M5-ARTIFACT-CACHE-LOCK-CI-WINDOWS-RED-001`; Windows arm64 also found equivalent zlib truncation wording outside the extraction test oracle under `E-M5-ARTIFACT-EXTRACTION-CI-WINDOWS-ARM64-RED-001`. The ownership-preserving release order and equivalent-error assertion are locally green under -`E-M5-WINDOWS-NATIVE-GATE-CORRECTIONS-LOCAL-001`; a fresh all-six Node 24 run is now required. Desktop consumers, cache publication/quarantine/eviction, SSH transfer/install, +`E-M5-WINDOWS-NATIVE-GATE-CORRECTIONS-LOCAL-001`. Replacement exact-head run +[29459424864](https://github.com/stablyai/orca/actions/runs/29459424864) closes the content lock on all +six primary native Node 24 jobs under `E-M5-ARTIFACT-CACHE-LOCK-CI-001`; both Linux supplements, +PR Checks [29459424977](https://github.com/stablyai/orca/actions/runs/29459424977), and Golden E2E +[29459424789](https://github.com/stablyai/orca/actions/runs/29459424789) pass. Windows arm64 retains +only the explicit hosted build-26200 versus required-26100 floor rejection after runtime/tree smoke. +Next is the disconnected immutable publication/lookup/quarantine package. Desktop consumers, cache eviction, SSH transfer/install, mode wiring, tuple enablement, publication, production keys/environment/seed, and merge to `main` remain disconnected. @@ -351,8 +357,8 @@ remain disconnected. active split packages and are not implemented yet. The lock-only audit/RED is recorded under `E-M5-ARTIFACT-CACHE-AUDIT-001` and `E-M5-ARTIFACT-CACHE-LOCK-LOCAL-RED-001`; the implementation is locally green and wired to both native families under `E-M5-ARTIFACT-CACHE-LOCK-LOCAL-001` - and `E-M5-ARTIFACT-CACHE-LOCK-CI-WIRING-LOCAL-001`. All-six execution remains required before - marking lock coordination complete. + and `E-M5-ARTIFACT-CACHE-LOCK-CI-WIRING-LOCAL-001`; all-six native execution is closed by + `E-M5-ARTIFACT-CACHE-LOCK-CI-001`. Immutable publication/lookup/quarantine remains next. - [ ] Prove verified cached bytes can be transferred while the client is offline. - [ ] Preserve `ORCA_RELAY_PATH` behind the official-build trust boundary. 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 2d74dec03ae..f567096625a 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 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: checkpoint and fresh all-six Windows-native correction proof**. E-M5-WINDOWS-NATIVE-GATE-CORRECTIONS-LOCAL-001 closes the narrow local release-order and equivalent truncation-oracle corrections exposed by run `29458719333`. Checkpoint and push the exact tree, then require all six primary Node 24 jobs plus companion PR Checks and Golden E2E to pass before beginning immutable cache-entry publication/quarantine. Do not add a desktop caller, SSH transfer/install, tuple enablement, mode wiring, release publication, or default behavior.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — resumed with user authority for end-to-end PR-contained implementation, commits, pushes, CI, rehearsals, and release writes, with merging to `main` explicitly prohibited. The worktree was clean and exactly matched pushed head `869603a3e86933cc9985f9e4d240e615146d86d8` at session start. Both Windows jobs then reproduced the open-handle `EPERM`; Windows arm64 also exposed equivalent zlib truncation wording outside the assertion oracle. The corrected local tree passes 31 focused lock/extraction tests, 95 artifact tests with one environment skip, all 279 release-runtime tests, typecheck, full lint, formatting, max-lines, and diff gates. Cache publication remains gated; there is no release publication, cache entry, cache consumer, SSH transfer/install, tuple enablement, or production/default behavior change. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: immutable cache-entry publication/lookup/quarantine package**. E-M5-ARTIFACT-CACHE-LOCK-CI-001 closes the disconnected lock capability on exact head `44ef150da39b3977f4a462dbf52f55c5988b6bf7`: all six primary native Node 24 jobs, both Linux oldest-userland supplements, PR Checks, and Golden E2E pass. Begin only the audited package-2 boundary: refine immutable-entry contracts, add a purpose-named RED, then implement exclusive same-filesystem staging, structured proof, final archive/tree revalidation, atomic publication, lookup, and corruption quarantine. Keep eviction/in-use leases and every consumer/default path separate.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — resumed with user authority for end-to-end PR-contained implementation, commits, pushes, CI, rehearsals, and release writes, with merging to `main` explicitly prohibited. The first native run exposed and recorded Windows open-handle rename and equivalent zlib-error-oracle REDs; narrow corrections passed locally and at exact-head CI. In replacement run `29459424864`, all six primary jobs pass the 10 lock and 21 extraction contracts plus deterministic artifact build/smoke. The workflow's sole failure is the retained Windows arm64 hosted build-26200 versus required-26100 floor rejection after runtime/archive/tree/Node/PTTY/watcher proof; no tuple is enabled from it. PR Checks `29459424977` and Golden E2E `29459424789` pass. There is no release publication, cache entry, cache consumer, SSH transfer/install, tuple enablement, or production/default behavior change. 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) @@ -1092,8 +1092,14 @@ Suggested concrete modules: The disconnected full-tree verifier passes locally and on all six native clients under E-M5-ARTIFACT-EXTRACTION-LOCAL-001 and E-M5-ARTIFACT-EXTRACTION-CI-001; atomic cache publication is deliberately absent and keeps this combined item open. -- [ ] Coordinate cache ownership across multiple Orca processes and windows. -- [ ] Recover stale download/extraction locks without deleting an active writer. +- [x] Coordinate cache ownership across multiple Orca processes and windows. + E-M5-ARTIFACT-CACHE-LOCK-LOCAL-001 and E-M5-ARTIFACT-CACHE-LOCK-CI-001 prove real + second-process contention and ownership transfer on all six native client tuples. Cache-entry + integration remains separately gated. +- [x] Recover stale download/extraction locks without deleting an active writer. + E-M5-ARTIFACT-CACHE-LOCK-LOCAL-001 and E-M5-ARTIFACT-CACHE-LOCK-CI-001 prove conservative + same-host liveness, stale-dead-owner tombstoning, live/ambiguous-owner preservation, and + displaced-owner successor safety. No downloader/extractor consumer is connected yet. - [ ] Never select partial files, partial directories, or entries with a failed verification record. - [ ] Re-verify cached metadata and expected identity before use; define when full file rehashing is required. @@ -13174,6 +13180,65 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea - Follow-up: checkpoint/push, require all six primary native jobs plus PR Checks and Golden E2E, and record exact runner/job evidence before opening the immutable cache-entry publication package. +### E-M5-ARTIFACT-CACHE-LOCK-CI-001 — Content lock and corrected extraction contracts pass all six native clients + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed correction head `44ef150da39b3977f4a462dbf52f55c5988b6bf7`; SSH Relay + Runtime Artifacts run [29459424864](https://github.com/stablyai/orca/actions/runs/29459424864). +- Exact evidence commands: + - `gh api 'repos/stablyai/orca/actions/runs/29459424864/jobs?per_page=100' --jq ...` for job, + runner, label, step, and timestamp identity; + - `gh api repos/stablyai/orca/actions/jobs//logs 2>&1 | rg + "ssh-relay-artifact-cache-lock.test.ts|ssh-relay-artifact-extraction.test.ts|Test Files|Tests " | + tail -8` for each primary job; + - `gh api repos/stablyai/orca/actions/jobs/87500906175/logs 2>&1 | rg -n + "Node runtime version|archive|tree|PTY|watcher|Windows|osBuild|26100|26200|runner does not prove|Process completed" + | tail -100` for the retained Windows arm64 floor gap. +- Primary native results: + + | Client tuple | Job / runner | Job duration | Contract duration | Lock suite | Extraction suite | Result | + | --- | --- | ---: | ---: | ---: | ---: | --- | + | darwin-arm64 | [87499607559](https://github.com/stablyai/orca/actions/runs/29459424864/job/87499607559), `GitHub Actions 1000056437`, `macos-15` | 1m52s | 13s | 10 pass, 530ms | 21 pass, 524ms | PASS | + | darwin-x64 | [87499607640](https://github.com/stablyai/orca/actions/runs/29459424864/job/87499607640), `GitHub Actions 1000056435`, `macos-15-intel` | 6m45s | 1m03s | 10 pass, 825ms | 21 pass, 3,062ms | PASS | + | linux-arm64-glibc | [87499607604](https://github.com/stablyai/orca/actions/runs/29459424864/job/87499607604), `GitHub Actions 1000056440`, `ubuntu-24.04-arm` | 12m20s | 11s | 10 pass, 514ms | 21 pass, 784ms | PASS | + | linux-x64-glibc | [87499607591](https://github.com/stablyai/orca/actions/runs/29459424864/job/87499607591), `GitHub Actions 1000056439`, `ubuntu-24.04` | 3m38s | 14s | 10 pass, 569ms | 21 pass, 949ms | PASS | + | win32-arm64 | [87499607605](https://github.com/stablyai/orca/actions/runs/29459424864/job/87499607605), `GitHub Actions 1000056441`, `windows-11-arm` | 9m01s | 29s | 10 pass, 801ms | 21 pass, 1,152ms | PASS | + | win32-x64 | [87499607584](https://github.com/stablyai/orca/actions/runs/29459424864/job/87499607584), `GitHub Actions 1000056438`, `windows-2022` | 5m29s | 22s | 10 pass, 751ms | 21 pass, 1,698ms | PASS | + +- Aggregate contract results: each POSIX job passes 64 files; each Windows job passes 65 files. The + suite includes same-process contention, a real second Node process, bounded cancellation, + owner-handle heartbeat, same-host stale-dead reclaim, live/malformed-owner preservation, and + nonce-displacement fail-closed behavior. Both Windows architectures therefore prove the corrected + close-after-final-ownership-check release ordering on native NTFS semantics. +- Supplemental results: Windows x64 oldest-baseline job + [87500906186](https://github.com/stablyai/orca/actions/runs/29459424864/job/87500906186) passes. Linux + x64 job [87501380503](https://github.com/stablyai/orca/actions/runs/29459424864/job/87501380503) + and Linux arm64 job + [87501380508](https://github.com/stablyai/orca/actions/runs/29459424864/job/87501380508) pass their + pinned oldest-userland supplements. +- Retained floor gap: Windows arm64 job + [87500906175](https://github.com/stablyai/orca/actions/runs/29459424864/job/87500906175) reconstructs + and verifies the exact runtime, archive, and tree and records Node/PTTY/watcher proof, then correctly + rejects hosted Windows build `10.0.26200` because the manifest requires build `26100`. This expected + fail-closed evidence is the workflow's sole failure and does not enable or close that floor cell. +- Companion regression evidence: PR Checks run + [29459424977](https://github.com/stablyai/orca/actions/runs/29459424977), job + [87499607955](https://github.com/stablyai/orca/actions/runs/29459424977/job/87499607955), runner + `GitHub Actions 1000056436` / `ubuntu-latest`, passes in 13m56s. Golden E2E run + [29459424789](https://github.com/stablyai/orca/actions/runs/29459424789) passes Linux job + [87499607002](https://github.com/stablyai/orca/actions/runs/29459424789/job/87499607002) in 4m31s and + macOS job [87499607004](https://github.com/stablyai/orca/actions/runs/29459424789/job/87499607004) + in 4m31s. +- Closure: cross-process content ownership, bounded wait/cancellation, heartbeat, conservative stale + recovery, owner-only release, native Windows release ordering, and equivalent truncation diagnostics + are closed for the disconnected cache-lock capability. +- Residual gaps: abrupt process kill was not separately injected on every OS; network filesystems and + PID namespaces remain unproved. Cache-entry publication/lookup/quarantine, in-use lease/eviction, + packaged Electron path selection, SSH, and every tuple/default path remain absent. +- Follow-up: checkpoint the evidence, then begin only audited package 2 with a purpose-named immutable + cache-entry RED. Keep eviction/in-use leases and consumers separate. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 3c1ead8fb1e1f3b64abaf78705abb0c801a1d64b Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 20:46:44 -0400 Subject: [PATCH 173/267] feat: add immutable relay artifact cache entries --- .../workflows/ssh-relay-runtime-artifacts.yml | 18 +- .../ssh-relay-runtime-workflow.test.mjs | 8 +- ...elease-implementation-checklist-summary.md | 7 +- ...github-release-implementation-checklist.md | 181 +++++++++- .../ssh-relay-artifact-cache-entry-fixture.ts | 176 ++++++++++ ...lay-artifact-cache-entry-full-size.test.ts | 141 ++++++++ .../ssh-relay-artifact-cache-entry-proof.ts | 81 +++++ ...relay-artifact-cache-entry-verification.ts | 288 ++++++++++++++++ .../ssh-relay-artifact-cache-entry.test.ts | 233 +++++++++++++ .../ssh/ssh-relay-artifact-cache-entry.ts | 318 ++++++++++++++++++ 10 files changed, 1445 insertions(+), 6 deletions(-) create mode 100644 src/main/ssh/ssh-relay-artifact-cache-entry-fixture.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-entry-proof.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-entry-verification.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-entry.test.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-entry.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 2a8b4540579..4d8f5b7de5c 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -265,6 +265,7 @@ jobs: src/main/ssh/ssh-relay-artifact-download.test.ts \ src/main/ssh/ssh-relay-artifact-extraction.test.ts \ src/main/ssh/ssh-relay-artifact-cache-lock.test.ts \ + src/main/ssh/ssh-relay-artifact-cache-entry.test.ts \ src/main/ssh/ssh-relay-runtime-identity.test.ts \ config/scripts/ssh-relay-runtime-compatibility.test.mjs \ config/scripts/ssh-relay-node-release-verification.test.mjs \ @@ -503,7 +504,7 @@ jobs: cp "$first_output"/*.tar.br "$first_output"/*.spdx.json "$first_output"/*.provenance.json "$first_output"/*.identity.json \ "$signing_report" "$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}/" - - name: Measure full-size desktop extraction boundary + - name: Measure full-size desktop extraction and cache boundaries shell: bash run: | set -euo pipefail @@ -519,6 +520,12 @@ jobs: pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 --reporter=verbose \ src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts test ! -e "$extraction_output" + ORCA_SSH_RELAY_FULL_SIZE_ARCHIVE="$archive" \ + ORCA_SSH_RELAY_FULL_SIZE_IDENTITY="$identity" \ + ORCA_SSH_RELAY_FULL_SIZE_OUTPUT="$extraction_output" \ + pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 --reporter=verbose \ + src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts + test ! -e "$extraction_output" - name: Upload unpublished artifact evidence uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 @@ -790,6 +797,7 @@ jobs: src/main/ssh/ssh-relay-artifact-download.test.ts ` src/main/ssh/ssh-relay-artifact-extraction.test.ts ` src/main/ssh/ssh-relay-artifact-cache-lock.test.ts ` + src/main/ssh/ssh-relay-artifact-cache-entry.test.ts ` src/main/ssh/ssh-relay-runtime-identity.test.ts ` config/scripts/ssh-relay-runtime-compatibility.test.mjs ` config/scripts/ssh-relay-node-release-verification.test.mjs ` @@ -1031,7 +1039,7 @@ jobs: Copy-Item -LiteralPath $signingReport -Destination $evidence Copy-Item -LiteralPath $sourceSignatureReport -Destination $evidence - - name: Measure full-size desktop extraction boundary + - name: Measure full-size desktop extraction and cache boundaries shell: pwsh run: | $ErrorActionPreference = 'Stop' @@ -1050,6 +1058,12 @@ jobs: if (Test-Path -LiteralPath $extractionOutput) { throw 'full-size desktop extraction measurement cleanup failed' } + pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 --reporter=verbose ` + src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts + if ($LASTEXITCODE -ne 0) { throw 'full-size desktop cache measurement failed' } + if (Test-Path -LiteralPath $extractionOutput) { + throw 'full-size desktop cache measurement cleanup failed' + } - name: Upload unpublished artifact evidence uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index f584e962ba4..de35c44d31b 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -95,13 +95,13 @@ describe('SSH relay runtime artifact workflow', () => { (step) => step.name === 'Upload unpublished artifact evidence' ) const measurementIndex = steps.findIndex( - (step) => step.name === 'Measure full-size desktop extraction boundary' + (step) => step.name === 'Measure full-size desktop extraction and cache boundaries' ) const windowsBuildIndex = windowsSteps.findIndex( (step) => step.name === 'Build twice, inspect, smoke, and compare exact runtime' ) const windowsMeasurementIndex = windowsSteps.findIndex( - (step) => step.name === 'Measure full-size desktop extraction boundary' + (step) => step.name === 'Measure full-size desktop extraction and cache boundaries' ) const windowsUploadIndex = windowsSteps.findIndex( (step) => step.name === 'Upload unpublished artifact evidence' @@ -123,6 +123,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-artifact-download.test.ts', 'ssh-relay-artifact-extraction.test.ts', 'ssh-relay-artifact-cache-lock.test.ts', + 'ssh-relay-artifact-cache-entry.test.ts', 'ssh-relay-runtime-identity.test.ts' ]) { // Why: portable desktop selection contracts need proof on every native runner family, not @@ -132,6 +133,9 @@ describe('SSH relay runtime artifact workflow', () => { expect( source.split('src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts') ).toHaveLength(3) + expect( + source.split('src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts') + ).toHaveLength(3) expect(source).toContain('pnpm install --frozen-lockfile --ignore-scripts') expect(source).toContain('--connect-timeout 20 --max-time 300 --retry 2') expect(source).toContain('mkdir -p "$output_root"') 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 01d7bd1cfec..e0689062502 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 @@ -53,7 +53,12 @@ six primary native Node 24 jobs under `E-M5-ARTIFACT-CACHE-LOCK-CI-001`; both Li PR Checks [29459424977](https://github.com/stablyai/orca/actions/runs/29459424977), and Golden E2E [29459424789](https://github.com/stablyai/orca/actions/runs/29459424789) pass. Windows arm64 retains only the explicit hosted build-26200 versus required-26100 floor rejection after runtime/tree smoke. -Next is the disconnected immutable publication/lookup/quarantine package. Desktop consumers, cache eviction, SSH transfer/install, +`E-M5-ARTIFACT-CACHE-ENTRY-AUDIT-001` fixes the disconnected immutable +publication/lookup/quarantine transaction; the absent-module contract RED is +`E-M5-ARTIFACT-CACHE-ENTRY-LOCAL-RED-001`. Seventeen local contracts plus broad/static gates are +green under `E-M5-ARTIFACT-CACHE-ENTRY-LOCAL-001`; both native families require the source suite and +exact full-size cold/warm measurement under `E-M5-ARTIFACT-CACHE-ENTRY-CI-WIRING-LOCAL-001`. Native +execution is next. Desktop consumers, cache eviction, SSH transfer/install, mode wiring, tuple enablement, publication, production keys/environment/seed, and merge to `main` remain disconnected. 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 f567096625a..e05839096b4 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    -Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: immutable cache-entry publication/lookup/quarantine package**. E-M5-ARTIFACT-CACHE-LOCK-CI-001 closes the disconnected lock capability on exact head `44ef150da39b3977f4a462dbf52f55c5988b6bf7`: all six primary native Node 24 jobs, both Linux oldest-userland supplements, PR Checks, and Golden E2E pass. Begin only the audited package-2 boundary: refine immutable-entry contracts, add a purpose-named RED, then implement exclusive same-filesystem staging, structured proof, final archive/tree revalidation, atomic publication, lookup, and corruption quarantine. Keep eviction/in-use leases and every consumer/default path separate.
    +Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: immutable cache-entry native/full-size gate**. E-M5-ARTIFACT-CACHE-ENTRY-LOCAL-001 closes 17 local transaction contracts plus broad/static gates; E-M5-ARTIFACT-CACHE-ENTRY-CI-WIRING-LOCAL-001 requires both the source suite and exact full-size cold-publication/verified-lookup measurement in both native job families. Checkpoint/push, then require all six Node 24 jobs and full-size time/RSS cells before cache-entry closure or any eviction/consumer work.
    Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — resumed with user authority for end-to-end PR-contained implementation, commits, pushes, CI, rehearsals, and release writes, with merging to `main` explicitly prohibited. The first native run exposed and recorded Windows open-handle rename and equivalent zlib-error-oracle REDs; narrow corrections passed locally and at exact-head CI. In replacement run `29459424864`, all six primary jobs pass the 10 lock and 21 extraction contracts plus deterministic artifact build/smoke. The workflow's sole failure is the retained Windows arm64 hosted build-26200 versus required-26100 floor rejection after runtime/archive/tree/Node/PTTY/watcher proof; no tuple is enabled from it. PR Checks `29459424977` and Golden E2E `29459424789` pass. There is no release publication, cache entry, cache consumer, SSH transfer/install, tuple enablement, or production/default behavior change. 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) @@ -13239,6 +13239,185 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea - Follow-up: checkpoint the evidence, then begin only audited package 2 with a purpose-named immutable cache-entry RED. Keep eviction/in-use leases and consumers separate. +### E-M5-ARTIFACT-CACHE-ENTRY-AUDIT-001 — Immutable entry transaction and corruption boundary + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed evidence head `5e742e4df305cc7ea883864e6d5569dc2e6f5d5d`, after + E-M5-ARTIFACT-CACHE-LOCK-CI-001 closes the prerequisite lock gate. +- Exact audit commands: complete reads of `ssh-relay-artifact-extraction.ts`, + `ssh-relay-artifact-tree-verification.ts`, `ssh-relay-runtime-identity.ts`, + `ssh-relay-artifact-selector.ts`, `ssh-relay-artifact-schema.ts`, + `ssh-relay-artifact-download.ts`, `ssh-relay-artifact-consistency.ts`, the extraction fixtures and + hostile tests, and the Milestone 5/7 cache/proof contracts; targeted `rg -n` over cache entry, + proof, same-filesystem, quarantine, integrity-error, file-sync, and atomic-rename patterns. +- Findings and fixed transaction: + - the caller supplies only a recursively frozen signature-verified selected artifact, an exact + verified archive path, an app-owned cache root, and optional cancellation. No Electron path or + SSH consumer is introduced in this package; + - the exact lowercase content ID alone derives `entries/`. The signed archive name is + the only archive basename. Tuple IDs, release tags, and caller-controlled text never derive + paths; + - publication acquires the proven content lock, removes only same-content token-shaped stale + staging left by a prior reclaimed owner, creates an exclusive sibling pending entry, streams and + syncs the archive through bounded SHA-256/size/state verification, extracts into `runtime`, and + writes a strict versioned `proof.json` last; + - proof binds schema, tuple/content identity, release tag, archive name/size/hash, file count, and + expanded bytes. Unknown/missing/duplicate-meaning fields are rejected; matching proof alone is + never treated as ongoing byte integrity; + - before publication, the implementation re-opens the proof and archive and full-tree rehashes the + complete staged runtime against the selected signed tuple. Only then may one same-filesystem + directory rename make the entry selectable. Failure/cancellation removes only owned staging; + - lookup acquires the same content lock and treats only an absent exact final path as a cache miss. + It strictly revalidates root shape, allowed members, proof, full archive, and full runtime tree; + - any detected partial entry, proof/identity mismatch, archive mutation, unexpected member, or tree + mutation is an integrity failure. The exact final entry is first atomically renamed into the + cache-root quarantine namespace, then an explicit integrity error is thrown. Quarantine failure + also fails closed and never becomes a miss; + - publication encountering a valid existing entry returns it unchanged. Publication encountering + corrupt existing state quarantines it and may replace it only from the freshly reverified source + archive in the same call; + - archive/tree verification remains intentionally expensive on cache selection because these bytes + can become the client-offline transfer source. Remote warm launch proof/latency policy is a later + Milestone 7 concern and is not weakened here. +- Concrete module split: `ssh-relay-artifact-cache-entry-proof.ts` owns strict proof bytes/parsing; + `ssh-relay-artifact-cache-entry-verification.ts` owns bounded archive copy/hash and complete entry + validation; `ssh-relay-artifact-cache-entry.ts` owns lock-scoped staging, publication, lookup, + quarantine, and cleanup. No vague helper/common module is permitted. +- Required RED contracts: exact path derivation; Linux and Windows archive publication; no visible + final path on failure/cancellation; strict proof and allowed-member validation; final archive/tree + revalidation; valid-existing reuse; concurrent same-content serialization; stale staging cleanup; + proof/archive/missing/unexpected/tree corruptions quarantined with an integrity error; and corrupt + existing recovery only from freshly verified bytes. +- Deliberately separate: in-use leases, recency, 2 GiB eviction, Electron cache-root selection, + proxy/certificate behavior, packaged manifest loading, ORCA_RELAY_PATH, SSH transfer/install, + fallback classification, setting/mode, tuple enablement, and release/default behavior. +- Follow-up: add the purpose-named cache-entry test with production modules absent, record the RED, + then implement only this transaction and require focused/broad/static/all-six native evidence. + +### E-M5-ARTIFACT-CACHE-ENTRY-LOCAL-RED-001 — Immutable cache-entry capability is absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: purpose-named `ssh-relay-artifact-cache-entry.test.ts` and its concrete Linux/Windows + archive fixture atop exact pushed evidence head `5e742e4df305cc7ea883864e6d5569dc2e6f5d5d`; production + entry modules deliberately absent. +- Exact command: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 + src/main/ssh/ssh-relay-artifact-cache-entry.test.ts`. +- Result: required RED; one failed suite / zero collected tests in 153ms Vitest / 1.11s wall, + 132,071,424-byte maximum RSS, zero swaps. Import fails only because + `./ssh-relay-artifact-cache-entry` does not exist. +- Contracts encoded before implementation: + - exact lowercase content-key path derivation and rejection of path-bearing/uppercase/malformed IDs; + - complete Linux TAR/Brotli and Windows ZIP publication with exact archive/runtime/proof members, + signed aggregate metadata, full file bytes, and successful revalidated lookup; + - exact miss without creating/quarantining state; no final entry after archive failure or + cancellation; concurrent publisher serialization and byte-identical existing-entry reuse; + - same-content stale token staging cleanup only after ownership; + - proof, archive, runtime-file, unexpected-member, missing-member, and partial-final corruption each + move the final entry to quarantine and raise the explicit integrity error rather than a miss; + - corrupt existing state can be republished only by passing the fresh source archive through the + complete verified publication transaction again. +- Consumer-disconnection oracle: tests import the capability directly. No index/app/Electron cache + root, downloader, SSH, setting, mode, tuple, release write, or default path is connected. +- Does not prove: implementation, native client behavior, full-size resource budgets, disk faults, + crash timing, in-use leases/eviction, packaged Electron, SSH, or any enabled tuple. +- Follow-up: implement only the audited proof/verification/transaction modules, make the focused + suite green without weakening any corruption oracle, then run broad/static and all-six native gates. + +### E-M5-ARTIFACT-CACHE-ENTRY-LOCAL-001 — Disconnected immutable publication and quarantine pass locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted package atop exact pushed evidence head + `5e742e4df305cc7ea883864e6d5569dc2e6f5d5d`, following + E-M5-ARTIFACT-CACHE-ENTRY-AUDIT-001 and E-M5-ARTIFACT-CACHE-ENTRY-LOCAL-RED-001. +- Implementation: + - `ssh-relay-artifact-cache-entry-proof.ts` owns a strict 16 KiB-bounded schema-1 proof binding + tuple/content/release, archive name/size/hash, and runtime file/expanded-byte aggregates; + - `ssh-relay-artifact-cache-entry-verification.ts` streams exact regular archives with a 64 KiB + buffer, SHA-256, stable file-state checks, destination sync, and failure cleanup; complete entry + validation strictly checks allowed root members, proof, archive, and the entire signed runtime tree; + - `ssh-relay-artifact-cache-entry.ts` validates physical app-owned directories, derives paths only + from exact content hex, acquires the proven content lock, cleans only token-shaped same-content + stale staging, publishes by one same-filesystem directory rename after final revalidation, and + atomically quarantines detected corruption before raising + `SshRelayArtifactCacheIntegrityError`; + - publication reuses a fully revalidated existing entry unchanged. A corrupt existing entry is + replaceable only after quarantine and a complete fresh-source copy/extract/proof/revalidation + transaction. Lookup treats only absent exact final state as a miss and full-rehashes archive/tree. +- Review correction: focused tests first passed 14 contracts. Manual handle-path review found that a + destination-open failure could leave the already-open source archive handle alive; the source now + closes on that branch. Proof tests were expanded to identity mismatch and unknown-field rejection, + directory-order assertions were made portable, and fixed transaction/copy/proof budgets were added. + No corruption behavior was weakened. +- Exact focused command: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 + src/main/ssh/ssh-relay-artifact-cache-entry.test.ts`. +- Final focused result: one passed file / 17 passed tests in 1.87s Vitest / 3.10s wall, + 137,035,776-byte maximum RSS, zero swaps. Contracts prove fixed 5m/80 MiB/64 KiB/16 KiB limits, + exact paths, Linux and Windows archive families, atomic complete publication, exact miss, + failure/cancellation invisibility, concurrent serialization/reuse, stale staging cleanup, strict + proof identity/fields, proof/archive/runtime/unexpected/missing/partial corruption quarantine, and + recovery only from freshly reverified bytes. +- Exact broader commands/results: + - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 + src/main/ssh/ssh-relay-artifact-*.test.ts`: final tree passes six files / two environment-skipped + full-size files, 112 passed / two skipped tests in 164.85s Vitest / 194.56s wall, + 123,568,128-byte maximum RSS, zero swaps under severe unrelated host contention; + - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 + src/main/ssh/ssh-relay-*.test.ts`: 19 passed files / two skipped files, 280 passed / two skipped + tests in 78.76s Vitest / 95.05s wall, 215,089,152-byte maximum RSS, zero swaps; + - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 + config/scripts/ssh-relay-runtime-*.test.mjs`: 50 passed files / 279 passed tests in 41.66s Vitest / + 45.35s wall, 190,808,064-byte maximum RSS, zero swaps; + - `pnpm typecheck`; `pnpm lint`; `pnpm run check:max-lines-ratchet`: pass. Full lint retains only + existing unrelated warnings; no max-lines disable or ratchet exception was added; + - `/usr/bin/git diff --check`: pass. The Orca-attributed Git wrapper first failed to create its + here-document temporary file because the shared host had only 115 MiB free; the system Git binary + proved the diff itself clean without deleting user data. Host-contention elapsed times are not + performance baselines. +- Consumer/diff oracle: direct source tests are the only caller. The preserved Milestone 0 resolver + pair and release-required-assets pair have zero diff. No Electron cache root, index/app caller, + downloader connection, eviction, SSH path, setting/mode, tuple, release write, or default change + exists. +- Does not prove: exact full-size cold/warm resources, Node 24/native clients, crash/power-loss + durability, read-only/disk-full/quota/inode behavior, in-use lease/eviction, packaged Electron, SSH, + or any enabled tuple. +- Follow-up: require source and exact full-size measurement on all six native jobs. Do not check the + combined publication item or begin eviction/consumer work until that evidence is recorded. + +### E-M5-ARTIFACT-CACHE-ENTRY-CI-WIRING-LOCAL-001 — Both native families require source and full-size cache gates + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source-suite purpose RED: after adding `ssh-relay-artifact-cache-entry.test.ts` to the workflow + source oracle, the exact workflow test fails one / passes six because the workflow has zero native + invocations and the source split count is one instead of three. +- Source-suite GREEN: adding the purpose suite to both POSIX and PowerShell native Node 24 contract + commands makes the workflow suite pass seven tests. +- Full-size purpose RED: after adding + `ssh-relay-artifact-cache-entry-full-size.test.ts` to the source oracle, the same workflow suite + fails one / passes six because the measurement has zero native invocations. The renamed combined + measurement step also required the ordering oracle's exact name to follow the expanded boundary. +- Final GREEN command/result: + `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 + config/scripts/ssh-relay-runtime-workflow.test.mjs + src/main/ssh/ssh-relay-artifact-cache-entry.test.ts + src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts`: workflow and focused source suites + pass; the environment-only full-size file is skipped locally, for 24 passed / one skipped tests. +- Native oracle: every primary client job must run all 17 transaction/corruption contracts before + artifact construction. After constructing the exact full-size artifact, every job must measure cold + copy/extract/proof/two-layer revalidation and warm proof/archive/full-tree lookup, log exact time and + incremental RSS, stay within 5m and 80 MiB per phase, and remove its owned cache root. +- Consumer-disconnection oracle: all artifacts remain unpublished Actions evidence and no desktop or + SSH caller exists. +- Does not prove: runner execution, exact full-size metrics, eviction/in-use safety, SSH, or an enabled + tuple. +- Follow-up: checkpoint/push and require all six primary jobs plus companion PR Checks/Golden E2E + before cache-entry closure. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/src/main/ssh/ssh-relay-artifact-cache-entry-fixture.ts b/src/main/ssh/ssh-relay-artifact-cache-entry-fixture.ts new file mode 100644 index 00000000000..de2bd48b420 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-entry-fixture.ts @@ -0,0 +1,176 @@ +import { createHash } from 'node:crypto' +import { writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { brotliCompressSync, constants as zlibConstants } from 'node:zlib' + +import nacl from 'tweetnacl' +import yazl from 'yazl' + +import type { SshRelayArtifactManifest, SshRelayRuntimeTuple } from './ssh-relay-artifact-schema' +import { + createSshRelayArtifactTestManifest, + createSshRelayWindowsArtifactTestManifest +} from './ssh-relay-artifact-test-manifest' +import { + selectSshRelayArtifact, + type SshRelayHostEvidence, + type SshRelaySelectedArtifact +} from './ssh-relay-artifact-selector' +import { + signSshRelayArtifactManifest, + sshRelayManifestKeyId, + verifySshRelayArtifactManifest +} from './ssh-relay-manifest-signature' +import { sshRelayRuntimeArchiveName } from './ssh-relay-release-asset' +import { computeSshRelayRuntimeContentId, type SshRelayDigest } from './ssh-relay-runtime-identity' + +const keyPair = nacl.sign.keyPair.fromSeed(Uint8Array.from({ length: 32 }, (_, index) => index)) + +function sha256(bytes: Uint8Array): SshRelayDigest { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +function tarOctal(header: Buffer, value: number, offset: number, length: number): void { + header.write(value.toString(8).padStart(length - 1, '0'), offset, length - 1, 'ascii') + header[offset + length - 1] = 0 +} + +function tarHeader(entry: SshRelayRuntimeTuple['entries'][number], size: number): Buffer { + const header = Buffer.alloc(512) + header.write(entry.type === 'directory' ? `${entry.path}/` : entry.path, 0, 100, 'utf8') + tarOctal(header, entry.mode, 100, 8) + tarOctal(header, 0, 108, 8) + tarOctal(header, 0, 116, 8) + tarOctal(header, size, 124, 12) + tarOctal(header, 0, 136, 12) + header.fill(0x20, 148, 156) + header[156] = entry.type === 'directory' ? 0x35 : 0x30 + header.write('ustar\0', 257, 6, 'ascii') + header.write('00', 263, 2, 'ascii') + const checksum = header.reduce((total, byte) => total + byte, 0) + header.write(`${checksum.toString(8).padStart(6, '0')}\0 `, 148, 8, 'ascii') + return header +} + +function tarBrotli(tuple: SshRelayRuntimeTuple, fileBytes: ReadonlyMap): Buffer { + const blocks: Buffer[] = [] + for (const entry of tuple.entries) { + const bytes = entry.type === 'file' ? fileBytes.get(entry.path) : undefined + if (entry.type === 'file' && !bytes) { + throw new Error(`Missing cache fixture bytes for ${entry.path}`) + } + blocks.push(tarHeader(entry, bytes?.length ?? 0)) + if (bytes) { + blocks.push(bytes, Buffer.alloc((512 - (bytes.length % 512)) % 512)) + } + } + blocks.push(Buffer.alloc(1024)) + return brotliCompressSync(Buffer.concat(blocks), { + params: { + [zlibConstants.BROTLI_PARAM_QUALITY]: 9, + [zlibConstants.BROTLI_PARAM_LGWIN]: 20 + } + }) +} + +async function zipArchive( + tuple: SshRelayRuntimeTuple, + fileBytes: ReadonlyMap +): Promise { + const zip = new yazl.ZipFile() + const mtime = new Date('2026-07-14T00:00:00.000Z') + for (const entry of tuple.entries) { + const options = { mode: entry.mode, mtime, forceDosTimestamp: true } + if (entry.type === 'directory') { + zip.addEmptyDirectory(entry.path, options) + continue + } + const bytes = fileBytes.get(entry.path) + if (!bytes) { + throw new Error(`Missing cache fixture bytes for ${entry.path}`) + } + zip.addBuffer(bytes, entry.path, { ...options, compress: true, compressionLevel: 9 }) + } + zip.end({ forceZip64Format: false }) + const chunks: Buffer[] = [] + for await (const chunk of zip.outputStream) { + chunks.push(Buffer.from(chunk)) + } + return Buffer.concat(chunks) +} + +export type SshRelayArtifactCacheEntryFixture = { + archivePath: string + artifact: SshRelaySelectedArtifact + fileBytes: ReadonlyMap +} + +export async function createSshRelayArtifactCacheEntryFixture({ + root, + os +}: { + root: string + os: 'linux' | 'win32' +}): Promise { + const manifest: SshRelayArtifactManifest = + os === 'win32' + ? createSshRelayWindowsArtifactTestManifest() + : createSshRelayArtifactTestManifest() + const tuple = manifest.tuples[0] + const fileBytes = new Map() + for (const entry of tuple.entries) { + if (entry.type !== 'file') { + continue + } + const bytes = Buffer.from(`cache entry fixture:${tuple.tupleId}:${entry.path}`) + fileBytes.set(entry.path, bytes) + entry.size = bytes.length + entry.sha256 = sha256(bytes) + } + for (const attestation of tuple.nativeVerification.files) { + const entry = tuple.entries.find((candidate) => candidate.path === attestation.path) + if (!entry || entry.type !== 'file') { + throw new Error(`Missing attested cache fixture entry: ${attestation.path}`) + } + attestation.sha256 = entry.sha256 + } + tuple.contentId = computeSshRelayRuntimeContentId(tuple) + tuple.archive.name = sshRelayRuntimeArchiveName(tuple.tupleId, tuple.contentId) + const files = tuple.entries.filter((entry) => entry.type === 'file') + tuple.archive.fileCount = files.length + tuple.archive.expandedSize = files.reduce((total, entry) => total + entry.size, 0) + const archive = os === 'win32' ? await zipArchive(tuple, fileBytes) : tarBrotli(tuple, fileBytes) + tuple.archive.size = archive.length + tuple.archive.sha256 = sha256(archive) + manifest.signatures = [signSshRelayArtifactManifest(manifest, keyPair.secretKey)] + const verified = verifySshRelayArtifactManifest(manifest, [ + { keyId: sshRelayManifestKeyId(keyPair.publicKey), publicKey: keyPair.publicKey } + ]) + const host: SshRelayHostEvidence = + os === 'win32' + ? { + os, + architecture: 'x64', + processTranslated: false, + build: 22631, + openSshVersion: '9.5p1', + powerShellVersion: '5.1', + dotNetFrameworkRelease: 528040 + } + : { + os, + architecture: 'x64', + processTranslated: false, + kernelVersion: '6.8', + libc: { family: 'glibc', version: '2.39' }, + libstdcxxVersion: '6.0.33', + glibcxxVersion: '3.4.33' + } + const artifact = selectSshRelayArtifact(verified, host) + if (artifact.kind !== 'selected') { + throw new Error(`Expected selected cache fixture, got ${artifact.reason}`) + } + const archivePath = join(root, tuple.archive.name) + await writeFile(archivePath, archive, { mode: 0o600 }) + return { archivePath, artifact, fileBytes } +} diff --git a/src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts b/src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts new file mode 100644 index 00000000000..1b90768526d --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts @@ -0,0 +1,141 @@ +import { readFile, rm, stat } from 'node:fs/promises' +import { performance } from 'node:perf_hooks' + +import { describe, expect, it } from 'vitest' + +import { + lookupSshRelayArtifactCacheEntry, + publishSshRelayArtifactCacheEntry, + SSH_RELAY_ARTIFACT_CACHE_ENTRY_LIMITS +} from './ssh-relay-artifact-cache-entry' +import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector' + +type MeasurementIdentity = { + tupleId: SshRelaySelectedArtifact['tupleId'] + contentId: SshRelaySelectedArtifact['contentId'] + os: SshRelaySelectedArtifact['tuple']['os'] + archive: SshRelaySelectedArtifact['tuple']['archive'] + entries: SshRelaySelectedArtifact['tuple']['entries'] +} + +const archivePath = process.env.ORCA_SSH_RELAY_FULL_SIZE_ARCHIVE +const identityPath = process.env.ORCA_SSH_RELAY_FULL_SIZE_IDENTITY +const cacheRoot = process.env.ORCA_SSH_RELAY_FULL_SIZE_OUTPUT +const hasMeasurementInput = Boolean(archivePath && identityPath && cacheRoot) + +function measurementIdentity(input: unknown): MeasurementIdentity { + if ( + typeof input !== 'object' || + input === null || + !('tupleId' in input) || + !('contentId' in input) || + !('os' in input) || + !('archive' in input) || + !('entries' in input) || + !Array.isArray(input.entries) + ) { + throw new Error('Full-size cache measurement identity is incomplete') + } + return input as MeasurementIdentity +} + +function measurementArtifact(identity: MeasurementIdentity): SshRelaySelectedArtifact { + const tuple = identity as unknown as SshRelaySelectedArtifact['tuple'] + // Why: this runner measures exact Actions artifact resources; it is never a product trust bypass. + return Object.freeze({ + kind: 'selected', + tupleId: identity.tupleId, + contentId: identity.contentId, + releaseTag: 'measurement-only', + archive: Object.freeze({ + ...identity.archive, + downloadUrl: 'https://invalid.example/measurement-only' + }), + tuple + }) +} + +async function measure(operation: () => Promise): Promise<{ + result: T + elapsedMs: number + baselineRss: number + peakRss: number + incrementalRssBytes: number +}> { + const baselineRss = process.memoryUsage().rss + let peakRss = baselineRss + const sample = (): void => { + peakRss = Math.max(peakRss, process.memoryUsage().rss) + } + const sampler = setInterval(sample, 1) + const startedAt = performance.now() + let result: T + try { + result = await operation() + } finally { + clearInterval(sampler) + sample() + } + return { + result, + elapsedMs: performance.now() - startedAt, + baselineRss, + peakRss, + incrementalRssBytes: Math.max(0, peakRss - baselineRss) + } +} + +describe.skipIf(!hasMeasurementInput)('SSH relay full-size immutable artifact cache entry', () => { + it( + 'keeps cold publication and verified lookup within desktop resource budgets', + async () => { + const identity = measurementIdentity( + JSON.parse(await readFile(identityPath as string, 'utf8')) as unknown + ) + const artifact = measurementArtifact(identity) + await expect(stat(cacheRoot as string)).rejects.toMatchObject({ code: 'ENOENT' }) + try { + const cold = await measure(() => + publishSshRelayArtifactCacheEntry({ + cacheRoot: cacheRoot as string, + artifact, + archivePath: archivePath as string + }) + ) + const warm = await measure(() => + lookupSshRelayArtifactCacheEntry({ cacheRoot: cacheRoot as string, artifact }) + ) + console.log( + `ssh_relay_full_size_cache=${JSON.stringify({ + tupleId: identity.tupleId, + archiveBytes: identity.archive.size, + expandedBytes: identity.archive.expandedSize, + files: identity.archive.fileCount, + coldElapsedMs: cold.elapsedMs, + coldIncrementalRssBytes: cold.incrementalRssBytes, + warmElapsedMs: warm.elapsedMs, + warmIncrementalRssBytes: warm.incrementalRssBytes + })}` + ) + expect(cold.result).toMatchObject({ + tupleId: identity.tupleId, + contentId: identity.contentId, + files: identity.archive.fileCount, + expandedBytes: identity.archive.expandedSize + }) + expect(warm.result).toEqual({ kind: 'hit', entry: cold.result }) + for (const measurement of [cold, warm]) { + expect(measurement.elapsedMs).toBeLessThanOrEqual( + SSH_RELAY_ARTIFACT_CACHE_ENTRY_LIMITS.transactionTimeoutMs + ) + expect(measurement.incrementalRssBytes).toBeLessThanOrEqual( + SSH_RELAY_ARTIFACT_CACHE_ENTRY_LIMITS.maximumIncrementalMemoryBytes + ) + } + } finally { + await rm(cacheRoot as string, { recursive: true, force: true }) + } + }, + SSH_RELAY_ARTIFACT_CACHE_ENTRY_LIMITS.transactionTimeoutMs * 2 + 10_000 + ) +}) diff --git a/src/main/ssh/ssh-relay-artifact-cache-entry-proof.ts b/src/main/ssh/ssh-relay-artifact-cache-entry-proof.ts new file mode 100644 index 00000000000..3451bb00496 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-entry-proof.ts @@ -0,0 +1,81 @@ +import { z } from 'zod' + +import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector' + +export const SSH_RELAY_ARTIFACT_CACHE_ENTRY_PROOF_NAME = 'proof.json' +export const SSH_RELAY_ARTIFACT_CACHE_ENTRY_PROOF_MAX_BYTES = 16 * 1024 + +const digestSchema = z.string().regex(/^sha256:[0-9a-f]{64}$/) +const safeSizeSchema = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER) +const proofSchema = z + .object({ + schemaVersion: z.literal(1), + tupleId: z.string().min(1).max(64), + contentId: digestSchema, + releaseTag: z.string().min(1).max(128), + archive: z + .object({ + name: z.string().regex(/^[A-Za-z0-9._-]+$/), + size: safeSizeSchema, + sha256: digestSchema + }) + .strict(), + runtime: z.object({ files: safeSizeSchema, expandedBytes: safeSizeSchema }).strict() + }) + .strict() + +export type SshRelayArtifactCacheEntryProof = z.infer + +export function createSshRelayArtifactCacheEntryProof( + artifact: SshRelaySelectedArtifact, + runtime: { files: number; expandedBytes: number } +): SshRelayArtifactCacheEntryProof { + return { + schemaVersion: 1, + tupleId: artifact.tupleId, + contentId: artifact.contentId, + releaseTag: artifact.releaseTag, + archive: { + name: artifact.archive.name, + size: artifact.archive.size, + sha256: artifact.archive.sha256 + }, + runtime: { files: runtime.files, expandedBytes: runtime.expandedBytes } + } +} + +export function sshRelayArtifactCacheEntryProofBytes( + proof: SshRelayArtifactCacheEntryProof +): Buffer { + return Buffer.from(`${JSON.stringify(proof)}\n`, 'utf8') +} + +export function parseSshRelayArtifactCacheEntryProof( + bytes: Buffer, + artifact: SshRelaySelectedArtifact +): SshRelayArtifactCacheEntryProof { + let input: unknown + try { + input = JSON.parse(bytes.toString('utf8')) + } catch (error) { + throw new Error('SSH relay artifact cache proof is not valid JSON', { cause: error }) + } + const proof = proofSchema.parse(input) + const expected = createSshRelayArtifactCacheEntryProof(artifact, { + files: artifact.archive.fileCount, + expandedBytes: artifact.archive.expandedSize + }) + if ( + proof.tupleId !== expected.tupleId || + proof.contentId !== expected.contentId || + proof.releaseTag !== expected.releaseTag || + proof.archive.name !== expected.archive.name || + proof.archive.size !== expected.archive.size || + proof.archive.sha256 !== expected.archive.sha256 || + proof.runtime.files !== expected.runtime.files || + proof.runtime.expandedBytes !== expected.runtime.expandedBytes + ) { + throw new Error('SSH relay artifact cache proof disagrees with the selected signed artifact') + } + return proof +} diff --git a/src/main/ssh/ssh-relay-artifact-cache-entry-verification.ts b/src/main/ssh/ssh-relay-artifact-cache-entry-verification.ts new file mode 100644 index 00000000000..ccf3b828181 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-entry-verification.ts @@ -0,0 +1,288 @@ +import { createHash } from 'node:crypto' +import { lstat, open, readdir, rm, type FileHandle } from 'node:fs/promises' +import { join } from 'node:path' + +import { + parseSshRelayArtifactCacheEntryProof, + SSH_RELAY_ARTIFACT_CACHE_ENTRY_PROOF_MAX_BYTES, + SSH_RELAY_ARTIFACT_CACHE_ENTRY_PROOF_NAME +} from './ssh-relay-artifact-cache-entry-proof' +import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector' +import { verifySshRelayArtifactTree } from './ssh-relay-artifact-tree-verification' + +const CHUNK_BYTES = 64 * 1024 + +type FileState = { + dev: bigint + ino: bigint + size: bigint + mtimeNs: bigint + ctimeNs: bigint +} + +function sameFileState(left: FileState, right: FileState): boolean { + return ( + left.dev === right.dev && + left.ino === right.ino && + left.size === right.size && + left.mtimeNs === right.mtimeNs && + left.ctimeNs === right.ctimeNs + ) +} + +async function writeComplete(handle: FileHandle, bytes: Buffer): Promise { + let offset = 0 + while (offset < bytes.length) { + const { bytesWritten } = await handle.write(bytes, offset, bytes.length - offset, null) + if (bytesWritten <= 0) { + throw new Error('SSH relay artifact cache archive copy could not be persisted') + } + offset += bytesWritten + } +} + +async function hashExactRegularFile({ + path, + expectedSize, + expectedSha256, + signal +}: { + path: string + expectedSize: number + expectedSha256: string + signal: AbortSignal +}): Promise { + signal.throwIfAborted() + const before = await lstat(path, { bigint: true }) + if (!before.isFile() || before.isSymbolicLink() || before.size !== BigInt(expectedSize)) { + throw new Error('SSH relay artifact cache archive is not the exact signed regular file') + } + const hash = createHash('sha256') + const handle = await open(path, 'r') + let size = 0 + try { + const opened = await handle.stat({ bigint: true }) + if (!opened.isFile() || !sameFileState(before, opened)) { + throw new Error('SSH relay artifact cache archive changed before hashing') + } + const buffer = Buffer.allocUnsafe(Math.min(CHUNK_BYTES, Math.max(expectedSize, 1))) + while (size < expectedSize) { + signal.throwIfAborted() + const { bytesRead } = await handle.read( + buffer, + 0, + Math.min(buffer.length, expectedSize - size), + size + ) + if (bytesRead === 0) { + break + } + size += bytesRead + hash.update(buffer.subarray(0, bytesRead)) + } + const after = await handle.stat({ bigint: true }) + if (!sameFileState(opened, after)) { + throw new Error('SSH relay artifact cache archive changed while hashing') + } + } finally { + await handle.close() + } + const after = await lstat(path, { bigint: true }) + if (!sameFileState(before, after) || size !== expectedSize) { + throw new Error('SSH relay artifact cache archive changed while hashing') + } + const sha256 = `sha256:${hash.digest('hex')}` + if (sha256 !== expectedSha256) { + throw new Error('SSH relay artifact cache archive SHA-256 disagrees with the signed manifest') + } +} + +export async function copyVerifiedSshRelayArtifactCacheArchive({ + sourcePath, + destinationPath, + artifact, + signal +}: { + sourcePath: string + destinationPath: string + artifact: SshRelaySelectedArtifact + signal: AbortSignal +}): Promise { + signal.throwIfAborted() + const before = await lstat(sourcePath, { bigint: true }) + if ( + !before.isFile() || + before.isSymbolicLink() || + before.size !== BigInt(artifact.archive.size) + ) { + throw new Error('SSH relay artifact cache source must be the exact signed regular archive') + } + const source = await open(sourcePath, 'r') + let destination: FileHandle + try { + destination = await open(destinationPath, 'wx', 0o600) + } catch (error) { + await source.close().catch(() => {}) + throw error + } + const hash = createHash('sha256') + let size = 0 + let complete = false + try { + const opened = await source.stat({ bigint: true }) + if (!opened.isFile() || !sameFileState(before, opened)) { + throw new Error('SSH relay artifact cache source changed before copying') + } + const buffer = Buffer.allocUnsafe(Math.min(CHUNK_BYTES, Math.max(artifact.archive.size, 1))) + while (size < artifact.archive.size) { + signal.throwIfAborted() + const { bytesRead } = await source.read( + buffer, + 0, + Math.min(buffer.length, artifact.archive.size - size), + size + ) + if (bytesRead === 0) { + break + } + const bytes = buffer.subarray(0, bytesRead) + hash.update(bytes) + await writeComplete(destination, bytes) + size += bytesRead + } + const readComplete = await source.stat({ bigint: true }) + if (!sameFileState(opened, readComplete)) { + throw new Error('SSH relay artifact cache source changed while copying') + } + await destination.sync() + const written = await destination.stat({ bigint: true }) + if (!written.isFile() || written.size !== BigInt(size)) { + throw new Error('SSH relay artifact cache archive copy is incomplete') + } + const sha256 = `sha256:${hash.digest('hex')}` + if (size !== artifact.archive.size || sha256 !== artifact.archive.sha256) { + throw new Error('SSH relay artifact cache source archive disagrees with the signed manifest') + } + signal.throwIfAborted() + complete = true + } finally { + await Promise.all([source.close().catch(() => {}), destination.close().catch(() => {})]) + if (!complete) { + await rm(destinationPath, { force: true }).catch(() => {}) + } + } + const after = await lstat(sourcePath, { bigint: true }) + if (!sameFileState(before, after)) { + await rm(destinationPath, { force: true }).catch(() => {}) + throw new Error('SSH relay artifact cache source changed while copying') + } +} + +async function readExactProof(path: string): Promise { + const before = await lstat(path, { bigint: true }) + if ( + !before.isFile() || + before.isSymbolicLink() || + before.size > BigInt(SSH_RELAY_ARTIFACT_CACHE_ENTRY_PROOF_MAX_BYTES) + ) { + throw new Error('SSH relay artifact cache proof is not a bounded regular file') + } + const handle = await open(path, 'r') + try { + const opened = await handle.stat({ bigint: true }) + if (!sameFileState(before, opened)) { + throw new Error('SSH relay artifact cache proof changed before reading') + } + const bytes = Buffer.alloc(Number(opened.size)) + let offset = 0 + while (offset < bytes.length) { + const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, offset) + if (bytesRead === 0) { + break + } + offset += bytesRead + } + const after = await handle.stat({ bigint: true }) + if (offset !== bytes.length || !sameFileState(opened, after)) { + throw new Error('SSH relay artifact cache proof changed while reading') + } + return bytes + } finally { + await handle.close() + } +} + +export type SshRelayArtifactCacheEntry = { + contentId: SshRelaySelectedArtifact['contentId'] + tupleId: SshRelaySelectedArtifact['tupleId'] + entryPath: string + archivePath: string + runtimeRoot: string + proofPath: string + files: number + expandedBytes: number +} + +export async function verifySshRelayArtifactCacheEntry({ + entryPath, + artifact, + signal +}: { + entryPath: string + artifact: SshRelaySelectedArtifact + signal: AbortSignal +}): Promise { + signal.throwIfAborted() + const root = await lstat(entryPath) + if (!root.isDirectory() || root.isSymbolicLink()) { + throw new Error('SSH relay artifact cache entry is not an immutable directory') + } + const expectedMembers = [ + artifact.archive.name, + SSH_RELAY_ARTIFACT_CACHE_ENTRY_PROOF_NAME, + 'runtime' + ].sort() + const members = (await readdir(entryPath)).sort() + if (JSON.stringify(members) !== JSON.stringify(expectedMembers)) { + throw new Error('SSH relay artifact cache entry has missing or unexpected members') + } + + const archivePath = join(entryPath, artifact.archive.name) + const proofPath = join(entryPath, SSH_RELAY_ARTIFACT_CACHE_ENTRY_PROOF_NAME) + const runtimeRoot = join(entryPath, 'runtime') + const proof = parseSshRelayArtifactCacheEntryProof(await readExactProof(proofPath), artifact) + await hashExactRegularFile({ + path: archivePath, + expectedSize: artifact.archive.size, + expectedSha256: artifact.archive.sha256, + signal + }) + const runtime = await lstat(runtimeRoot) + if (!runtime.isDirectory() || runtime.isSymbolicLink()) { + throw new Error('SSH relay artifact cache runtime is not an immutable directory') + } + const tree = await verifySshRelayArtifactTree({ + runtimeRoot, + tuple: artifact.tuple, + signal, + chunkBytes: CHUNK_BYTES + }) + if (tree.files !== proof.runtime.files || tree.expandedBytes !== proof.runtime.expandedBytes) { + throw new Error('SSH relay artifact cache proof disagrees with the complete runtime tree') + } + signal.throwIfAborted() + return { + contentId: artifact.contentId, + tupleId: artifact.tupleId, + entryPath, + archivePath, + runtimeRoot, + proofPath, + ...tree + } +} + +export const SSH_RELAY_ARTIFACT_CACHE_ENTRY_VERIFICATION_LIMITS = Object.freeze({ + chunkBytes: CHUNK_BYTES, + proofMaxBytes: SSH_RELAY_ARTIFACT_CACHE_ENTRY_PROOF_MAX_BYTES +}) diff --git a/src/main/ssh/ssh-relay-artifact-cache-entry.test.ts b/src/main/ssh/ssh-relay-artifact-cache-entry.test.ts new file mode 100644 index 00000000000..3f935a29d35 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-entry.test.ts @@ -0,0 +1,233 @@ +import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { + lookupSshRelayArtifactCacheEntry, + publishSshRelayArtifactCacheEntry, + SshRelayArtifactCacheIntegrityError, + sshRelayArtifactCacheEntryPath, + SSH_RELAY_ARTIFACT_CACHE_ENTRY_LIMITS +} from './ssh-relay-artifact-cache-entry' +import { createSshRelayArtifactCacheEntryFixture } from './ssh-relay-artifact-cache-entry-fixture' +import { SSH_RELAY_ARTIFACT_CACHE_ENTRY_VERIFICATION_LIMITS } from './ssh-relay-artifact-cache-entry-verification' +import type { SshRelayDigest } from './ssh-relay-runtime-identity' + +const temporaryDirectories: string[] = [] + +async function testRoot(): Promise { + const root = await mkdtemp(join(tmpdir(), 'orca-relay-cache-entry-')) + temporaryDirectories.push(root) + return root +} + +async function entryFixture(os: 'linux' | 'win32' = 'linux') { + const root = await testRoot() + return { + root, + cacheRoot: join(root, 'cache'), + ...(await createSshRelayArtifactCacheEntryFixture({ root, os })) + } +} + +async function entryChildren(cacheRoot: string): Promise { + return readdir(join(cacheRoot, 'entries')).catch((error: NodeJS.ErrnoException) => { + if (error.code === 'ENOENT') { + return [] + } + throw error + }) +} + +async function expectIntegrityQuarantine(options: { + cacheRoot: string + artifact: Awaited>['artifact'] +}): Promise { + const error = await lookupSshRelayArtifactCacheEntry(options).catch((reason: unknown) => reason) + expect(error).toBeInstanceOf(SshRelayArtifactCacheIntegrityError) + const integrity = error as SshRelayArtifactCacheIntegrityError + expect(integrity.quarantinePath).toBeTruthy() + await expect( + stat(sshRelayArtifactCacheEntryPath(options.cacheRoot, options.artifact.contentId)) + ).rejects.toMatchObject({ code: 'ENOENT' }) + await expect(stat(integrity.quarantinePath!)).resolves.toMatchObject({ + isDirectory: expect.any(Function) + }) + return integrity +} + +afterEach(async () => { + await Promise.all( + temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true })) + ) +}) + +describe('SSH relay immutable artifact cache entry', () => { + it('pins bounded transaction, copy, hash, and proof limits', () => { + expect(SSH_RELAY_ARTIFACT_CACHE_ENTRY_LIMITS).toEqual({ + transactionTimeoutMs: 5 * 60_000, + maximumIncrementalMemoryBytes: 80 * 1024 * 1024 + }) + expect(SSH_RELAY_ARTIFACT_CACHE_ENTRY_VERIFICATION_LIMITS).toEqual({ + chunkBytes: 64 * 1024, + proofMaxBytes: 16 * 1024 + }) + }) + + it('derives the final entry only from an exact lowercase content digest', async () => { + const root = await testRoot() + const contentId = `sha256:${'a'.repeat(64)}` as SshRelayDigest + expect(sshRelayArtifactCacheEntryPath(root, contentId)).toBe( + join(root, 'entries', 'a'.repeat(64)) + ) + for (const invalid of [ + `sha256:${'A'.repeat(64)}`, + `sha256:${'a'.repeat(63)}`, + `sha256:${'a'.repeat(64)}/../escape`, + 'a'.repeat(64) + ]) { + expect(() => sshRelayArtifactCacheEntryPath(root, invalid as SshRelayDigest)).toThrow( + /content id/i + ) + } + }) + + it.each(['linux', 'win32'] as const)( + 'publishes and revalidates one complete immutable %s entry', + async (os) => { + const value = await entryFixture(os) + const published = await publishSshRelayArtifactCacheEntry(value) + + expect(published).toMatchObject({ + contentId: value.artifact.contentId, + tupleId: value.artifact.tupleId, + files: value.artifact.archive.fileCount, + expandedBytes: value.artifact.archive.expandedSize + }) + expect((await readdir(published.entryPath)).sort()).toEqual( + [value.artifact.archive.name, 'proof.json', 'runtime'].sort() + ) + for (const [path, bytes] of value.fileBytes) { + expect(await readFile(join(published.runtimeRoot, ...path.split('/')))).toEqual(bytes) + } + const lookup = await lookupSshRelayArtifactCacheEntry(value) + expect(lookup).toEqual({ kind: 'hit', entry: published }) + expect(await entryChildren(value.cacheRoot)).toEqual([value.artifact.contentId.slice(7)]) + } + ) + + it('returns an exact miss without creating or quarantining state', async () => { + const value = await entryFixture() + await expect(lookupSshRelayArtifactCacheEntry(value)).resolves.toEqual({ kind: 'miss' }) + expect(await entryChildren(value.cacheRoot)).toEqual([]) + }) + + it('never exposes a final entry when archive verification or cancellation fails', async () => { + const corrupt = await entryFixture() + const bytes = await readFile(corrupt.archivePath) + bytes[0] ^= 0x01 + await writeFile(corrupt.archivePath, bytes) + await expect(publishSshRelayArtifactCacheEntry(corrupt)).rejects.toThrow(/archive|sha|hash/i) + expect(await entryChildren(corrupt.cacheRoot)).toEqual([]) + + const cancelled = await entryFixture() + const controller = new AbortController() + controller.abort(new Error('cancel cache publication')) + await expect( + publishSshRelayArtifactCacheEntry({ ...cancelled, signal: controller.signal }) + ).rejects.toThrow(/cancel cache publication/i) + expect(await entryChildren(cancelled.cacheRoot)).toEqual([]) + }) + + it('serializes concurrent publishers and reuses a valid immutable entry', async () => { + const value = await entryFixture() + const [first, second] = await Promise.all([ + publishSshRelayArtifactCacheEntry(value), + publishSshRelayArtifactCacheEntry(value) + ]) + expect(second).toEqual(first) + const before = await stat(first.proofPath) + await expect(publishSshRelayArtifactCacheEntry(value)).resolves.toEqual(first) + expect((await stat(first.proofPath)).mtimeMs).toBe(before.mtimeMs) + expect(await entryChildren(value.cacheRoot)).toEqual([value.artifact.contentId.slice(7)]) + }) + + it('cleans only same-content stale staging after acquiring ownership', async () => { + const value = await entryFixture() + const finalPath = sshRelayArtifactCacheEntryPath(value.cacheRoot, value.artifact.contentId) + const stalePath = `${finalPath}.pending-${'f'.repeat(32)}` + await mkdir(stalePath, { recursive: true }) + await writeFile(join(stalePath, 'partial'), 'not selectable') + + await publishSshRelayArtifactCacheEntry(value) + + await expect(stat(stalePath)).rejects.toMatchObject({ code: 'ENOENT' }) + expect(await entryChildren(value.cacheRoot)).toEqual([value.artifact.contentId.slice(7)]) + }) + + it.each([ + ['proof', async (entryPath: string) => writeFile(join(entryPath, 'proof.json'), '{}')], + [ + 'proof identity', + async (entryPath: string) => { + const proofPath = join(entryPath, 'proof.json') + const proof = JSON.parse(await readFile(proofPath, 'utf8')) as Record + await writeFile(proofPath, `${JSON.stringify({ ...proof, releaseTag: 'v0.0.0' })}\n`) + } + ], + [ + 'proof unknown field', + async (entryPath: string) => { + const proofPath = join(entryPath, 'proof.json') + const proof = JSON.parse(await readFile(proofPath, 'utf8')) as Record + await writeFile(proofPath, `${JSON.stringify({ ...proof, latest: true })}\n`) + } + ], + [ + 'archive', + async (entryPath: string, archiveName: string) => + writeFile(join(entryPath, archiveName), 'changed archive bytes') + ], + [ + 'runtime file', + async (entryPath: string) => writeFile(join(entryPath, 'runtime', 'relay.js'), 'changed') + ], + [ + 'unexpected member', + async (entryPath: string) => writeFile(join(entryPath, 'unexpected'), 'partial state') + ], + ['missing member', async (entryPath: string) => rm(join(entryPath, 'runtime', 'relay.js'))] + ] as const)('quarantines %s corruption instead of returning a miss', async (_name, mutate) => { + const value = await entryFixture() + const entry = await publishSshRelayArtifactCacheEntry(value) + await mutate(entry.entryPath, value.artifact.archive.name) + + await expectIntegrityQuarantine(value) + }) + + it('quarantines a partial final entry', async () => { + const value = await entryFixture() + const entryPath = sshRelayArtifactCacheEntryPath(value.cacheRoot, value.artifact.contentId) + await mkdir(entryPath, { recursive: true }) + await writeFile(join(entryPath, 'partial'), 'crash residue') + + await expectIntegrityQuarantine(value) + }) + + it('recovers corrupt existing state only from freshly reverified bytes', async () => { + const value = await entryFixture() + const first = await publishSshRelayArtifactCacheEntry(value) + await writeFile(join(first.runtimeRoot, 'relay.js'), 'changed') + + const replacement = await publishSshRelayArtifactCacheEntry(value) + + expect(replacement.entryPath).toBe(first.entryPath) + await expect(lookupSshRelayArtifactCacheEntry(value)).resolves.toEqual({ + kind: 'hit', + entry: replacement + }) + expect(await readdir(join(value.cacheRoot, 'quarantine'))).toHaveLength(1) + }) +}) diff --git a/src/main/ssh/ssh-relay-artifact-cache-entry.ts b/src/main/ssh/ssh-relay-artifact-cache-entry.ts new file mode 100644 index 00000000000..10fedba7cf5 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-entry.ts @@ -0,0 +1,318 @@ +import { randomBytes } from 'node:crypto' +import { lstat, mkdir, open, readdir, realpath, rename, rm } from 'node:fs/promises' +import { basename, dirname, join, resolve } from 'node:path' + +import { + createSshRelayArtifactCacheEntryProof, + sshRelayArtifactCacheEntryProofBytes, + SSH_RELAY_ARTIFACT_CACHE_ENTRY_PROOF_NAME +} from './ssh-relay-artifact-cache-entry-proof' +import { + copyVerifiedSshRelayArtifactCacheArchive, + verifySshRelayArtifactCacheEntry, + type SshRelayArtifactCacheEntry +} from './ssh-relay-artifact-cache-entry-verification' +import { acquireSshRelayArtifactCacheLock } from './ssh-relay-artifact-cache-lock' +import { sshRelayArtifactCacheErrorCode } from './ssh-relay-artifact-cache-lock-record' +import { extractSshRelayArtifact } from './ssh-relay-artifact-extraction' +import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector' +import type { SshRelayDigest } from './ssh-relay-runtime-identity' + +const TRANSACTION_TIMEOUT_MS = 5 * 60_000 +const MAXIMUM_INCREMENTAL_MEMORY_BYTES = 80 * 1024 * 1024 +const CONTENT_ID = /^sha256:([0-9a-f]{64})$/ + +type CacheDirectories = { + root: string + entries: string + quarantine: string +} + +function exactContentHex(contentId: SshRelayDigest): string { + const match = CONTENT_ID.exec(contentId) + if (!match) { + throw new Error('SSH relay artifact cache content ID must be an exact lowercase SHA-256 digest') + } + return match[1] +} + +export function sshRelayArtifactCacheEntryPath( + cacheRoot: string, + contentId: SshRelayDigest +): string { + return resolve(cacheRoot, 'entries', exactContentHex(contentId)) +} + +async function ensureOwnedDirectory(parent: string, name: string): Promise { + const logical = join(parent, name) + await mkdir(logical, { recursive: true, mode: 0o700 }) + const metadata = await lstat(logical) + if (!metadata.isDirectory() || metadata.isSymbolicLink()) { + throw new Error(`SSH relay artifact cache ${name} path must be an owned directory`) + } + const physical = await realpath(logical) + if (dirname(physical) !== parent || basename(physical) !== name) { + throw new Error(`SSH relay artifact cache ${name} path must not traverse a link`) + } + return physical +} + +async function prepareCacheDirectories(cacheRoot: string): Promise { + const logicalRoot = resolve(cacheRoot) + await mkdir(logicalRoot, { recursive: true, mode: 0o700 }) + const rootMetadata = await lstat(logicalRoot) + if (!rootMetadata.isDirectory() || rootMetadata.isSymbolicLink()) { + throw new Error('SSH relay artifact cache root must be an owned directory') + } + const root = await realpath(logicalRoot) + const entries = await ensureOwnedDirectory(root, 'entries') + const quarantine = await ensureOwnedDirectory(root, 'quarantine') + return { root, entries, quarantine } +} + +function effectiveSignal(signal?: AbortSignal): AbortSignal { + const timeout = AbortSignal.timeout(TRANSACTION_TIMEOUT_MS) + return signal ? AbortSignal.any([signal, timeout]) : timeout +} + +async function pathExists(path: string): Promise { + try { + await lstat(path) + return true + } catch (error) { + if (sshRelayArtifactCacheErrorCode(error) === 'ENOENT') { + return false + } + throw error + } +} + +async function cleanStaleStaging( + entries: string, + contentHex: string, + signal: AbortSignal +): Promise { + const pattern = new RegExp(`^${contentHex}\\.pending-[0-9a-f]{32}$`) + for (const name of await readdir(entries)) { + signal.throwIfAborted() + if (pattern.test(name)) { + // Why: the same-content lock proves these token-shaped remnants have no active publisher. + await rm(join(entries, name), { recursive: true, force: true }) + } + } +} + +export class SshRelayArtifactCacheIntegrityError extends Error { + readonly quarantinePath: string | null + + constructor(message: string, quarantinePath: string | null, cause: unknown) { + super(message, { cause }) + this.name = 'SshRelayArtifactCacheIntegrityError' + this.quarantinePath = quarantinePath + } +} + +async function quarantineCorruptEntry({ + entryPath, + quarantineDirectory, + contentHex, + cause +}: { + entryPath: string + quarantineDirectory: string + contentHex: string + cause: unknown +}): Promise { + const quarantinePath = join( + quarantineDirectory, + `${contentHex}.corrupt-${randomBytes(16).toString('hex')}` + ) + try { + // Why: detected corruption becomes unselectable before diagnostics or later recovery can proceed. + await rename(entryPath, quarantinePath) + } catch (quarantineError) { + throw new SshRelayArtifactCacheIntegrityError( + 'SSH relay artifact cache entry is corrupt and could not be quarantined', + null, + new AggregateError([cause, quarantineError], 'Cache verification and quarantine both failed') + ) + } + throw new SshRelayArtifactCacheIntegrityError( + 'SSH relay artifact cache entry failed integrity verification and was quarantined', + quarantinePath, + cause + ) +} + +async function verifyExistingOrQuarantine({ + entryPath, + artifact, + signal, + directories +}: { + entryPath: string + artifact: SshRelaySelectedArtifact + signal: AbortSignal + directories: CacheDirectories +}): Promise { + try { + return await verifySshRelayArtifactCacheEntry({ entryPath, artifact, signal }) + } catch (error) { + if (signal.aborted) { + signal.throwIfAborted() + } + return quarantineCorruptEntry({ + entryPath, + quarantineDirectory: directories.quarantine, + contentHex: exactContentHex(artifact.contentId), + cause: error + }) + } +} + +async function writeProof( + path: string, + artifact: SshRelaySelectedArtifact, + runtime: { files: number; expandedBytes: number } +): Promise { + const handle = await open(path, 'wx', 0o600) + try { + await handle.writeFile( + sshRelayArtifactCacheEntryProofBytes(createSshRelayArtifactCacheEntryProof(artifact, runtime)) + ) + await handle.sync() + } finally { + await handle.close() + } +} + +export type SshRelayArtifactCacheLookup = + | { kind: 'miss' } + | { kind: 'hit'; entry: SshRelayArtifactCacheEntry } + +export async function lookupSshRelayArtifactCacheEntry({ + cacheRoot, + artifact, + signal +}: { + cacheRoot: string + artifact: SshRelaySelectedArtifact + signal?: AbortSignal +}): Promise { + const activeSignal = effectiveSignal(signal) + activeSignal.throwIfAborted() + const directories = await prepareCacheDirectories(cacheRoot) + const contentHex = exactContentHex(artifact.contentId) + const entryPath = join(directories.entries, contentHex) + const lock = await acquireSshRelayArtifactCacheLock({ + cacheRoot: directories.root, + contentId: artifact.contentId, + signal: activeSignal + }) + try { + await cleanStaleStaging(directories.entries, contentHex, activeSignal) + if (!(await pathExists(entryPath))) { + return { kind: 'miss' } + } + return { + kind: 'hit', + entry: await verifyExistingOrQuarantine({ + entryPath, + artifact, + signal: activeSignal, + directories + }) + } + } finally { + await lock.release() + } +} + +export async function publishSshRelayArtifactCacheEntry({ + cacheRoot, + artifact, + archivePath, + signal +}: { + cacheRoot: string + artifact: SshRelaySelectedArtifact + archivePath: string + signal?: AbortSignal +}): Promise { + const activeSignal = effectiveSignal(signal) + activeSignal.throwIfAborted() + const directories = await prepareCacheDirectories(cacheRoot) + const contentHex = exactContentHex(artifact.contentId) + const entryPath = join(directories.entries, contentHex) + const lock = await acquireSshRelayArtifactCacheLock({ + cacheRoot: directories.root, + contentId: artifact.contentId, + signal: activeSignal + }) + let stagingPath: string | null = null + try { + await cleanStaleStaging(directories.entries, contentHex, activeSignal) + if (await pathExists(entryPath)) { + try { + return await verifyExistingOrQuarantine({ + entryPath, + artifact, + signal: activeSignal, + directories + }) + } catch (error) { + if (!(error instanceof SshRelayArtifactCacheIntegrityError) || !error.quarantinePath) { + throw error + } + // A quarantined entry may be replaced only by the fresh verified source below. + } + } + + stagingPath = `${entryPath}.pending-${lock.token}` + await mkdir(stagingPath, { mode: 0o700 }) + const stagedArchive = join(stagingPath, artifact.archive.name) + await copyVerifiedSshRelayArtifactCacheArchive({ + sourcePath: resolve(archivePath), + destinationPath: stagedArchive, + artifact, + signal: activeSignal + }) + const extraction = await extractSshRelayArtifact({ + artifact, + archivePath: stagedArchive, + outputDirectory: join(stagingPath, 'runtime'), + signal: activeSignal + }) + await writeProof( + join(stagingPath, SSH_RELAY_ARTIFACT_CACHE_ENTRY_PROOF_NAME), + artifact, + extraction + ) + const verified = await verifySshRelayArtifactCacheEntry({ + entryPath: stagingPath, + artifact, + signal: activeSignal + }) + await lock.assertOwned() + activeSignal.throwIfAborted() + await rename(stagingPath, entryPath) + stagingPath = null + return { + ...verified, + entryPath, + archivePath: join(entryPath, artifact.archive.name), + runtimeRoot: join(entryPath, 'runtime'), + proofPath: join(entryPath, SSH_RELAY_ARTIFACT_CACHE_ENTRY_PROOF_NAME) + } + } finally { + if (stagingPath) { + await rm(stagingPath, { recursive: true, force: true }).catch(() => {}) + } + await lock.release() + } +} + +export const SSH_RELAY_ARTIFACT_CACHE_ENTRY_LIMITS = Object.freeze({ + transactionTimeoutMs: TRANSACTION_TIMEOUT_MS, + maximumIncrementalMemoryBytes: MAXIMUM_INCREMENTAL_MEMORY_BYTES +}) From 0ae3e005f803aa1956e4b7832d8be703f69b4edb Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 21:05:41 -0400 Subject: [PATCH 174/267] docs: record native relay cache entry evidence Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 75 ++------ ...github-release-implementation-checklist.md | 162 +++++++++++++----- 2 files changed, 132 insertions(+), 105 deletions(-) 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 e0689062502..8940f7da7fe 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 @@ -9,58 +9,17 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — disconnected -cross-process content-lock all-six native gate, 2026-07-15, Codex implementation owner.** Exact-head run -[29449869519](https://github.com/stablyai/orca/actions/runs/29449869519) closes deterministic bounded -`.tar.br` under `E-M5-PORTABLE-ARCHIVE-CI-001`: all six primary Node 24 jobs and both Linux -oldest-userland supplements pass. PR Checks and Golden E2E are green; Windows x64 floor passes and -Windows arm64 passes smoke before the retained hosted build-26200 versus required-26100 rejection. -The isolated desktop extractor is locally green under `E-M5-ARTIFACT-EXTRACTION-LOCAL-001` and -`E-M5-ARTIFACT-EXTRACTION-BROAD-LOCAL-001`: 17 focused tests cover both archive families, hostile -inputs, exact tree/modes, mutation, cancellation, cleanup, and ownership; broad relay/release/static -gates pass. An exact downloaded 122,027,869-byte Darwin arm64 tree extracts in 878.73ms with -49,283,072-byte incremental RSS under `E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001`. Both native job -families now require synthetic cross-family and post-build full-size gates under -`E-M5-ARTIFACT-EXTRACTION-CI-WIRING-LOCAL-001`. Exact-head Darwin arm64 then supplied the required -native RED: Darwin arm64 uses 71,368,704 bytes and Linux x64 uses 68,505,600 bytes, disproving the -pre-implementation 64 MiB assumption and blocking upload under -`E-M5-ARTIFACT-EXTRACTION-CI-MEMORY-RED-001`. Evidence decision -`E-M5-ARTIFACT-EXTRACTION-BUDGET-CORRECTION-001` selects 80 MiB for cold desktop extraction only; -remote remains 32 MiB and no buffer exceeds 1 MiB. The strict bounded replacement is locally green -under `E-M5-ARTIFACT-EXTRACTION-TAR-MEMORY-LOCAL-001`: 21 focused tests and five exact payload runs -use at most 69,402,624 bytes. Replacement exact-head run -[29455294208](https://github.com/stablyai/orca/actions/runs/29455294208) closes all six native -extraction/full-tree cells under `E-M5-ARTIFACT-EXTRACTION-CI-001`: 33,517,568–45,989,888-byte -incremental RSS and 1,294.77–5,757.55ms. Both Linux oldest-userland supplements and Windows x64 -floor pass; Windows arm64 runtime/extraction passes before the retained hosted build-26200 versus -required-26100 floor rejection. Exact-head PR Checks -[29455294230](https://github.com/stablyai/orca/actions/runs/29455294230) and Golden E2E -[29455294207](https://github.com/stablyai/orca/actions/runs/29455294207) are green. The cache audit -splits lock, immutable publication/quarantine, and in-use eviction into separate gates under -`E-M5-ARTIFACT-CACHE-AUDIT-001`. The absent-module RED is -`E-M5-ARTIFACT-CACHE-LOCK-LOCAL-RED-001`; the disconnected implementation and ten focused contracts -are locally green under `E-M5-ARTIFACT-CACHE-LOCK-LOCAL-001`, including a real second process, -heartbeat, cancellation, stale-dead reclaim, and live/ambiguous/displaced-owner safety. Artifact, -relay, release, typecheck, full lint, max-lines, and workflow gates pass. Both native job families -require the suite under `E-M5-ARTIFACT-CACHE-LOCK-CI-WIRING-LOCAL-001`. Exact-head Windows x64 then -found the open-heartbeat-handle directory-rename incompatibility under -`E-M5-ARTIFACT-CACHE-LOCK-CI-WINDOWS-RED-001`; Windows arm64 also found equivalent zlib truncation -wording outside the extraction test oracle under `E-M5-ARTIFACT-EXTRACTION-CI-WINDOWS-ARM64-RED-001`. -The ownership-preserving release order and equivalent-error assertion are locally green under -`E-M5-WINDOWS-NATIVE-GATE-CORRECTIONS-LOCAL-001`. Replacement exact-head run -[29459424864](https://github.com/stablyai/orca/actions/runs/29459424864) closes the content lock on all -six primary native Node 24 jobs under `E-M5-ARTIFACT-CACHE-LOCK-CI-001`; both Linux supplements, -PR Checks [29459424977](https://github.com/stablyai/orca/actions/runs/29459424977), and Golden E2E -[29459424789](https://github.com/stablyai/orca/actions/runs/29459424789) pass. Windows arm64 retains -only the explicit hosted build-26200 versus required-26100 floor rejection after runtime/tree smoke. -`E-M5-ARTIFACT-CACHE-ENTRY-AUDIT-001` fixes the disconnected immutable -publication/lookup/quarantine transaction; the absent-module contract RED is -`E-M5-ARTIFACT-CACHE-ENTRY-LOCAL-RED-001`. Seventeen local contracts plus broad/static gates are -green under `E-M5-ARTIFACT-CACHE-ENTRY-LOCAL-001`; both native families require the source suite and -exact full-size cold/warm measurement under `E-M5-ARTIFACT-CACHE-ENTRY-CI-WIRING-LOCAL-001`. Native -execution is next. Desktop consumers, cache eviction, SSH transfer/install, -mode wiring, tuple enablement, publication, production keys/environment/seed, and merge to `main` -remain disconnected. +Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — immutable cache entry +closed; in-use lease/2 GiB eviction is next, 2026-07-15, Codex implementation owner.** Exact-head +run [29462394311](https://github.com/stablyai/orca/actions/runs/29462394311) passes all 17 cache-entry +contracts and exact full-size cold/warm measurements on Linux, macOS, and Windows, x64 and arm64, +under `E-M5-ARTIFACT-CACHE-ENTRY-CI-001`. Cold publication is 1.17–6.17s and 34.5–48.8 MiB +incremental RSS; warm verified lookup is 0.12–0.70s and 0–6.9 MiB. PR Checks +[29462394310](https://github.com/stablyai/orca/actions/runs/29462394310) and Golden E2E +[29462394344](https://github.com/stablyai/orca/actions/runs/29462394344) pass. Windows arm64 retains +only the declared hosted build-26200 versus required-26100 floor gap after its primary runtime/cache +proof. Desktop consumers, eviction, SSH transfer/install, mode wiring, tuple enablement, publication, +production keys/environment/seed, and merge to `main` remain disconnected. ## Safety status @@ -356,14 +315,10 @@ remain disconnected. execution is closed under `E-M5-ARTIFACT-EXTRACTION-CI-001`. Packaged signature loading remains open. - [ ] Add exclusive staging, atomic publication, quarantine, locking, and the 2 GiB cache policy. - Exclusive owned staging and failure/cancellation cleanup are green locally and on all six - native clients under `E-M5-ARTIFACT-EXTRACTION-LOCAL-001` and - `E-M5-ARTIFACT-EXTRACTION-CI-001`; publication, quarantine, locking, and eviction are the - active split packages and are not implemented yet. The lock-only audit/RED is recorded under - `E-M5-ARTIFACT-CACHE-AUDIT-001` and `E-M5-ARTIFACT-CACHE-LOCK-LOCAL-RED-001`; the implementation - is locally green and wired to both native families under `E-M5-ARTIFACT-CACHE-LOCK-LOCAL-001` - and `E-M5-ARTIFACT-CACHE-LOCK-CI-WIRING-LOCAL-001`; all-six native execution is closed by - `E-M5-ARTIFACT-CACHE-LOCK-CI-001`. Immutable publication/lookup/quarantine remains next. + Locking and immutable publication/lookup/quarantine are closed locally and on all six native + clients under `E-M5-ARTIFACT-CACHE-LOCK-CI-001` and + `E-M5-ARTIFACT-CACHE-ENTRY-CI-001`. In-use leases, recency, and bounded 2 GiB eviction remain + the only open part of this combined item. - [ ] Prove verified cached bytes can be transferred while the client is offline. - [ ] Preserve `ORCA_RELAY_PATH` behind the official-build trust boundary. 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 e05839096b4..6aa5897f6e1 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    -Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: immutable cache-entry native/full-size gate**. E-M5-ARTIFACT-CACHE-ENTRY-LOCAL-001 closes 17 local transaction contracts plus broad/static gates; E-M5-ARTIFACT-CACHE-ENTRY-CI-WIRING-LOCAL-001 requires both the source suite and exact full-size cold-publication/verified-lookup measurement in both native job families. Checkpoint/push, then require all six Node 24 jobs and full-size time/RSS cells before cache-entry closure or any eviction/consumer work.
    +Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **Checkpoint — 2026-07-15, Codex implementation owner: immutable cache-entry gate closed; evidence checkpoint/push required before in-use lease/eviction work**. E-M5-ARTIFACT-CACHE-ENTRY-CI-001 closes all six primary Node 24 source and exact full-size cold-publication/warm-verified-lookup cells at `3c1ead8fb1e1f3b64abaf78705abb0c801a1d64b`. PR Checks and Golden E2E pass. No cache consumer, eviction, SSH path, mode, tuple, release write, or default behavior is connected.
    Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — resumed with user authority for end-to-end PR-contained implementation, commits, pushes, CI, rehearsals, and release writes, with merging to `main` explicitly prohibited. The first native run exposed and recorded Windows open-handle rename and equivalent zlib-error-oracle REDs; narrow corrections passed locally and at exact-head CI. In replacement run `29459424864`, all six primary jobs pass the 10 lock and 21 extraction contracts plus deterministic artifact build/smoke. The workflow's sole failure is the retained Windows arm64 hosted build-26200 versus required-26100 floor rejection after runtime/archive/tree/Node/PTTY/watcher proof; no tuple is enabled from it. PR Checks `29459424977` and Golden E2E `29459424789` pass. There is no release publication, cache entry, cache consumer, SSH transfer/install, tuple enablement, or production/default behavior change. 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) @@ -1077,7 +1077,8 @@ Suggested concrete modules: ### Download and local cache -- [ ] Store archives and extracted trees under an app-owned cache, keyed by signed content identity. +- [x] Store archives and extracted trees under an app-owned cache, keyed by signed content identity. + E-M5-ARTIFACT-CACHE-ENTRY-LOCAL-001 and E-M5-ARTIFACT-CACHE-ENTRY-CI-001. - [x] Stream downloads into exclusive temporary files; never buffer a full runtime in memory. E-M5-ARTIFACT-DOWNLOAD-LOCAL-001 and E-M5-ARTIFACT-DOWNLOAD-CI-001. - [x] Apply download size and time budgets while streaming. @@ -1088,10 +1089,9 @@ Suggested concrete modules: E-M5-ARTIFACT-EXTRACTION-LOCAL-001 and E-M5-ARTIFACT-EXTRACTION-CI-001 prove strict TAR/Brotli and ZIP inspection, exclusive owned staging, cancellation/failure cleanup, and exact full-size execution on all six native Node 24 clients. -- [ ] Verify the exact expanded tree before atomically publishing the cache entry. - The disconnected full-tree verifier passes locally and on all six native clients under - E-M5-ARTIFACT-EXTRACTION-LOCAL-001 and E-M5-ARTIFACT-EXTRACTION-CI-001; atomic cache - publication is deliberately absent and keeps this combined item open. +- [x] Verify the exact expanded tree before atomically publishing the cache entry. + E-M5-ARTIFACT-CACHE-ENTRY-LOCAL-001 and E-M5-ARTIFACT-CACHE-ENTRY-CI-001 prove complete staged + archive/tree revalidation before same-filesystem atomic publication on all six native clients. - [x] Coordinate cache ownership across multiple Orca processes and windows. E-M5-ARTIFACT-CACHE-LOCK-LOCAL-001 and E-M5-ARTIFACT-CACHE-LOCK-CI-001 prove real second-process contention and ownership transfer on all six native client tuples. Cache-entry @@ -1100,9 +1100,12 @@ Suggested concrete modules: E-M5-ARTIFACT-CACHE-LOCK-LOCAL-001 and E-M5-ARTIFACT-CACHE-LOCK-CI-001 prove conservative same-host liveness, stale-dead-owner tombstoning, live/ambiguous-owner preservation, and displaced-owner successor safety. No downloader/extractor consumer is connected yet. -- [ ] Never select partial files, partial directories, or entries with a failed verification record. -- [ ] Re-verify cached metadata and expected identity before use; define when full file rehashing is - required. +- [x] Never select partial files, partial directories, or entries with a failed verification record. + E-M5-ARTIFACT-CACHE-ENTRY-LOCAL-001 and E-M5-ARTIFACT-CACHE-ENTRY-CI-001. +- [x] Re-verify cached metadata and expected identity before use; define when full file rehashing is + required. E-M5-ARTIFACT-CACHE-ENTRY-LOCAL-001 and E-M5-ARTIFACT-CACHE-ENTRY-CI-001 require a + strict proof/archive/full-tree rehash on every disconnected lookup; warm-launch proof trust is + a later remote-install contract and is not inferred here. - [ ] Implement bounded eviction that never removes an in-use entry. - [ ] Handle read-only cache, permission failures, disk full, quota, inode exhaustion, cancellation, crash, corruption, and concurrent download deterministically. @@ -12901,14 +12904,14 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea The measurement must clean its owned output and pass before unpublished artifact upload. - Exact full-size results: - | Client tuple | Job | Job duration | Incremental RSS | Extraction/tree time | Result | - | --- | --- | ---: | ---: | ---: | --- | - | darwin-arm64 | [87487012482](https://github.com/stablyai/orca/actions/runs/29455294208/job/87487012482) | 2m01s | 45,989,888 bytes | 1,345.51ms | PASS | - | darwin-x64 | [87487012496](https://github.com/stablyai/orca/actions/runs/29455294208/job/87487012496) | 7m02s | 40,116,224 bytes | 5,757.55ms | PASS | - | linux-arm64-glibc | [87487012507](https://github.com/stablyai/orca/actions/runs/29455294208/job/87487012507) | 2m51s | 36,732,928 bytes | 1,294.77ms | PASS | - | linux-x64-glibc | [87487012518](https://github.com/stablyai/orca/actions/runs/29455294208/job/87487012518) | 2m52s | 38,420,480 bytes | 1,727.80ms | PASS | - | win32-arm64 | [87487012510](https://github.com/stablyai/orca/actions/runs/29455294208/job/87487012510) | 11m07s | 33,517,568 bytes | 2,467.74ms | PASS | - | win32-x64 | [87487012515](https://github.com/stablyai/orca/actions/runs/29455294208/job/87487012515) | 5m24s | 40,140,800 bytes | 1,683.02ms | PASS | + | Client tuple | Job | Job duration | Incremental RSS | Extraction/tree time | Result | + | ----------------- | ---------------------------------------------------------------------------------------- | -----------: | ---------------: | -------------------: | ------ | + | darwin-arm64 | [87487012482](https://github.com/stablyai/orca/actions/runs/29455294208/job/87487012482) | 2m01s | 45,989,888 bytes | 1,345.51ms | PASS | + | darwin-x64 | [87487012496](https://github.com/stablyai/orca/actions/runs/29455294208/job/87487012496) | 7m02s | 40,116,224 bytes | 5,757.55ms | PASS | + | linux-arm64-glibc | [87487012507](https://github.com/stablyai/orca/actions/runs/29455294208/job/87487012507) | 2m51s | 36,732,928 bytes | 1,294.77ms | PASS | + | linux-x64-glibc | [87487012518](https://github.com/stablyai/orca/actions/runs/29455294208/job/87487012518) | 2m52s | 38,420,480 bytes | 1,727.80ms | PASS | + | win32-arm64 | [87487012510](https://github.com/stablyai/orca/actions/runs/29455294208/job/87487012510) | 11m07s | 33,517,568 bytes | 2,467.74ms | PASS | + | win32-x64 | [87487012515](https://github.com/stablyai/orca/actions/runs/29455294208/job/87487012515) | 5m24s | 40,140,800 bytes | 1,683.02ms | PASS | - Adjacent native gates: Linux arm64 oldest-userland job [87488270107](https://github.com/stablyai/orca/actions/runs/29455294208/job/87488270107) passes in @@ -12922,7 +12925,7 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea the exact source, reconstructs and verifies the exact archive, runs Node v24.18.0, PTY, watcher, resource-settlement, and complete-tree smoke, then reports observed Windows `10.0.26200` and `osBuild: false` against the required build `26100`. It fails with `runner does not prove the - declared Windows floor` after 3m40s. This is correct fail-closed evidence and does not close the +declared Windows floor` after 3m40s. This is correct fail-closed evidence and does not close the Windows arm64 oldest-floor cell. - Repository regression gates: exact-head PR Checks [29455294230](https://github.com/stablyai/orca/actions/runs/29455294230), job @@ -12944,7 +12947,7 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea - Owner: Codex implementation owner - Source: exact pushed evidence head `ef76f38d4` on draft PR #8741. - Exact audit commands: `rg --files src/main | rg '(cache|ssh-relay-artifact|lock|quarantine|evict)' | - sort`; targeted `rg -n` over `app.getPath('userData')|cache|lockfile|rename|quarantine|evict` in +sort`; targeted `rg -n` over `app.getPath('userData')|cache|lockfile|rename|quarantine|evict` in `src/main`/`src/shared`; complete reads of the relay downloader, extractor, tree verifier, selector/content-identity/path-policy modules, startup single-instance lock, and Milestone 5 cache contracts. @@ -13157,18 +13160,18 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea cleanup, verification, and fallback classification are unchanged. - Exact commands/results: - `pnpm exec oxfmt --check src/main/ssh/ssh-relay-artifact-cache-lock-lease.ts - src/main/ssh/ssh-relay-artifact-cache-lock.test.ts - src/main/ssh/ssh-relay-artifact-extraction.test.ts`: pass; +src/main/ssh/ssh-relay-artifact-cache-lock.test.ts +src/main/ssh/ssh-relay-artifact-extraction.test.ts`: pass; - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 - src/main/ssh/ssh-relay-artifact-cache-lock.test.ts - src/main/ssh/ssh-relay-artifact-extraction.test.ts`: two passed files / 31 passed tests in 2.88s +src/main/ssh/ssh-relay-artifact-cache-lock.test.ts +src/main/ssh/ssh-relay-artifact-extraction.test.ts`: two passed files / 31 passed tests in 2.88s Vitest / 8.25s wall, 156,925,952-byte maximum RSS, zero swaps; - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 - src/main/ssh/ssh-relay-artifact-*.test.ts`: five passed files / one environment-skipped file, +src/main/ssh/ssh-relay-artifact-*.test.ts`: five passed files / one environment-skipped file, 95 passed / one skipped tests in 23.52s Vitest / 26.47s wall, 143,228,928-byte maximum RSS, zero swaps; - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 - config/scripts/ssh-relay-runtime-*.test.mjs`: 50 passed files / 279 passed tests in 105.46s +config/scripts/ssh-relay-runtime-*.test.mjs`: 50 passed files / 279 passed tests in 105.46s Vitest / 109.78s wall, 193,576,960-byte maximum RSS, zero swaps; - `pnpm typecheck`; `pnpm lint`; `pnpm run check:max-lines-ratchet`; `git diff --check`: pass. Full lint retains only existing unrelated warnings. @@ -13190,21 +13193,21 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea - `gh api 'repos/stablyai/orca/actions/runs/29459424864/jobs?per_page=100' --jq ...` for job, runner, label, step, and timestamp identity; - `gh api repos/stablyai/orca/actions/jobs//logs 2>&1 | rg - "ssh-relay-artifact-cache-lock.test.ts|ssh-relay-artifact-extraction.test.ts|Test Files|Tests " | - tail -8` for each primary job; +"ssh-relay-artifact-cache-lock.test.ts|ssh-relay-artifact-extraction.test.ts|Test Files|Tests " | +tail -8` for each primary job; - `gh api repos/stablyai/orca/actions/jobs/87500906175/logs 2>&1 | rg -n - "Node runtime version|archive|tree|PTY|watcher|Windows|osBuild|26100|26200|runner does not prove|Process completed" - | tail -100` for the retained Windows arm64 floor gap. +"Node runtime version|archive|tree|PTY|watcher|Windows|osBuild|26100|26200|runner does not prove|Process completed" +| tail -100` for the retained Windows arm64 floor gap. - Primary native results: - | Client tuple | Job / runner | Job duration | Contract duration | Lock suite | Extraction suite | Result | - | --- | --- | ---: | ---: | ---: | ---: | --- | - | darwin-arm64 | [87499607559](https://github.com/stablyai/orca/actions/runs/29459424864/job/87499607559), `GitHub Actions 1000056437`, `macos-15` | 1m52s | 13s | 10 pass, 530ms | 21 pass, 524ms | PASS | - | darwin-x64 | [87499607640](https://github.com/stablyai/orca/actions/runs/29459424864/job/87499607640), `GitHub Actions 1000056435`, `macos-15-intel` | 6m45s | 1m03s | 10 pass, 825ms | 21 pass, 3,062ms | PASS | - | linux-arm64-glibc | [87499607604](https://github.com/stablyai/orca/actions/runs/29459424864/job/87499607604), `GitHub Actions 1000056440`, `ubuntu-24.04-arm` | 12m20s | 11s | 10 pass, 514ms | 21 pass, 784ms | PASS | - | linux-x64-glibc | [87499607591](https://github.com/stablyai/orca/actions/runs/29459424864/job/87499607591), `GitHub Actions 1000056439`, `ubuntu-24.04` | 3m38s | 14s | 10 pass, 569ms | 21 pass, 949ms | PASS | - | win32-arm64 | [87499607605](https://github.com/stablyai/orca/actions/runs/29459424864/job/87499607605), `GitHub Actions 1000056441`, `windows-11-arm` | 9m01s | 29s | 10 pass, 801ms | 21 pass, 1,152ms | PASS | - | win32-x64 | [87499607584](https://github.com/stablyai/orca/actions/runs/29459424864/job/87499607584), `GitHub Actions 1000056438`, `windows-2022` | 5m29s | 22s | 10 pass, 751ms | 21 pass, 1,698ms | PASS | + | Client tuple | Job / runner | Job duration | Contract duration | Lock suite | Extraction suite | Result | + | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -----------: | ----------------: | -------------: | ---------------: | ------ | + | darwin-arm64 | [87499607559](https://github.com/stablyai/orca/actions/runs/29459424864/job/87499607559), `GitHub Actions 1000056437`, `macos-15` | 1m52s | 13s | 10 pass, 530ms | 21 pass, 524ms | PASS | + | darwin-x64 | [87499607640](https://github.com/stablyai/orca/actions/runs/29459424864/job/87499607640), `GitHub Actions 1000056435`, `macos-15-intel` | 6m45s | 1m03s | 10 pass, 825ms | 21 pass, 3,062ms | PASS | + | linux-arm64-glibc | [87499607604](https://github.com/stablyai/orca/actions/runs/29459424864/job/87499607604), `GitHub Actions 1000056440`, `ubuntu-24.04-arm` | 12m20s | 11s | 10 pass, 514ms | 21 pass, 784ms | PASS | + | linux-x64-glibc | [87499607591](https://github.com/stablyai/orca/actions/runs/29459424864/job/87499607591), `GitHub Actions 1000056439`, `ubuntu-24.04` | 3m38s | 14s | 10 pass, 569ms | 21 pass, 949ms | PASS | + | win32-arm64 | [87499607605](https://github.com/stablyai/orca/actions/runs/29459424864/job/87499607605), `GitHub Actions 1000056441`, `windows-11-arm` | 9m01s | 29s | 10 pass, 801ms | 21 pass, 1,152ms | PASS | + | win32-x64 | [87499607584](https://github.com/stablyai/orca/actions/runs/29459424864/job/87499607584), `GitHub Actions 1000056438`, `windows-2022` | 5m29s | 22s | 10 pass, 751ms | 21 pass, 1,698ms | PASS | - Aggregate contract results: each POSIX job passes 64 files; each Windows job passes 65 files. The suite includes same-process contention, a real second Node process, bounded cancellation, @@ -13304,7 +13307,7 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea entry modules deliberately absent. - Exact command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 - src/main/ssh/ssh-relay-artifact-cache-entry.test.ts`. +src/main/ssh/ssh-relay-artifact-cache-entry.test.ts`. - Result: required RED; one failed suite / zero collected tests in 153ms Vitest / 1.11s wall, 132,071,424-byte maximum RSS, zero swaps. Import fails only because `./ssh-relay-artifact-cache-entry` does not exist. @@ -13354,7 +13357,7 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea No corruption behavior was weakened. - Exact focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 - src/main/ssh/ssh-relay-artifact-cache-entry.test.ts`. +src/main/ssh/ssh-relay-artifact-cache-entry.test.ts`. - Final focused result: one passed file / 17 passed tests in 1.87s Vitest / 3.10s wall, 137,035,776-byte maximum RSS, zero swaps. Contracts prove fixed 5m/80 MiB/64 KiB/16 KiB limits, exact paths, Linux and Windows archive families, atomic complete publication, exact miss, @@ -13363,14 +13366,14 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea recovery only from freshly reverified bytes. - Exact broader commands/results: - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 - src/main/ssh/ssh-relay-artifact-*.test.ts`: final tree passes six files / two environment-skipped +src/main/ssh/ssh-relay-artifact-*.test.ts`: final tree passes six files / two environment-skipped full-size files, 112 passed / two skipped tests in 164.85s Vitest / 194.56s wall, 123,568,128-byte maximum RSS, zero swaps under severe unrelated host contention; - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 - src/main/ssh/ssh-relay-*.test.ts`: 19 passed files / two skipped files, 280 passed / two skipped +src/main/ssh/ssh-relay-*.test.ts`: 19 passed files / two skipped files, 280 passed / two skipped tests in 78.76s Vitest / 95.05s wall, 215,089,152-byte maximum RSS, zero swaps; - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 - config/scripts/ssh-relay-runtime-*.test.mjs`: 50 passed files / 279 passed tests in 41.66s Vitest / +config/scripts/ssh-relay-runtime-*.test.mjs`: 50 passed files / 279 passed tests in 41.66s Vitest / 45.35s wall, 190,808,064-byte maximum RSS, zero swaps; - `pnpm typecheck`; `pnpm lint`; `pnpm run check:max-lines-ratchet`: pass. Full lint retains only existing unrelated warnings; no max-lines disable or ratchet exception was added; @@ -13403,9 +13406,9 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea measurement step also required the ordering oracle's exact name to follow the expanded boundary. - Final GREEN command/result: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 - config/scripts/ssh-relay-runtime-workflow.test.mjs - src/main/ssh/ssh-relay-artifact-cache-entry.test.ts - src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts`: workflow and focused source suites +config/scripts/ssh-relay-runtime-workflow.test.mjs +src/main/ssh/ssh-relay-artifact-cache-entry.test.ts +src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts`: workflow and focused source suites pass; the environment-only full-size file is skipped locally, for 24 passed / one skipped tests. - Native oracle: every primary client job must run all 17 transaction/corruption contracts before artifact construction. After constructing the exact full-size artifact, every job must measure cold @@ -13418,6 +13421,75 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea - Follow-up: checkpoint/push and require all six primary jobs plus companion PR Checks/Golden E2E before cache-entry closure. +### E-M5-ARTIFACT-CACHE-ENTRY-CI-001 — Immutable publication and verified lookup pass all six native cells + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Exact source: `3c1ead8fb1e1f3b64abaf78705abb0c801a1d64b` on draft PR #8741. +- Workflow: SSH Relay Runtime Artifacts run + [29462394311](https://github.com/stablyai/orca/actions/runs/29462394311). All six primary Node 24 + jobs pass their complete source-contract step, including all 17 immutable cache-entry transaction, + corruption, quarantine, concurrency, and recovery tests. POSIX jobs pass 65 source files and + Windows jobs pass 66 source files. +- Exact native cells and `ssh_relay_full_size_cache` results: + - linux-x64-glibc job + [87508477435](https://github.com/stablyai/orca/actions/runs/29462394311/job/87508477435): + `ubuntu-24.04`, image `ubuntu24` `20260705.232.1`, X64; 36,412,425-byte archive, + 124,846,430 expanded bytes / 34 files; cold 2,244.83ms / 40,214,528-byte incremental RSS; warm + 239.44ms / 0 bytes; + - linux-arm64-glibc job + [87508477441](https://github.com/stablyai/orca/actions/runs/29462394311/job/87508477441): + `ubuntu-24.04-arm`, image `ubuntu24-arm64` `20260706.52.2`, ARM64; 36,363,087-byte archive, + 122,865,324 expanded bytes / 34 files; cold 1,703.97ms / 46,911,488 bytes; warm 195.47ms / + 131,072 bytes; + - darwin-arm64 job + [87508477446](https://github.com/stablyai/orca/actions/runs/29462394311/job/87508477446): + `macos-15`, image `macos15` `20260706.0213.1`, ARM64; 31,776,560-byte archive, + 122,027,869 expanded bytes / 35 files; cold 1,173.97ms / 47,104,000 bytes; warm 123.58ms / + 212,992 bytes; + - darwin-x64 job + [87508477453](https://github.com/stablyai/orca/actions/runs/29462394311/job/87508477453): + `macos-15-intel`, image `macos15` `20260629.0276.1`, X64; 32,922,888-byte archive, + 124,316,655 expanded bytes / 35 files; cold 6,171.98ms / 43,335,680 bytes; warm 702.78ms / + 372,736 bytes; + - win32-x64 job + [87508477439](https://github.com/stablyai/orca/actions/runs/29462394311/job/87508477439): + `windows-2022`, image `win22` `20260714.244.1`, X64; 37,168,778-byte archive, + 96,527,161 expanded bytes / 42 files; cold 2,682.54ms / 48,775,168 bytes; warm 233.89ms / + 466,944 bytes; + - win32-arm64 job + [87508477458](https://github.com/stablyai/orca/actions/runs/29462394311/job/87508477458): + `windows-11-arm`, image `win11-arm64` `20260714.109.1`, ARM64; 33,204,738-byte archive, + 85,213,511 expanded bytes / 42 files; cold 3,535.96ms / 34,484,224 bytes; warm 513.02ms / + 6,926,336 bytes. +- Budget result: every cold and warm phase is below the separate five-minute and 80 MiB incremental + RSS ceilings; every environment-only full-size test reports one passed file and removes its owned + cache root. Observed cold range is 1,173.97–6,171.98ms and 34,484,224–48,775,168 bytes; warm range + is 123.58–702.78ms and 0–6,926,336 bytes. +- Companion regressions: PR Checks run + [29462394310](https://github.com/stablyai/orca/actions/runs/29462394310), job + [87508477369](https://github.com/stablyai/orca/actions/runs/29462394310/job/87508477369), passes in + 14m02s. Golden E2E run + [29462394344](https://github.com/stablyai/orca/actions/runs/29462394344) passes Linux job + [87508477410](https://github.com/stablyai/orca/actions/runs/29462394344/job/87508477410) in 4m18s + and macOS job + [87508477409](https://github.com/stablyai/orca/actions/runs/29462394344/job/87508477409) in 3m06s. +- Supplemental/floor result: both Linux oldest-userland supplements pass. Windows x64 oldest-floor + job [87509946279](https://github.com/stablyai/orca/actions/runs/29462394311/job/87509946279) + passes. Windows arm64 primary runtime/source/cache proof passes, then oldest-floor job + [87509946287](https://github.com/stablyai/orca/actions/runs/29462394311/job/87509946287) + retains the declared build-26200 versus required-26100 rejection; therefore the overall artifact + workflow is expected red and no Windows arm64 tuple is enabled. +- Closure: exact content-key paths, same-filesystem exclusive staging, full archive/tree revalidation + before atomic publication, strict proof/archive/tree lookup, corruption quarantine, concurrent + serialization, and fresh-byte recovery are proven locally and on all six native clients. +- Residual gaps: crash/power-loss durability, disk-full/quota/inode/read-only faults, in-use leases, + recency/2 GiB eviction, packaged Electron/cache-root selection, proxy/certificate behavior, SSH, + and every tuple/default path remain open. No artifact is published and no production consumer is + connected. +- Follow-up: checkpoint and push this evidence, then audit and RED-test only the disconnected in-use + lease/recency/2 GiB eviction package. Keep consumers and SSH behavior separate. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From b79bcbd04e50b86186645bcba04d811671eb3035 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 21:40:51 -0400 Subject: [PATCH 175/267] feat: add disconnected relay cache eviction Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 4 + .../ssh-relay-runtime-workflow.test.mjs | 2 + ...elease-implementation-checklist-summary.md | 15 +- ...github-release-implementation-checklist.md | 184 ++++++++++- ...h-relay-artifact-cache-entry-accounting.ts | 67 ++++ ...lay-artifact-cache-entry-full-size.test.ts | 63 +++- ...h-relay-artifact-cache-eviction-fixture.ts | 45 +++ .../ssh-relay-artifact-cache-eviction.test.ts | 273 ++++++++++++++++ .../ssh/ssh-relay-artifact-cache-eviction.ts | 293 ++++++++++++++++++ ...-relay-artifact-cache-in-use-inspection.ts | 87 ++++++ ...-relay-artifact-cache-in-use-lease.test.ts | 178 +++++++++++ .../ssh-relay-artifact-cache-in-use-lease.ts | 258 +++++++++++++++ .../ssh-relay-artifact-cache-in-use-record.ts | 113 +++++++ .../ssh/ssh-relay-artifact-cache-recency.ts | 113 +++++++ 14 files changed, 1676 insertions(+), 19 deletions(-) create mode 100644 src/main/ssh/ssh-relay-artifact-cache-entry-accounting.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-eviction-fixture.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-eviction.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-in-use-inspection.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-in-use-lease.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-in-use-record.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-recency.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 4d8f5b7de5c..289adc424d3 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -266,6 +266,8 @@ jobs: src/main/ssh/ssh-relay-artifact-extraction.test.ts \ src/main/ssh/ssh-relay-artifact-cache-lock.test.ts \ src/main/ssh/ssh-relay-artifact-cache-entry.test.ts \ + src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts \ + src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts \ src/main/ssh/ssh-relay-runtime-identity.test.ts \ config/scripts/ssh-relay-runtime-compatibility.test.mjs \ config/scripts/ssh-relay-node-release-verification.test.mjs \ @@ -798,6 +800,8 @@ jobs: src/main/ssh/ssh-relay-artifact-extraction.test.ts ` src/main/ssh/ssh-relay-artifact-cache-lock.test.ts ` src/main/ssh/ssh-relay-artifact-cache-entry.test.ts ` + src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts ` + src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts ` src/main/ssh/ssh-relay-runtime-identity.test.ts ` config/scripts/ssh-relay-runtime-compatibility.test.mjs ` config/scripts/ssh-relay-node-release-verification.test.mjs ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index de35c44d31b..0e9b36bf790 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -124,6 +124,8 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-artifact-extraction.test.ts', 'ssh-relay-artifact-cache-lock.test.ts', 'ssh-relay-artifact-cache-entry.test.ts', + 'ssh-relay-artifact-cache-in-use-lease.test.ts', + 'ssh-relay-artifact-cache-eviction.test.ts', 'ssh-relay-runtime-identity.test.ts' ]) { // Why: portable desktop selection contracts need proof on every native runner family, not 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 8940f7da7fe..117702c0387 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 @@ -9,8 +9,12 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — immutable cache entry -closed; in-use lease/2 GiB eviction is next, 2026-07-15, Codex implementation owner.** Exact-head +Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — local lease/recency/ +eviction contracts are green; wiring full-size all-six native proof, 2026-07-15, Codex implementation +owner.** Fourteen focused contracts, workflow wiring, broad relay/release suites, typecheck, lint, and +static gates pass locally under `E-M5-ARTIFACT-CACHE-EVICTION-LOCAL-001` and +`E-M5-ARTIFACT-CACHE-EVICTION-CI-WIRING-LOCAL-001`; the package remains open until exact-head native +CI supplies all-six full-size retention/eviction metrics. Exact-head run [29462394311](https://github.com/stablyai/orca/actions/runs/29462394311) passes all 17 cache-entry contracts and exact full-size cold/warm measurements on Linux, macOS, and Windows, x64 and arm64, under `E-M5-ARTIFACT-CACHE-ENTRY-CI-001`. Cold publication is 1.17–6.17s and 34.5–48.8 MiB @@ -18,7 +22,7 @@ incremental RSS; warm verified lookup is 0.12–0.70s and 0–6.9 MiB. PR Checks [29462394310](https://github.com/stablyai/orca/actions/runs/29462394310) and Golden E2E [29462394344](https://github.com/stablyai/orca/actions/runs/29462394344) pass. Windows arm64 retains only the declared hosted build-26200 versus required-26100 floor gap after its primary runtime/cache -proof. Desktop consumers, eviction, SSH transfer/install, mode wiring, tuple enablement, publication, +proof. Desktop consumers, SSH transfer/install, mode wiring, tuple enablement, publication, production keys/environment/seed, and merge to `main` remain disconnected. ## Safety status @@ -317,8 +321,9 @@ production keys/environment/seed, and merge to `main` remain disconnected. - [ ] Add exclusive staging, atomic publication, quarantine, locking, and the 2 GiB cache policy. Locking and immutable publication/lookup/quarantine are closed locally and on all six native clients under `E-M5-ARTIFACT-CACHE-LOCK-CI-001` and - `E-M5-ARTIFACT-CACHE-ENTRY-CI-001`. In-use leases, recency, and bounded 2 GiB eviction remain - the only open part of this combined item. + `E-M5-ARTIFACT-CACHE-ENTRY-CI-001`. In-use leases, recency, exact byte accounting, and bounded + eviction pass locally under `E-M5-ARTIFACT-CACHE-EVICTION-LOCAL-001`; all-six native/full-size + eviction evidence remains the only open part of this combined item. - [ ] Prove verified cached bytes can be transferred while the client is offline. - [ ] Preserve `ORCA_RELAY_PATH` behind the official-build trust boundary. 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 6aa5897f6e1..2bded9c897a 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 5 / Work Package 4 desktop cache boundary — **Checkpoint — 2026-07-15, Codex implementation owner: immutable cache-entry gate closed; evidence checkpoint/push required before in-use lease/eviction work**. E-M5-ARTIFACT-CACHE-ENTRY-CI-001 closes all six primary Node 24 source and exact full-size cold-publication/warm-verified-lookup cells at `3c1ead8fb1e1f3b64abaf78705abb0c801a1d64b`. PR Checks and Golden E2E pass. No cache consumer, eviction, SSH path, mode, tuple, release write, or default behavior is connected.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — resumed with user authority for end-to-end PR-contained implementation, commits, pushes, CI, rehearsals, and release writes, with merging to `main` explicitly prohibited. The first native run exposed and recorded Windows open-handle rename and equivalent zlib-error-oracle REDs; narrow corrections passed locally and at exact-head CI. In replacement run `29459424864`, all six primary jobs pass the 10 lock and 21 extraction contracts plus deterministic artifact build/smoke. The workflow's sole failure is the retained Windows arm64 hosted build-26200 versus required-26100 floor rejection after runtime/archive/tree/Node/PTTY/watcher proof; no tuple is enabled from it. PR Checks `29459424977` and Golden E2E `29459424789` pass. There is no release publication, cache entry, cache consumer, SSH transfer/install, tuple enablement, or production/default behavior change. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: wire and prove disconnected in-use lease/recency/2 GiB eviction on all six native runners**. The purpose-named RED is recorded, 14 focused contracts and the 126-test artifact source suite are locally green, and the next gate is full-size eviction measurement plus workflow-contract and all-six native evidence. Keep every Electron, downloader, SSH, setting, tuple, publication, and default-path consumer disconnected.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — user authority covers end-to-end PR-contained implementation, commits, pushes, CI, rehearsals, and release writes; merging to `main` remains explicitly prohibited. The active uncommitted package implements only cross-process in-use leases, persistent recency, exact logical-byte accounting, and bounded eviction. It has no Electron, downloader, SSH, setting, tuple, publication, or default-path consumer. 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) @@ -13490,6 +13490,170 @@ src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts`: workflow and foc - Follow-up: checkpoint and push this evidence, then audit and RED-test only the disconnected in-use lease/recency/2 GiB eviction package. Keep consumers and SSH behavior separate. +### E-M5-ARTIFACT-CACHE-EVICTION-AUDIT-001 — In-use lease, recency, and 2 GiB eviction boundary + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed cache-entry evidence head `0ae3e005f` on draft PR #8741. +- Exact audit commands: complete reads of the cache entry, proof, verification, content-lock record, + content-lock lease, and content-lock tests; `rg --files src/main/ssh` over relay cache/artifact + modules; targeted `rg -n` over cache eviction, in-use lease, recency/LRU, protected content, and + native workflow invocations in `src/main`, `src/shared`, `.github/workflows`, and `config/scripts`. +- Findings: + - no relay cache eviction, in-use lease, persistent recency, exact cache-byte accounting, or + consumer exists. The immutable entry becomes selectable only after complete verification, but a + future consumer needs a cross-process reference before the content lock is released to eviction; + - in-use references must be shared, not exclusive. Each exact content ID therefore owns a + token-named lease directory with a strict nonce/host/PID/acquired/heartbeat record and owner-handle + heartbeat. Active, malformed, remote-host, PID-reused, or otherwise ambiguous leases protect the + entry. Only a confirmed stale same-host dead owner may be tombstoned and removed; + - lease acquisition and eviction both acquire the proven per-content lock. Acquisition rechecks the + exact final entry path before making its lease visible; eviction rechecks entry state, staging, and + leases while holding the same lock. This closes the lookup-to-reference/delete race without + changing cache lookup or adding a consumer; + - successful lease acquisition/release records persistent recency outside the immutable entry via + token-shaped append records. Readers take the greatest valid timestamp; malformed or unexpected + recency is ambiguous and conservatively protects the entry. Missing recency uses the immutable + entry timestamp as its initial publication order; + - one global eviction transaction is serialized by the proven lock implementation in a dedicated + eviction namespace, so no valid content digest is reserved. Per-entry locks still serialize with + publication and reference acquisition; + - exact accounting sums stable logical regular-file sizes without following links and rejects + unsafe-integer arithmetic or ambiguous/special trees. The 2 GiB limit applies to complete + selectable entries; staging, locked, ambiguous, current-manifest, and running content are never + deleted and can leave an explicit over-budget residual; + - eviction orders idle complete entries by persistent LRU with a content-ID tie break. Strictly + protected content is never removed. Caller-declared current/previous retention is considered only + after ordinary candidates and may be removed only when required to meet the cap, matching “retain + when they fit”; + - deletion rechecks all guards under the content lock, atomically renames only the exact final entry + to a token-shaped unselectable tombstone, then removes owned bytes. Cancellation is checked before + each bounded scan/acquire/delete stage and all acquired locks/leases must settle. +- Required purpose-named contracts: exact paths/limits; multiple concurrent references; real + cross-process visibility; heartbeat and owner-only release; stale-dead cleanup; live/malformed/ + remote/displaced-owner preservation; exact logical-byte accounting; deterministic LRU and tie + break; hard and preferred retention; staging/content-lock protection; concurrent eviction; + cancellation settlement; symlink/special/unsafe state protection; and no partial/tombstone selection. +- Deliberately separate: cache-root selection, embedded-manifest loading, proxy/certificates, + downloader calls, offline transfer, SSH, remote install, ORCA_RELAY_PATH, fallback classification, + settings/mode, tuple enablement, release publication, and production/default behavior. +- Follow-up: add `ssh-relay-artifact-cache-eviction.test.ts` with production modules absent, record + the RED, then implement only the audited lease/recency/eviction modules and require focused, + broad/static, native-workflow wiring, and all-six native evidence. + +### E-M5-ARTIFACT-CACHE-EVICTION-LOCAL-RED-001 — In-use lease and eviction capabilities are absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: purpose-named `ssh-relay-artifact-cache-in-use-lease.test.ts` and + `ssh-relay-artifact-cache-eviction.test.ts` atop pushed evidence head `0ae3e005f`; production + lease/eviction modules deliberately absent. +- Exact command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts +src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts`. +- Environment: macOS 26.2 arm64 build 25C56, Node v26.0.0, pnpm 10.24.0, Vitest 4.1.5. +- Result: required RED; two failed suites / zero collected tests in 642ms Vitest / 2.57s wall, + 132,841,472-byte maximum RSS, zero swaps. Imports fail because both audited production modules do + not exist. +- Contracts encoded before implementation: exact paths/limits; shared concurrent and real + second-process leases; heartbeat and displaced-owner release; missing-entry rejection; exact + logical-byte accounting; LRU; hard/preferred retention; active/malformed/live/remote/stale-dead + lease handling; staging/content-lock/cancellation safety; concurrent evictors; and unsafe-link + preservation. +- Consumer-disconnection oracle: tests import the new source capabilities directly and construct + isolated cache fixtures. No Electron, downloader, SSH, setting, mode, tuple, release, or default + call site is added. +- Follow-up: implement only concrete in-use record, recency, lease, and eviction responsibilities; + make both purpose suites green without weakening conservative ambiguity or reference safety. + +### E-M5-ARTIFACT-CACHE-EVICTION-LOCAL-001 — Disconnected in-use leases and bounded eviction pass locally + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted package atop exact pushed evidence head `0ae3e005f`, following + E-M5-ARTIFACT-CACHE-EVICTION-AUDIT-001 and E-M5-ARTIFACT-CACHE-EVICTION-LOCAL-RED-001. +- Implementation: + - strict token-named shared lease records bind content ID, nonce, host, PID, acquisition, heartbeat, + and directory identity. Acquisition and release serialize through the proven per-content lock; + - 5s heartbeats and owner-only release preserve active and displaced owners. Active, malformed, + remote-host, PID-reused/live, or otherwise ambiguous references block deletion. Only a stale + same-host owner whose PID is confirmed dead is atomically tombstoned and reclaimed; + - persistent token-shaped recency records live outside immutable entries and are bounded to the + newest valid timestamp. Missing recency starts at publication mtime; malformed/racing state is + ambiguous and protected; + - exact stable logical-file accounting follows no links and bounds each entry to 10,000 members. + Unsafe, special, unstable, or unaccountable trees remain protected; + - one dedicated global eviction lock serializes transactions, while per-content locks close + publication/reference/delete races. The default cap is 2 GiB; hard-protected content is never + removed, preferred current/previous content follows ordinary LRU candidates, and complete idle + entries become unselectable by atomic tombstone rename before owned recursive deletion. +- Exact focused command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts +src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts`. +- Final focused result: two passed files / 14 passed tests in 2.10s Vitest / 4.12s wall, + 131,629,056-byte maximum RSS, zero swaps on macOS 26.2 arm64 build 25C56, Node v26.0.0, pnpm + 10.24.0, Vitest 4.1.5. +- Contracts prove exact paths/limits and invalid-input rejection, multiple references, real + child-process visibility, heartbeat/displaced-owner safety, missing-entry rejection, bounded + recency, exact accounting/deterministic LRU, hard/preferred retention, active/malformed/live/ + remote/stale-dead behavior, staging/content-lock/cancellation settlement, concurrent eviction, and + unsafe linked-tree preservation. +- Exact broader commands/results: + - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-artifact-*.test.ts`: eight passed files / two environment-skipped files, + 126 passed / two skipped tests in 15.49s Vitest / 23.55s wall, 159,383,552-byte maximum RSS, + zero swaps; + - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-*.test.ts`: 21 passed files / three skipped files, 294 passed / three + skipped tests in 52.31s Vitest / 55.78s wall, 205,340,672-byte maximum RSS, zero swaps; + - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +config/scripts/ssh-relay-runtime-*.test.mjs`: 50 passed files / 279 passed tests in 86.52s + Vitest / 93.38s wall, 188,940,288-byte maximum RSS, zero swaps; + - `/usr/bin/time -l pnpm typecheck`: pass in 5.70s wall with 1,208,139,776-byte maximum RSS; + - `/usr/bin/time -l pnpm lint`: pass in 30.71s wall with 2,093,400,064-byte maximum RSS. Full + lint retains only existing unrelated warnings and includes switch exhaustiveness, 41 reliability + gates, max-lines ratchet, bundled-skill, and localization gates; + - `pnpm exec oxfmt --write` over all package/workflow/checklist files: pass in 4.334s; + `/usr/bin/git diff --check`: pass. No max-lines suppression or ratchet exception was added. +- Consumer/diff oracle: direct source tests are the only caller. The preserved Milestone 0 resolver + pair has zero diff. No Electron/cache-root/downloader/SSH/setting/mode/tuple/publication/default + consumer exists; legacy remains the only production path. +- Does not prove: Node 24/native-client behavior, exact full-size active retention/eviction resources, + disk-full/read-only/quota/inode faults, crash/power-loss durability, packaged Electron, SSH, native + trust, or any enabled tuple. All remain separately gated. +- Follow-up: wire the purpose suites into both native families and extend the exact full-size cache + measurement through active retention, release, exact-byte eviction, and cleanup. Push the isolated + package and require all-six native source/full-size evidence before closing this combined cache + item or adding any consumer. + +### E-M5-ARTIFACT-CACHE-EVICTION-CI-WIRING-LOCAL-001 — Both native families require source and full-size eviction gates + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: uncommitted workflow/test extension atop E-M5-ARTIFACT-CACHE-EVICTION-LOCAL-001. +- Implementation: both POSIX and PowerShell native Node 24 source commands now require the 14 + purpose-named lease/eviction contracts. The existing exact-artifact cache measurement keeps every + cold/warm JSON field and additionally acquires a real in-use lease, proves a zero-byte-cap eviction + retains the complete entry, releases the lease, then measures exact logical bytes reclaimed, + elapsed time, incremental RSS, entry removal, and test-root cleanup. +- Budgets/oracles: each retention and eviction transaction is bounded at 5m and 80 MiB incremental + RSS; lease acquisition is bounded at 2m. The test timeout is the explicit sum of two entry, two + eviction, one acquisition, and 10s cleanup budgets. The native workflow must remove the measurement + root after the test on both shell families. +- Exact command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 config/scripts/ssh-relay-runtime-workflow.test.mjs +src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts`. +- Result without full-size environment: workflow file passes seven tests and the full-size file skips + its one environment-gated test as intended in 1.36s Vitest / 3.41s wall, 134,496,256-byte maximum + RSS, zero swaps. The standalone workflow command also passed seven tests in 861ms Vitest / 4.32s + wall, 131,629,056-byte maximum RSS. +- Does not prove: actual full-size eviction or native runner behavior. Those require the pushed exact + head on Linux, macOS, and Windows x64/arm64; no checklist box closes from this local wiring proof. +- Follow-up: commit and push this isolated package, then capture exact run/job IDs, commit SHA, + runner images/architectures, per-tuple retention/eviction timings and RSS, exact reclaimed bytes, + cleanup results, and residual floor gaps under a new CI evidence ID. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -13548,15 +13712,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Audit cache ownership across processes/windows, immutable entry naming/proofs, exclusive locking, -stale-lock recovery, atomic publication, corruption quarantine, in-use protection, and bounded 2 GiB -eviction. Record the audit and add purpose-named failing contracts before implementing this narrow, -disconnected cache-state package. Do not add a desktop call site, SSH transfer/install, mode wiring, -tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, -Windows ZIP, -`ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, -Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, -and every tuple separately gated. +Commit and push the disconnected in-use lease/recency/2 GiB eviction package, then require both +purpose suites and exact full-size active-retention/release/eviction measurements on all six native +jobs. Do not add a desktop call site, SSH transfer/install, mode wiring, tuple enablement, release +publication, or default behavior until that CI evidence is complete. +Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets +behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, +release-cut, desktop builds, publication, and every tuple separately gated. 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 diff --git a/src/main/ssh/ssh-relay-artifact-cache-entry-accounting.ts b/src/main/ssh/ssh-relay-artifact-cache-entry-accounting.ts new file mode 100644 index 00000000000..5bed0bb274b --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-entry-accounting.ts @@ -0,0 +1,67 @@ +import { lstat, readdir } from 'node:fs/promises' +import { join } from 'node:path' + +export const SSH_RELAY_ARTIFACT_CACHE_MAXIMUM_MEMBERS_PER_ENTRY = 10_000 + +function safeAdd(left: number, right: bigint): number | null { + if (right < 0n || right > BigInt(Number.MAX_SAFE_INTEGER)) { + return null + } + const total = left + Number(right) + return Number.isSafeInteger(total) ? total : null +} + +export async function measureSshRelayArtifactCacheEntryLogicalBytes( + root: string, + signal: AbortSignal +): Promise { + const pending = [root] + let members = 0 + let bytes = 0 + while (pending.length > 0) { + signal.throwIfAborted() + if (++members > SSH_RELAY_ARTIFACT_CACHE_MAXIMUM_MEMBERS_PER_ENTRY) { + return null + } + const path = pending.pop()! + const before = await lstat(path, { bigint: true }).catch(() => null) + if (!before || before.isSymbolicLink()) { + return null + } + if (before.isFile()) { + const next = safeAdd(bytes, before.size) + if (next === null) { + return null + } + bytes = next + const after = await lstat(path, { bigint: true }).catch(() => null) + if ( + !after?.isFile() || + after.dev !== before.dev || + after.ino !== before.ino || + after.size !== before.size || + after.mtimeNs !== before.mtimeNs || + after.ctimeNs !== before.ctimeNs + ) { + return null + } + continue + } + if (!before.isDirectory()) { + return null + } + const names = await readdir(path) + pending.push(...names.map((name) => join(path, name))) + const after = await lstat(path, { bigint: true }).catch(() => null) + if ( + !after?.isDirectory() || + after.dev !== before.dev || + after.ino !== before.ino || + after.mtimeNs !== before.mtimeNs || + after.ctimeNs !== before.ctimeNs + ) { + return null + } + } + return bytes +} diff --git a/src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts b/src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts index 1b90768526d..f50f6e88b53 100644 --- a/src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts +++ b/src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts @@ -8,6 +8,14 @@ import { publishSshRelayArtifactCacheEntry, SSH_RELAY_ARTIFACT_CACHE_ENTRY_LIMITS } from './ssh-relay-artifact-cache-entry' +import { + evictSshRelayArtifactCache, + SSH_RELAY_ARTIFACT_CACHE_EVICTION_LIMITS +} from './ssh-relay-artifact-cache-eviction' +import { + acquireSshRelayArtifactCacheInUseLease, + SSH_RELAY_ARTIFACT_CACHE_IN_USE_LIMITS +} from './ssh-relay-artifact-cache-in-use-lease' import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector' type MeasurementIdentity = { @@ -87,7 +95,7 @@ async function measure(operation: () => Promise): Promise<{ describe.skipIf(!hasMeasurementInput)('SSH relay full-size immutable artifact cache entry', () => { it( - 'keeps cold publication and verified lookup within desktop resource budgets', + 'keeps cold publication, verified lookup, active retention, and eviction within budgets', async () => { const identity = measurementIdentity( JSON.parse(await readFile(identityPath as string, 'utf8')) as unknown @@ -105,6 +113,19 @@ describe.skipIf(!hasMeasurementInput)('SSH relay full-size immutable artifact ca const warm = await measure(() => lookupSshRelayArtifactCacheEntry({ cacheRoot: cacheRoot as string, artifact }) ) + if (warm.result.kind !== 'hit') { + throw new Error('Full-size cache entry unexpectedly missed after cold publication') + } + const lease = await acquireSshRelayArtifactCacheInUseLease({ + cacheRoot: cacheRoot as string, + entry: warm.result.entry + }) + const retained = await measure(() => + evictSshRelayArtifactCache({ cacheRoot: cacheRoot as string, maximumBytes: 0 }) + ).finally(() => lease.release()) + const eviction = await measure(() => + evictSshRelayArtifactCache({ cacheRoot: cacheRoot as string, maximumBytes: 0 }) + ) console.log( `ssh_relay_full_size_cache=${JSON.stringify({ tupleId: identity.tupleId, @@ -114,7 +135,13 @@ describe.skipIf(!hasMeasurementInput)('SSH relay full-size immutable artifact ca coldElapsedMs: cold.elapsedMs, coldIncrementalRssBytes: cold.incrementalRssBytes, warmElapsedMs: warm.elapsedMs, - warmIncrementalRssBytes: warm.incrementalRssBytes + warmIncrementalRssBytes: warm.incrementalRssBytes, + retainedElapsedMs: retained.elapsedMs, + retainedIncrementalRssBytes: retained.incrementalRssBytes, + evictionElapsedMs: eviction.elapsedMs, + evictionIncrementalRssBytes: eviction.incrementalRssBytes, + evictionInitialBytes: eviction.result.initialBytes, + evictionReclaimedBytes: eviction.result.reclaimedBytes })}` ) expect(cold.result).toMatchObject({ @@ -124,6 +151,25 @@ describe.skipIf(!hasMeasurementInput)('SSH relay full-size immutable artifact ca expandedBytes: identity.archive.expandedSize }) expect(warm.result).toEqual({ kind: 'hit', entry: cold.result }) + expect(retained.result).toMatchObject({ + initialBytes: expect.any(Number), + finalBytes: expect.any(Number), + reclaimedBytes: 0, + evictedContentIds: [], + blockedContentIds: [identity.contentId], + accountingComplete: true + }) + expect(retained.result.initialBytes).toBeGreaterThan(0) + expect(retained.result.finalBytes).toBe(retained.result.initialBytes) + expect(eviction.result).toEqual({ + initialBytes: retained.result.initialBytes, + finalBytes: 0, + reclaimedBytes: retained.result.initialBytes, + evictedContentIds: [identity.contentId], + blockedContentIds: [], + accountingComplete: true + }) + await expect(stat(cold.result.entryPath)).rejects.toMatchObject({ code: 'ENOENT' }) for (const measurement of [cold, warm]) { expect(measurement.elapsedMs).toBeLessThanOrEqual( SSH_RELAY_ARTIFACT_CACHE_ENTRY_LIMITS.transactionTimeoutMs @@ -132,10 +178,21 @@ describe.skipIf(!hasMeasurementInput)('SSH relay full-size immutable artifact ca SSH_RELAY_ARTIFACT_CACHE_ENTRY_LIMITS.maximumIncrementalMemoryBytes ) } + for (const measurement of [retained, eviction]) { + expect(measurement.elapsedMs).toBeLessThanOrEqual( + SSH_RELAY_ARTIFACT_CACHE_EVICTION_LIMITS.transactionTimeoutMs + ) + expect(measurement.incrementalRssBytes).toBeLessThanOrEqual( + SSH_RELAY_ARTIFACT_CACHE_ENTRY_LIMITS.maximumIncrementalMemoryBytes + ) + } } finally { await rm(cacheRoot as string, { recursive: true, force: true }) } }, - SSH_RELAY_ARTIFACT_CACHE_ENTRY_LIMITS.transactionTimeoutMs * 2 + 10_000 + SSH_RELAY_ARTIFACT_CACHE_ENTRY_LIMITS.transactionTimeoutMs * 2 + + SSH_RELAY_ARTIFACT_CACHE_EVICTION_LIMITS.transactionTimeoutMs * 2 + + SSH_RELAY_ARTIFACT_CACHE_IN_USE_LIMITS.acquisitionTimeoutMs + + 10_000 ) }) diff --git a/src/main/ssh/ssh-relay-artifact-cache-eviction-fixture.ts b/src/main/ssh/ssh-relay-artifact-cache-eviction-fixture.ts new file mode 100644 index 00000000000..f5aa49ed94f --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-eviction-fixture.ts @@ -0,0 +1,45 @@ +import { mkdir, writeFile } from 'node:fs/promises' +import { join } from 'node:path' + +import type { SshRelayArtifactCacheEntry } from './ssh-relay-artifact-cache-entry-verification' +import type { SshRelayDigest } from './ssh-relay-runtime-identity' + +export function sshRelayArtifactCacheEvictionContentId(character: string): SshRelayDigest { + return `sha256:${character.repeat(64)}` as SshRelayDigest +} + +export async function createSshRelayArtifactCacheEvictionFixture({ + cacheRoot, + character, + payloadBytes = 32 +}: { + cacheRoot: string + character: string + payloadBytes?: number +}): Promise<{ entry: SshRelayArtifactCacheEntry; logicalBytes: number }> { + const contentId = sshRelayArtifactCacheEvictionContentId(character) + const entryPath = join(cacheRoot, 'entries', contentId.slice('sha256:'.length)) + const runtimeRoot = join(entryPath, 'runtime') + const archivePath = join(entryPath, 'runtime.tar.br') + const proofPath = join(entryPath, 'proof.json') + const archive = Buffer.alloc(payloadBytes, character) + const runtime = Buffer.alloc(payloadBytes + 1, character) + const proof = Buffer.from('{}\n') + await mkdir(runtimeRoot, { recursive: true, mode: 0o700 }) + await writeFile(archivePath, archive, { mode: 0o600 }) + await writeFile(join(runtimeRoot, 'relay.js'), runtime, { mode: 0o700 }) + await writeFile(proofPath, proof, { mode: 0o600 }) + return { + entry: { + contentId, + tupleId: 'linux-x64-glibc', + entryPath, + archivePath, + runtimeRoot, + proofPath, + files: 1, + expandedBytes: runtime.length + }, + logicalBytes: archive.length + runtime.length + proof.length + } +} diff --git a/src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts b/src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts new file mode 100644 index 00000000000..fdaa5467654 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts @@ -0,0 +1,273 @@ +import { mkdir, mkdtemp, readFile, rm, stat, symlink, writeFile } from 'node:fs/promises' +import { hostname, tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { createSshRelayArtifactCacheEvictionFixture } from './ssh-relay-artifact-cache-eviction-fixture' +import { + evictSshRelayArtifactCache, + SSH_RELAY_ARTIFACT_CACHE_EVICTION_LIMITS +} from './ssh-relay-artifact-cache-eviction' +import { acquireSshRelayArtifactCacheInUseLease } from './ssh-relay-artifact-cache-in-use-lease' +import { acquireSshRelayArtifactCacheLock } from './ssh-relay-artifact-cache-lock' +import type { SshRelayDigest } from './ssh-relay-runtime-identity' + +const temporaryDirectories: string[] = [] + +async function cacheRoot(): Promise { + const root = await mkdtemp(join(tmpdir(), 'orca-relay-cache-eviction-')) + temporaryDirectories.push(root) + return join(root, 'cache') +} + +afterEach(async () => { + vi.useRealTimers() + await Promise.all( + temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true })) + ) +}) + +describe('SSH relay artifact cache eviction', () => { + it('pins the 2 GiB cap and bounded stable-tree scan', () => { + expect(SSH_RELAY_ARTIFACT_CACHE_EVICTION_LIMITS).toEqual({ + maximumBytes: 2 * 1024 * 1024 * 1024, + transactionTimeoutMs: 5 * 60_000, + maximumMembersPerEntry: 10_000 + }) + expect(Object.isFrozen(SSH_RELAY_ARTIFACT_CACHE_EVICTION_LIMITS)).toBe(true) + }) + + it('rejects invalid budgets and protected identities before touching cache state', async () => { + const root = await cacheRoot() + await expect(evictSshRelayArtifactCache({ cacheRoot: root, maximumBytes: -1 })).rejects.toThrow( + /maximum bytes/i + ) + await expect( + evictSshRelayArtifactCache({ + cacheRoot: root, + protectedContentIds: ['not-a-digest' as SshRelayDigest] + }) + ).rejects.toThrow(/content id/i) + }) + + it('accounts exact logical bytes and evicts least-recently-used idle content', async () => { + vi.useFakeTimers() + const root = await cacheRoot() + vi.setSystemTime(1_000) + const oldest = await createSshRelayArtifactCacheEvictionFixture({ + cacheRoot: root, + character: 'a' + }) + const firstLease = await acquireSshRelayArtifactCacheInUseLease({ + cacheRoot: root, + entry: oldest.entry + }) + await firstLease.release() + vi.setSystemTime(2_000) + const newest = await createSshRelayArtifactCacheEvictionFixture({ + cacheRoot: root, + character: 'b' + }) + const secondLease = await acquireSshRelayArtifactCacheInUseLease({ + cacheRoot: root, + entry: newest.entry + }) + await secondLease.release() + + const result = await evictSshRelayArtifactCache({ + cacheRoot: root, + maximumBytes: newest.logicalBytes + }) + + expect(result).toMatchObject({ + initialBytes: oldest.logicalBytes + newest.logicalBytes, + finalBytes: newest.logicalBytes, + reclaimedBytes: oldest.logicalBytes, + evictedContentIds: [oldest.entry.contentId] + }) + await expect(stat(oldest.entry.entryPath)).rejects.toMatchObject({ code: 'ENOENT' }) + await expect(stat(newest.entry.entryPath)).resolves.toMatchObject({ + isDirectory: expect.any(Function) + }) + }) + + it('never evicts active references and becomes evictable after every release', async () => { + const root = await cacheRoot() + const value = await createSshRelayArtifactCacheEvictionFixture({ + cacheRoot: root, + character: 'a' + }) + const first = await acquireSshRelayArtifactCacheInUseLease({ + cacheRoot: root, + entry: value.entry + }) + const second = await acquireSshRelayArtifactCacheInUseLease({ + cacheRoot: root, + entry: value.entry + }) + const retained = await evictSshRelayArtifactCache({ cacheRoot: root, maximumBytes: 0 }) + expect(retained).toMatchObject({ finalBytes: value.logicalBytes, evictedContentIds: [] }) + + await Promise.all([first.release(), second.release()]) + const removed = await evictSshRelayArtifactCache({ cacheRoot: root, maximumBytes: 0 }) + expect(removed.evictedContentIds).toEqual([value.entry.contentId]) + }) + + it('retains hard-protected content and prefers current/previous content while it fits', async () => { + const root = await cacheRoot() + const hard = await createSshRelayArtifactCacheEvictionFixture({ + cacheRoot: root, + character: 'a' + }) + const preferred = await createSshRelayArtifactCacheEvictionFixture({ + cacheRoot: root, + character: 'b' + }) + const ordinary = await createSshRelayArtifactCacheEvictionFixture({ + cacheRoot: root, + character: 'c' + }) + const result = await evictSshRelayArtifactCache({ + cacheRoot: root, + maximumBytes: hard.logicalBytes + preferred.logicalBytes, + protectedContentIds: [hard.entry.contentId], + preferredRetentionContentIds: [preferred.entry.contentId] + }) + expect(result.evictedContentIds).toEqual([ordinary.entry.contentId]) + + const forced = await evictSshRelayArtifactCache({ + cacheRoot: root, + maximumBytes: hard.logicalBytes, + protectedContentIds: [hard.entry.contentId], + preferredRetentionContentIds: [preferred.entry.contentId] + }) + expect(forced.evictedContentIds).toEqual([preferred.entry.contentId]) + expect(forced.finalBytes).toBe(hard.logicalBytes) + }) + + it('preserves malformed, live, and remote-host lease state as ambiguous', async () => { + const root = await cacheRoot() + const values = await Promise.all( + ['a', 'b', 'c'].map((character) => + createSshRelayArtifactCacheEvictionFixture({ cacheRoot: root, character }) + ) + ) + const owners = [ + '{not-json', + JSON.stringify({ + schemaVersion: 1, + contentId: values[1].entry.contentId, + token: 'd'.repeat(32), + hostname: hostname(), + pid: process.pid, + acquiredAtMs: 0, + heartbeatAtMs: 0 + }), + JSON.stringify({ + schemaVersion: 1, + contentId: values[2].entry.contentId, + token: 'e'.repeat(32), + hostname: 'another-host', + pid: 2_147_483_647, + acquiredAtMs: 0, + heartbeatAtMs: 0 + }) + ] + for (const [index, value] of values.entries()) { + const token = index === 0 ? 'f'.repeat(32) : index === 1 ? 'd'.repeat(32) : 'e'.repeat(32) + const leasePath = join(root, 'in-use', value.entry.contentId.slice(7), `${token}.lease`) + await mkdir(leasePath, { recursive: true }) + await writeFile(join(leasePath, 'owner.json'), `${owners[index]}\n`) + } + const result = await evictSshRelayArtifactCache({ cacheRoot: root, maximumBytes: 0 }) + expect(result.evictedContentIds).toEqual([]) + expect(result.finalBytes).toBe(values.reduce((total, value) => total + value.logicalBytes, 0)) + }) + + it('reclaims only a confirmed stale same-host dead lease before eviction', async () => { + const root = await cacheRoot() + const value = await createSshRelayArtifactCacheEvictionFixture({ + cacheRoot: root, + character: 'a' + }) + const token = 'd'.repeat(32) + const leasePath = join(root, 'in-use', value.entry.contentId.slice(7), `${token}.lease`) + await mkdir(leasePath, { recursive: true }) + await writeFile( + join(leasePath, 'owner.json'), + `${JSON.stringify({ + schemaVersion: 1, + contentId: value.entry.contentId, + token, + hostname: hostname(), + pid: 2_147_483_647, + acquiredAtMs: 0, + heartbeatAtMs: 0 + })}\n` + ) + const result = await evictSshRelayArtifactCache({ cacheRoot: root, maximumBytes: 0 }) + expect(result.evictedContentIds).toEqual([value.entry.contentId]) + await expect(stat(leasePath)).rejects.toMatchObject({ code: 'ENOENT' }) + }) + + it('preserves staging and safely settles cancellation behind a content lock', async () => { + const root = await cacheRoot() + const value = await createSshRelayArtifactCacheEvictionFixture({ + cacheRoot: root, + character: 'a' + }) + await mkdir(`${value.entry.entryPath}.pending-${'f'.repeat(32)}`) + const staged = await evictSshRelayArtifactCache({ cacheRoot: root, maximumBytes: 0 }) + expect(staged.evictedContentIds).toEqual([]) + + await rm(`${value.entry.entryPath}.pending-${'f'.repeat(32)}`, { recursive: true }) + const lock = await acquireSshRelayArtifactCacheLock({ + cacheRoot: root, + contentId: value.entry.contentId + }) + const controller = new AbortController() + const pending = evictSshRelayArtifactCache({ + cacheRoot: root, + maximumBytes: 0, + signal: controller.signal + }) + controller.abort(new Error('cancel eviction wait')) + await expect(pending).rejects.toThrow(/cancel eviction wait/i) + await lock.release() + await expect(stat(value.entry.entryPath)).resolves.toMatchObject({ + isDirectory: expect.any(Function) + }) + }) + + it('serializes concurrent evictors and protects an unsafe linked tree', async () => { + const root = await cacheRoot() + const first = await createSshRelayArtifactCacheEvictionFixture({ + cacheRoot: root, + character: 'a' + }) + const second = await createSshRelayArtifactCacheEvictionFixture({ + cacheRoot: root, + character: 'b' + }) + const [left, right] = await Promise.all([ + evictSshRelayArtifactCache({ cacheRoot: root, maximumBytes: second.logicalBytes }), + evictSshRelayArtifactCache({ cacheRoot: root, maximumBytes: second.logicalBytes }) + ]) + expect(new Set([...left.evictedContentIds, ...right.evictedContentIds])).toEqual( + new Set([first.entry.contentId]) + ) + + const unsafe = await createSshRelayArtifactCacheEvictionFixture({ + cacheRoot: root, + character: 'c' + }) + await symlink( + join(unsafe.entry.runtimeRoot, 'relay.js'), + join(unsafe.entry.runtimeRoot, 'link') + ) + const result = await evictSshRelayArtifactCache({ cacheRoot: root, maximumBytes: 0 }) + expect(result.evictedContentIds).not.toContain(unsafe.entry.contentId) + expect(await readFile(join(unsafe.entry.runtimeRoot, 'relay.js'))).toHaveLength(33) + }) +}) diff --git a/src/main/ssh/ssh-relay-artifact-cache-eviction.ts b/src/main/ssh/ssh-relay-artifact-cache-eviction.ts new file mode 100644 index 00000000000..6d8f703f32b --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-eviction.ts @@ -0,0 +1,293 @@ +import { randomBytes } from 'node:crypto' +import { lstat, mkdir, readdir, rename, rm } from 'node:fs/promises' +import { join, resolve } from 'node:path' + +import { + measureSshRelayArtifactCacheEntryLogicalBytes, + SSH_RELAY_ARTIFACT_CACHE_MAXIMUM_MEMBERS_PER_ENTRY +} from './ssh-relay-artifact-cache-entry-accounting' +import { inspectSshRelayArtifactCacheInUse } from './ssh-relay-artifact-cache-in-use-inspection' +import { acquireSshRelayArtifactCacheLock } from './ssh-relay-artifact-cache-lock' +import { sshRelayArtifactCacheErrorCode } from './ssh-relay-artifact-cache-lock-record' +import { + readSshRelayArtifactCacheRecency, + removeSshRelayArtifactCacheRecency +} from './ssh-relay-artifact-cache-recency' +import type { SshRelayDigest } from './ssh-relay-runtime-identity' + +const MAXIMUM_BYTES = 2 * 1024 * 1024 * 1024 +const TRANSACTION_TIMEOUT_MS = 5 * 60_000 +const CONTENT_HEX = /^[0-9a-f]{64}$/ +const CONTENT_ID = /^sha256:[0-9a-f]{64}$/ +const EVICTION_LOCK_ID = `sha256:${'0'.repeat(64)}` as SshRelayDigest + +type CacheCandidate = { + contentId: SshRelayDigest + contentHex: string + entryPath: string + logicalBytes: number + usedAtMs: number + state: 'idle' | 'active' | 'ambiguous' | 'staging' +} + +function exactMaximumBytes(value: number): number { + if (!Number.isSafeInteger(value) || value < 0) { + throw new Error('SSH relay artifact cache maximum bytes must be a non-negative safe integer') + } + return value +} + +function exactContentSet(values: readonly SshRelayDigest[] | undefined): Set { + const result = new Set() + for (const value of values ?? []) { + if (!CONTENT_ID.test(value)) { + throw new Error('SSH relay artifact cache protected content ID must be an exact digest') + } + result.add(value) + } + return result +} + +async function hasSameContentStaging(entries: string, contentHex: string): Promise { + const pattern = new RegExp(`^${contentHex}\\.pending-[0-9a-f]{32}$`) + return (await readdir(entries)).some((name) => pattern.test(name)) +} + +async function inspectCandidate({ + cacheRoot, + entries, + contentHex, + signal +}: { + cacheRoot: string + entries: string + contentHex: string + signal: AbortSignal +}): Promise { + const contentId = `sha256:${contentHex}` as SshRelayDigest + const lock = await acquireSshRelayArtifactCacheLock({ cacheRoot, contentId, signal }) + try { + signal.throwIfAborted() + const entryPath = join(entries, contentHex) + const root = await lstat(entryPath).catch((error) => { + if (sshRelayArtifactCacheErrorCode(error) === 'ENOENT') { + return null + } + throw error + }) + if (!root) { + return null + } + const logicalBytes = await measureSshRelayArtifactCacheEntryLogicalBytes(entryPath, signal) + if (!root.isDirectory() || root.isSymbolicLink() || logicalBytes === null) { + return { + contentId, + contentHex, + entryPath, + logicalBytes: 0, + usedAtMs: Math.trunc(root.mtimeMs), + state: 'ambiguous' + } + } + const recency = await readSshRelayArtifactCacheRecency({ + cacheRoot, + contentId, + initialUsedAtMs: root.mtimeMs + }) + const inUse = await inspectSshRelayArtifactCacheInUse({ cacheRoot, contentId, signal }) + const staging = await hasSameContentStaging(entries, contentHex) + return { + contentId, + contentHex, + entryPath, + logicalBytes, + usedAtMs: recency.kind === 'known' ? recency.usedAtMs : Math.trunc(root.mtimeMs), + state: staging ? 'staging' : recency.kind === 'ambiguous' ? 'ambiguous' : inUse + } + } finally { + await lock.release() + } +} + +function candidateOrder( + preferred: ReadonlySet +): (left: CacheCandidate, right: CacheCandidate) => number { + return (left, right) => { + const preference = + Number(preferred.has(left.contentId)) - Number(preferred.has(right.contentId)) + return ( + preference || + left.usedAtMs - right.usedAtMs || + left.contentHex.localeCompare(right.contentHex) + ) + } +} + +async function deleteIdleCandidate({ + cacheRoot, + entries, + candidate, + signal +}: { + cacheRoot: string + entries: string + candidate: CacheCandidate + signal: AbortSignal +}): Promise { + const lock = await acquireSshRelayArtifactCacheLock({ + cacheRoot, + contentId: candidate.contentId, + signal + }) + try { + signal.throwIfAborted() + if (await hasSameContentStaging(entries, candidate.contentHex)) { + return null + } + const root = await lstat(candidate.entryPath).catch(() => null) + if (!root?.isDirectory() || root.isSymbolicLink()) { + return null + } + const recency = await readSshRelayArtifactCacheRecency({ + cacheRoot, + contentId: candidate.contentId, + initialUsedAtMs: root.mtimeMs + }) + // Why: a use that races the initial LRU scan wins; a later transaction can reconsider it. + if (recency.kind !== 'known' || recency.usedAtMs !== candidate.usedAtMs) { + return null + } + const inUse = await inspectSshRelayArtifactCacheInUse({ + cacheRoot, + contentId: candidate.contentId, + signal + }) + if (inUse !== 'idle') { + return null + } + const bytes = await measureSshRelayArtifactCacheEntryLogicalBytes(candidate.entryPath, signal) + if (bytes === null) { + return null + } + const tombstone = `${candidate.entryPath}.evicting-${randomBytes(16).toString('hex')}` + // Why: only an exact final entry is renamed unselectable before owned recursive deletion. + await rename(candidate.entryPath, tombstone) + await rm(tombstone, { recursive: true, force: true }) + await Promise.all([ + removeSshRelayArtifactCacheRecency(cacheRoot, candidate.contentId).catch(() => {}), + rm(resolve(cacheRoot, 'in-use', candidate.contentHex), { + recursive: true, + force: true + }).catch(() => {}) + ]) + return bytes + } catch (error) { + if (sshRelayArtifactCacheErrorCode(error) === 'ENOENT') { + return null + } + throw error + } finally { + await lock.release() + } +} + +export type SshRelayArtifactCacheEvictionResult = { + initialBytes: number + finalBytes: number + reclaimedBytes: number + evictedContentIds: SshRelayDigest[] + blockedContentIds: SshRelayDigest[] + accountingComplete: boolean +} + +export async function evictSshRelayArtifactCache({ + cacheRoot, + maximumBytes = MAXIMUM_BYTES, + protectedContentIds, + preferredRetentionContentIds, + signal +}: { + cacheRoot: string + maximumBytes?: number + protectedContentIds?: readonly SshRelayDigest[] + preferredRetentionContentIds?: readonly SshRelayDigest[] + signal?: AbortSignal +}): Promise { + const limit = exactMaximumBytes(maximumBytes) + const protectedSet = exactContentSet(protectedContentIds) + const preferredSet = exactContentSet(preferredRetentionContentIds) + const timeout = AbortSignal.timeout(TRANSACTION_TIMEOUT_MS) + const activeSignal = signal ? AbortSignal.any([signal, timeout]) : timeout + activeSignal.throwIfAborted() + const root = resolve(cacheRoot) + const entries = join(root, 'entries') + await mkdir(entries, { recursive: true, mode: 0o700 }) + // Why: the dedicated namespace serializes LRU transactions without reserving a content digest. + const transaction = await acquireSshRelayArtifactCacheLock({ + cacheRoot: join(root, 'eviction-transaction'), + contentId: EVICTION_LOCK_ID, + signal: activeSignal + }) + try { + const candidates: CacheCandidate[] = [] + for (const name of (await readdir(entries)).sort()) { + activeSignal.throwIfAborted() + if (!CONTENT_HEX.test(name)) { + continue + } + const candidate = await inspectCandidate({ + cacheRoot: root, + entries, + contentHex: name, + signal: activeSignal + }) + if (candidate) { + candidates.push(candidate) + } + } + const accountingComplete = candidates.every((candidate) => candidate.logicalBytes > 0) + const initialBytes = candidates.reduce((total, candidate) => total + candidate.logicalBytes, 0) + let finalBytes = initialBytes + const blocked = candidates + .filter((candidate) => candidate.state !== 'idle' || protectedSet.has(candidate.contentId)) + .map((candidate) => candidate.contentId) + const evictable = candidates + .filter((candidate) => candidate.state === 'idle' && !protectedSet.has(candidate.contentId)) + .sort(candidateOrder(preferredSet)) + const evictedContentIds: SshRelayDigest[] = [] + for (const candidate of evictable) { + if (finalBytes <= limit) { + break + } + activeSignal.throwIfAborted() + const reclaimed = await deleteIdleCandidate({ + cacheRoot: root, + entries, + candidate, + signal: activeSignal + }) + if (reclaimed === null) { + blocked.push(candidate.contentId) + continue + } + finalBytes -= reclaimed + evictedContentIds.push(candidate.contentId) + } + return { + initialBytes, + finalBytes, + reclaimedBytes: initialBytes - finalBytes, + evictedContentIds, + blockedContentIds: [...new Set(blocked)], + accountingComplete + } + } finally { + await transaction.release() + } +} + +export const SSH_RELAY_ARTIFACT_CACHE_EVICTION_LIMITS = Object.freeze({ + maximumBytes: MAXIMUM_BYTES, + transactionTimeoutMs: TRANSACTION_TIMEOUT_MS, + maximumMembersPerEntry: SSH_RELAY_ARTIFACT_CACHE_MAXIMUM_MEMBERS_PER_ENTRY +}) diff --git a/src/main/ssh/ssh-relay-artifact-cache-in-use-inspection.ts b/src/main/ssh/ssh-relay-artifact-cache-in-use-inspection.ts new file mode 100644 index 00000000000..7e969cf9b55 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-in-use-inspection.ts @@ -0,0 +1,87 @@ +import { randomBytes } from 'node:crypto' +import { lstat, readdir, rename, rm } from 'node:fs/promises' +import { hostname } from 'node:os' +import { join, resolve } from 'node:path' + +import { + readSshRelayArtifactCacheInUseOwner, + sameSshRelayArtifactCacheInUseDirectory +} from './ssh-relay-artifact-cache-in-use-record' +import { sshRelayArtifactCacheErrorCode } from './ssh-relay-artifact-cache-lock-record' +import type { SshRelayDigest } from './ssh-relay-runtime-identity' + +export const SSH_RELAY_ARTIFACT_CACHE_IN_USE_STALE_AFTER_MS = 30_000 +const CONTENT_ID = /^sha256:([0-9a-f]{64})$/ +const LEASE_NAME = /^([0-9a-f]{32})\.lease$/ + +function exactContentHex(contentId: SshRelayDigest): string { + const match = CONTENT_ID.exec(contentId) + if (!match) { + throw new Error('SSH relay artifact cache in-use content ID must be an exact lowercase digest') + } + return match[1] +} + +function localProcessIsAlive(pid: number): boolean { + try { + process.kill(pid, 0) + return true + } catch (error) { + return sshRelayArtifactCacheErrorCode(error) !== 'ESRCH' + } +} + +export async function inspectSshRelayArtifactCacheInUse({ + cacheRoot, + contentId, + signal +}: { + cacheRoot: string + contentId: SshRelayDigest + signal: AbortSignal +}): Promise<'idle' | 'active' | 'ambiguous'> { + const parent = resolve(cacheRoot, 'in-use', exactContentHex(contentId)) + let names: string[] + try { + names = await readdir(parent) + } catch (error) { + return sshRelayArtifactCacheErrorCode(error) === 'ENOENT' ? 'idle' : 'ambiguous' + } + for (const name of names) { + signal.throwIfAborted() + const match = LEASE_NAME.exec(name) + if (!match) { + return 'ambiguous' + } + const token = match[1] + const leasePath = join(parent, name) + const directory = await lstat(leasePath, { bigint: true }).catch(() => null) + const owner = await readSshRelayArtifactCacheInUseOwner(leasePath, contentId, token) + if (!directory?.isDirectory() || directory.isSymbolicLink() || !owner) { + return 'ambiguous' + } + if (Date.now() - owner.heartbeatAtMs < SSH_RELAY_ARTIFACT_CACHE_IN_USE_STALE_AFTER_MS) { + return 'active' + } + if (owner.hostname !== hostname()) { + return 'ambiguous' + } + if (localProcessIsAlive(owner.pid)) { + return 'active' + } + const confirmed = await readSshRelayArtifactCacheInUseOwner(leasePath, contentId, token) + const current = await lstat(leasePath, { bigint: true }).catch(() => null) + if ( + !confirmed || + confirmed.heartbeatAtMs !== owner.heartbeatAtMs || + !current || + !sameSshRelayArtifactCacheInUseDirectory(directory, current) + ) { + return 'ambiguous' + } + const tombstone = `${leasePath}.stale-${randomBytes(16).toString('hex')}` + await rename(leasePath, tombstone) + await rm(tombstone, { recursive: true, force: true }) + } + return 'idle' +} diff --git a/src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts b/src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts new file mode 100644 index 00000000000..a92ed8cb738 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts @@ -0,0 +1,178 @@ +import { spawn } from 'node:child_process' +import { mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' + +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { createSshRelayArtifactCacheEvictionFixture } from './ssh-relay-artifact-cache-eviction-fixture' +import { evictSshRelayArtifactCache } from './ssh-relay-artifact-cache-eviction' +import { + acquireSshRelayArtifactCacheInUseLease, + sshRelayArtifactCacheInUseLeasePath, + SSH_RELAY_ARTIFACT_CACHE_IN_USE_LIMITS +} from './ssh-relay-artifact-cache-in-use-lease' +import type { SshRelayDigest } from './ssh-relay-runtime-identity' + +const temporaryDirectories: string[] = [] +const leaseModuleUrl = pathToFileURL( + join(import.meta.dirname, 'ssh-relay-artifact-cache-in-use-lease.ts') +).href + +async function cacheRoot(): Promise { + const root = await mkdtemp(join(tmpdir(), 'orca-relay-cache-in-use-')) + temporaryDirectories.push(root) + return join(root, 'cache') +} + +afterEach(async () => { + vi.useRealTimers() + await Promise.all( + temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true })) + ) +}) + +describe('SSH relay artifact cache in-use lease', () => { + it('pins exact paths and bounded heartbeat/stale timing', async () => { + const root = await cacheRoot() + const { entry } = await createSshRelayArtifactCacheEvictionFixture({ + cacheRoot: root, + character: 'a' + }) + const token = 'b'.repeat(32) + expect(sshRelayArtifactCacheInUseLeasePath(root, entry.contentId, token)).toBe( + join(root, 'in-use', 'a'.repeat(64), `${token}.lease`) + ) + expect(SSH_RELAY_ARTIFACT_CACHE_IN_USE_LIMITS).toEqual({ + heartbeatIntervalMs: 5_000, + staleAfterMs: 30_000, + acquisitionTimeoutMs: 120_000 + }) + expect(Object.isFrozen(SSH_RELAY_ARTIFACT_CACHE_IN_USE_LIMITS)).toBe(true) + for (const invalid of [ + `sha256:${'A'.repeat(64)}`, + `sha256:${'a'.repeat(63)}`, + `${entry.contentId}/../escape` + ]) { + expect(() => + sshRelayArtifactCacheInUseLeasePath(root, invalid as SshRelayDigest, token) + ).toThrow(/content id/i) + } + expect(() => sshRelayArtifactCacheInUseLeasePath(root, entry.contentId, '../escape')).toThrow( + /token/i + ) + }) + + it('allows multiple references and heartbeats owner records independently', async () => { + vi.useFakeTimers() + vi.setSystemTime(100_000) + const root = await cacheRoot() + const { entry } = await createSshRelayArtifactCacheEvictionFixture({ + cacheRoot: root, + character: 'a' + }) + const first = await acquireSshRelayArtifactCacheInUseLease({ cacheRoot: root, entry }) + const second = await acquireSshRelayArtifactCacheInUseLease({ cacheRoot: root, entry }) + expect(second.token).not.toBe(first.token) + + await vi.advanceTimersByTimeAsync(5_000) + await first.assertOwned() + const owner = JSON.parse(await readFile(join(first.leasePath, 'owner.json'), 'utf8')) as { + heartbeatAtMs: number + } + expect(owner.heartbeatAtMs).toBe(105_000) + await Promise.all([first.release(), second.release()]) + expect(await readdir(join(root, 'recency', entry.contentId.slice(7)))).toHaveLength(1) + }) + + it('does not delete a displaced successor through owner release', async () => { + const root = await cacheRoot() + const { entry } = await createSshRelayArtifactCacheEvictionFixture({ + cacheRoot: root, + character: 'a' + }) + const lease = await acquireSshRelayArtifactCacheInUseLease({ cacheRoot: root, entry }) + const successor = { token: 'c'.repeat(32) } + const owner = JSON.parse(await readFile(join(lease.leasePath, 'owner.json'), 'utf8')) as object + await writeFile( + join(lease.leasePath, 'owner.json'), + `${JSON.stringify({ ...owner, ...successor })}\n` + ) + + await expect(lease.assertOwned()).rejects.toThrow(/ownership/i) + await lease.release() + await expect(stat(lease.leasePath)).resolves.toMatchObject({ + isDirectory: expect.any(Function) + }) + }) + + it('makes a live lease visible to a real second process', async () => { + const root = await cacheRoot() + const { entry } = await createSshRelayArtifactCacheEvictionFixture({ + cacheRoot: root, + character: 'a' + }) + const script = ` + import { registerHooks } from 'node:module' + registerHooks({ resolve(specifier, context, nextResolve) { + try { return nextResolve(specifier, context) } + catch (error) { if (specifier.startsWith('.')) return nextResolve(specifier + '.ts', context); throw error } + } }) + const { acquireSshRelayArtifactCacheInUseLease } = await import(${JSON.stringify(leaseModuleUrl)}) + const entry = JSON.parse(process.env.ORCA_RELAY_CACHE_ENTRY) + const lease = await acquireSshRelayArtifactCacheInUseLease({ cacheRoot: process.env.ORCA_RELAY_CACHE_ROOT, entry }) + process.stdout.write(lease.leasePath + '\\n') + await new Promise(resolve => process.stdin.once('data', resolve)) + await lease.release() + ` + const child = spawn( + process.execPath, + [ + '--experimental-strip-types', + '--disable-warning=MODULE_TYPELESS_PACKAGE_JSON', + '--input-type=module', + '--eval', + script + ], + { + cwd: process.cwd(), + env: { + ...process.env, + ORCA_RELAY_CACHE_ROOT: root, + ORCA_RELAY_CACHE_ENTRY: JSON.stringify(entry) + }, + stdio: ['pipe', 'pipe', 'pipe'] + } + ) + let childError = '' + child.stderr.on('data', (bytes: Buffer) => (childError += bytes.toString('utf8'))) + const leasePath = await new Promise((resolve, reject) => { + child.once('error', reject) + child.stdout.once('data', (bytes: Buffer) => resolve(bytes.toString('utf8').trim())) + child.once('exit', (code) => reject(new Error(`Lease child exited ${code}: ${childError}`))) + }) + await expect(stat(leasePath)).resolves.toMatchObject({ isDirectory: expect.any(Function) }) + const retained = await evictSshRelayArtifactCache({ cacheRoot: root, maximumBytes: 0 }) + expect(retained.evictedContentIds).toEqual([]) + child.stdin.end('release\n') + const code = await new Promise((resolve) => child.once('exit', resolve)) + expect(code).toBe(0) + }) + + it('rejects a reference when the exact final entry no longer exists', async () => { + const root = await cacheRoot() + const { entry } = await createSshRelayArtifactCacheEvictionFixture({ + cacheRoot: root, + character: 'a' + }) + await rm(entry.entryPath, { recursive: true }) + await expect( + acquireSshRelayArtifactCacheInUseLease({ cacheRoot: root, entry }) + ).rejects.toThrow(/entry/i) + await mkdir(join(root, 'in-use'), { recursive: true }) + expect(sshRelayArtifactCacheInUseLeasePath(root, entry.contentId, 'd'.repeat(32))).toContain( + join('in-use', 'a'.repeat(64)) + ) + }) +}) diff --git a/src/main/ssh/ssh-relay-artifact-cache-in-use-lease.ts b/src/main/ssh/ssh-relay-artifact-cache-in-use-lease.ts new file mode 100644 index 00000000000..4a1603d01c5 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-in-use-lease.ts @@ -0,0 +1,258 @@ +import { randomBytes } from 'node:crypto' +import { lstat, mkdir, open, rename, rm, type FileHandle } from 'node:fs/promises' +import { hostname } from 'node:os' +import { dirname, join, resolve } from 'node:path' + +import type { SshRelayArtifactCacheEntry } from './ssh-relay-artifact-cache-entry-verification' +import { SSH_RELAY_ARTIFACT_CACHE_IN_USE_STALE_AFTER_MS } from './ssh-relay-artifact-cache-in-use-inspection' +import { + readSshRelayArtifactCacheInUseOwner, + sameSshRelayArtifactCacheInUseDirectory, + sshRelayArtifactCacheInUseOwnerBytes, + SSH_RELAY_ARTIFACT_CACHE_IN_USE_SCHEMA_VERSION, + type SshRelayArtifactCacheInUseDirectoryIdentity, + type SshRelayArtifactCacheInUseOwner +} from './ssh-relay-artifact-cache-in-use-record' +import { acquireSshRelayArtifactCacheLock } from './ssh-relay-artifact-cache-lock' +import { sshRelayArtifactCacheErrorCode } from './ssh-relay-artifact-cache-lock-record' +import { recordSshRelayArtifactCacheRecency } from './ssh-relay-artifact-cache-recency' +import type { SshRelayDigest } from './ssh-relay-runtime-identity' + +const HEARTBEAT_INTERVAL_MS = 5_000 +const ACQUISITION_TIMEOUT_MS = 120_000 +const CONTENT_ID = /^sha256:([0-9a-f]{64})$/ +const TOKEN = /^[0-9a-f]{32}$/ + +function exactContentHex(contentId: SshRelayDigest): string { + const match = CONTENT_ID.exec(contentId) + if (!match) { + throw new Error('SSH relay artifact cache in-use content ID must be an exact lowercase digest') + } + return match[1] +} + +export function sshRelayArtifactCacheInUseLeasePath( + cacheRoot: string, + contentId: SshRelayDigest, + token: string +): string { + if (!TOKEN.test(token)) { + throw new Error('SSH relay artifact cache in-use token must be exact lowercase hex') + } + return resolve(cacheRoot, 'in-use', exactContentHex(contentId), `${token}.lease`) +} + +async function writeCompleteOwner(handle: FileHandle, owner: SshRelayArtifactCacheInUseOwner) { + const bytes = sshRelayArtifactCacheInUseOwnerBytes(owner) + let offset = 0 + while (offset < bytes.length) { + const { bytesWritten } = await handle.write(bytes, offset, bytes.length - offset, offset) + if (bytesWritten <= 0) { + throw new Error('SSH relay artifact cache in-use heartbeat could not be persisted') + } + offset += bytesWritten + } + await handle.truncate(bytes.length) + await handle.sync() +} + +class SshRelayArtifactCacheInUseLeaseOwner { + readonly leasePath: string + readonly token: string + private readonly cacheRoot: string + private readonly contentId: SshRelayDigest + private readonly directory: SshRelayArtifactCacheInUseDirectoryIdentity + private readonly ownerHandle: FileHandle + private readonly owner: SshRelayArtifactCacheInUseOwner + private heartbeat: Promise = Promise.resolve() + private heartbeatError: unknown + private heartbeatTimer: NodeJS.Timeout | undefined + private released = false + + constructor(options: { + cacheRoot: string + leasePath: string + contentId: SshRelayDigest + token: string + directory: SshRelayArtifactCacheInUseDirectoryIdentity + ownerHandle: FileHandle + owner: SshRelayArtifactCacheInUseOwner + }) { + this.cacheRoot = options.cacheRoot + this.leasePath = options.leasePath + this.contentId = options.contentId + this.token = options.token + this.directory = options.directory + this.ownerHandle = options.ownerHandle + this.owner = options.owner + this.heartbeatTimer = setInterval(() => this.queueHeartbeat(), HEARTBEAT_INTERVAL_MS) + this.heartbeatTimer.unref() + } + + private queueHeartbeat(): void { + this.heartbeat = this.heartbeat.then(async () => { + if (this.released || this.heartbeatError) { + return + } + try { + await this.assertPathOwnership() + this.owner.heartbeatAtMs = Date.now() + await writeCompleteOwner(this.ownerHandle, this.owner) + } catch (error) { + this.heartbeatError = error + } + }) + } + + private async assertPathOwnership(): Promise { + const metadata = await lstat(this.leasePath, { bigint: true }).catch(() => null) + const current = await readSshRelayArtifactCacheInUseOwner( + this.leasePath, + this.contentId, + this.token + ) + if ( + !metadata?.isDirectory() || + !sameSshRelayArtifactCacheInUseDirectory(this.directory, metadata) || + current?.token !== this.token + ) { + throw new Error('SSH relay artifact cache in-use ownership was displaced') + } + } + + async assertOwned(): Promise { + await this.heartbeat + if (this.released || this.heartbeatError) { + throw new Error('SSH relay artifact cache in-use ownership is no longer active', { + cause: this.heartbeatError + }) + } + await this.assertPathOwnership() + } + + async release(): Promise { + if (this.released) { + return + } + this.released = true + if (this.heartbeatTimer) { + clearInterval(this.heartbeatTimer) + } + this.heartbeatTimer = undefined + await this.heartbeat + let owned = false + try { + await this.assertPathOwnership() + owned = true + } catch { + // A displaced lease must never remove another process's visible reference. + } + await this.ownerHandle.close() + if (!owned) { + return + } + const tombstone = `${this.leasePath}.released-${this.token}` + await rename(this.leasePath, tombstone) + await rm(tombstone, { recursive: true, force: true }) + await recordSshRelayArtifactCacheRecency({ + cacheRoot: this.cacheRoot, + contentId: this.contentId + }) + } +} + +export type SshRelayArtifactCacheInUseLease = Pick< + SshRelayArtifactCacheInUseLeaseOwner, + 'leasePath' | 'token' | 'assertOwned' | 'release' +> + +async function createLease( + cacheRoot: string, + contentId: SshRelayDigest +): Promise { + const token = randomBytes(16).toString('hex') + const leasePath = sshRelayArtifactCacheInUseLeasePath(cacheRoot, contentId, token) + await mkdir(dirname(leasePath), { recursive: true, mode: 0o700 }) + await mkdir(leasePath, { mode: 0o700 }) + let handle: FileHandle | undefined + try { + handle = await open(join(leasePath, 'owner.json'), 'wx+', 0o600) + const now = Date.now() + const owner: SshRelayArtifactCacheInUseOwner = { + schemaVersion: SSH_RELAY_ARTIFACT_CACHE_IN_USE_SCHEMA_VERSION, + contentId, + token, + hostname: hostname(), + pid: process.pid, + acquiredAtMs: now, + heartbeatAtMs: now + } + await writeCompleteOwner(handle, owner) + const directory = await lstat(leasePath, { bigint: true }) + return new SshRelayArtifactCacheInUseLeaseOwner({ + cacheRoot, + leasePath, + contentId, + token, + directory, + ownerHandle: handle, + owner + }) + } catch (error) { + await handle?.close().catch(() => {}) + await rm(leasePath, { recursive: true, force: true }).catch(() => {}) + throw error + } +} + +export async function acquireSshRelayArtifactCacheInUseLease({ + cacheRoot, + entry, + signal +}: { + cacheRoot: string + entry: SshRelayArtifactCacheEntry + signal?: AbortSignal +}): Promise { + const timeout = AbortSignal.timeout(ACQUISITION_TIMEOUT_MS) + const activeSignal = signal ? AbortSignal.any([signal, timeout]) : timeout + activeSignal.throwIfAborted() + const expectedPath = resolve(cacheRoot, 'entries', exactContentHex(entry.contentId)) + if (resolve(entry.entryPath) !== expectedPath) { + throw new Error( + 'SSH relay artifact cache in-use entry path disagrees with its content identity' + ) + } + const lock = await acquireSshRelayArtifactCacheLock({ + cacheRoot, + contentId: entry.contentId, + signal: activeSignal + }) + try { + const metadata = await lstat(expectedPath).catch((error) => { + if (sshRelayArtifactCacheErrorCode(error) === 'ENOENT') { + throw new Error('SSH relay artifact cache in-use entry no longer exists', { cause: error }) + } + throw error + }) + if (!metadata.isDirectory() || metadata.isSymbolicLink()) { + throw new Error('SSH relay artifact cache in-use entry is not a complete final directory') + } + const lease = await createLease(cacheRoot, entry.contentId) + try { + await recordSshRelayArtifactCacheRecency({ cacheRoot, contentId: entry.contentId }) + return lease + } catch (error) { + await lease.release().catch(() => {}) + throw error + } + } finally { + await lock.release() + } +} + +export const SSH_RELAY_ARTIFACT_CACHE_IN_USE_LIMITS = Object.freeze({ + heartbeatIntervalMs: HEARTBEAT_INTERVAL_MS, + staleAfterMs: SSH_RELAY_ARTIFACT_CACHE_IN_USE_STALE_AFTER_MS, + acquisitionTimeoutMs: ACQUISITION_TIMEOUT_MS +}) diff --git a/src/main/ssh/ssh-relay-artifact-cache-in-use-record.ts b/src/main/ssh/ssh-relay-artifact-cache-in-use-record.ts new file mode 100644 index 00000000000..c1a83d905fd --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-in-use-record.ts @@ -0,0 +1,113 @@ +import { lstat, open } from 'node:fs/promises' +import { join } from 'node:path' + +import { sshRelayArtifactCacheErrorCode } from './ssh-relay-artifact-cache-lock-record' +import type { SshRelayDigest } from './ssh-relay-runtime-identity' + +export const SSH_RELAY_ARTIFACT_CACHE_IN_USE_SCHEMA_VERSION = 1 +const MAXIMUM_RECORD_BYTES = 16 * 1024 + +export type SshRelayArtifactCacheInUseOwner = { + schemaVersion: typeof SSH_RELAY_ARTIFACT_CACHE_IN_USE_SCHEMA_VERSION + contentId: SshRelayDigest + token: string + hostname: string + pid: number + acquiredAtMs: number + heartbeatAtMs: number +} + +export type SshRelayArtifactCacheInUseDirectoryIdentity = { dev: bigint; ino: bigint } + +function validOwner( + value: unknown, + contentId: SshRelayDigest, + token: string +): value is SshRelayArtifactCacheInUseOwner { + if (!value || typeof value !== 'object') { + return false + } + const owner = value as Partial + return ( + owner.schemaVersion === SSH_RELAY_ARTIFACT_CACHE_IN_USE_SCHEMA_VERSION && + owner.contentId === contentId && + owner.token === token && + /^[0-9a-f]{32}$/.test(owner.token) && + typeof owner.hostname === 'string' && + owner.hostname.length > 0 && + typeof owner.pid === 'number' && + Number.isSafeInteger(owner.pid) && + owner.pid > 0 && + typeof owner.acquiredAtMs === 'number' && + Number.isSafeInteger(owner.acquiredAtMs) && + owner.acquiredAtMs >= 0 && + typeof owner.heartbeatAtMs === 'number' && + Number.isSafeInteger(owner.heartbeatAtMs) && + owner.heartbeatAtMs >= owner.acquiredAtMs + ) +} + +export function sshRelayArtifactCacheInUseOwnerBytes( + owner: SshRelayArtifactCacheInUseOwner +): Buffer { + return Buffer.from(`${JSON.stringify(owner)}\n`, 'utf8') +} + +export async function readSshRelayArtifactCacheInUseOwner( + leasePath: string, + contentId: SshRelayDigest, + token: string +): Promise { + const path = join(leasePath, 'owner.json') + let metadata + try { + metadata = await lstat(path, { bigint: true }) + } catch (error) { + if (sshRelayArtifactCacheErrorCode(error) === 'ENOENT') { + return null + } + throw error + } + if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAXIMUM_RECORD_BYTES) { + return null + } + const handle = await open(path, 'r') + try { + const opened = await handle.stat({ bigint: true }) + if ( + !opened.isFile() || + opened.dev !== metadata.dev || + opened.ino !== metadata.ino || + opened.size !== metadata.size + ) { + return null + } + const bytes = Buffer.alloc(Number(opened.size)) + const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0) + const after = await handle.stat({ bigint: true }) + if ( + bytesRead !== bytes.length || + after.size !== opened.size || + after.mtimeNs !== opened.mtimeNs || + after.ctimeNs !== opened.ctimeNs + ) { + return null + } + let input: unknown + try { + input = JSON.parse(bytes.toString('utf8')) + } catch { + return null + } + return validOwner(input, contentId, token) ? input : null + } finally { + await handle.close() + } +} + +export function sameSshRelayArtifactCacheInUseDirectory( + left: SshRelayArtifactCacheInUseDirectoryIdentity, + right: SshRelayArtifactCacheInUseDirectoryIdentity +): boolean { + return left.dev === right.dev && left.ino === right.ino +} diff --git a/src/main/ssh/ssh-relay-artifact-cache-recency.ts b/src/main/ssh/ssh-relay-artifact-cache-recency.ts new file mode 100644 index 00000000000..9d572f84f2d --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-recency.ts @@ -0,0 +1,113 @@ +import { randomBytes } from 'node:crypto' +import { lstat, mkdir, open, readdir, rm } from 'node:fs/promises' +import { join, resolve } from 'node:path' + +import { sshRelayArtifactCacheErrorCode } from './ssh-relay-artifact-cache-lock-record' +import type { SshRelayDigest } from './ssh-relay-runtime-identity' + +const CONTENT_ID = /^sha256:([0-9a-f]{64})$/ +const RECORD_NAME = /^(\d{16})-([0-9a-f]{32})\.used$/ + +function exactContentHex(contentId: SshRelayDigest): string { + const match = CONTENT_ID.exec(contentId) + if (!match) { + throw new Error('SSH relay artifact cache recency content ID must be an exact lowercase digest') + } + return match[1] +} + +function exactUsedAtMs(value: number): number { + if (!Number.isSafeInteger(value) || value < 0) { + throw new Error('SSH relay artifact cache recency time must be a non-negative safe integer') + } + return value +} + +function recencyDirectory(cacheRoot: string, contentId: SshRelayDigest): string { + return resolve(cacheRoot, 'recency', exactContentHex(contentId)) +} + +export async function recordSshRelayArtifactCacheRecency({ + cacheRoot, + contentId, + usedAtMs = Date.now() +}: { + cacheRoot: string + contentId: SshRelayDigest + usedAtMs?: number +}): Promise { + const directory = recencyDirectory(cacheRoot, contentId) + await mkdir(directory, { recursive: true, mode: 0o700 }) + const timestamp = String(exactUsedAtMs(usedAtMs)).padStart(16, '0') + const path = join(directory, `${timestamp}-${randomBytes(16).toString('hex')}.used`) + const handle = await open(path, 'wx', 0o600) + try { + await handle.sync() + } finally { + await handle.close() + } + const names = await readdir(directory) + const valid = names.filter((name) => RECORD_NAME.test(name)).sort() + const newest = valid.at(-1) + await Promise.all( + valid + .filter((name) => name !== newest) + .map((name) => + rm(join(directory, name), { force: true }).catch((error) => { + if (sshRelayArtifactCacheErrorCode(error) !== 'ENOENT') { + throw error + } + }) + ) + ) +} + +export type SshRelayArtifactCacheRecency = + | { kind: 'known'; usedAtMs: number } + | { kind: 'ambiguous' } + +export async function readSshRelayArtifactCacheRecency({ + cacheRoot, + contentId, + initialUsedAtMs +}: { + cacheRoot: string + contentId: SshRelayDigest + initialUsedAtMs: number +}): Promise { + let names: string[] + try { + names = await readdir(recencyDirectory(cacheRoot, contentId)) + } catch (error) { + if (sshRelayArtifactCacheErrorCode(error) === 'ENOENT') { + return { kind: 'known', usedAtMs: exactUsedAtMs(Math.trunc(initialUsedAtMs)) } + } + return { kind: 'ambiguous' } + } + let usedAtMs = exactUsedAtMs(Math.trunc(initialUsedAtMs)) + for (const name of names) { + const match = RECORD_NAME.exec(name) + if (!match) { + return { kind: 'ambiguous' } + } + const metadata = await lstat(join(recencyDirectory(cacheRoot, contentId), name)).catch( + () => null + ) + if (!metadata?.isFile() || metadata.isSymbolicLink() || metadata.size !== 0) { + return { kind: 'ambiguous' } + } + const timestamp = Number(match[1]) + if (!Number.isSafeInteger(timestamp)) { + return { kind: 'ambiguous' } + } + usedAtMs = Math.max(usedAtMs, timestamp) + } + return { kind: 'known', usedAtMs } +} + +export async function removeSshRelayArtifactCacheRecency( + cacheRoot: string, + contentId: SshRelayDigest +): Promise { + await rm(recencyDirectory(cacheRoot, contentId), { recursive: true, force: true }) +} From 20728605330b0320e2ea6ec3c5a4380e68d1a0d8 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 22:03:02 -0400 Subject: [PATCH 176/267] docs: record native relay cache eviction evidence Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 22 ++-- ...github-release-implementation-checklist.md | 122 ++++++++++++++---- 2 files changed, 113 insertions(+), 31 deletions(-) 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 117702c0387..7fec2fe4857 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 @@ -9,12 +9,16 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — local lease/recency/ -eviction contracts are green; wiring full-size all-six native proof, 2026-07-15, Codex implementation -owner.** Fourteen focused contracts, workflow wiring, broad relay/release suites, typecheck, lint, and -static gates pass locally under `E-M5-ARTIFACT-CACHE-EVICTION-LOCAL-001` and -`E-M5-ARTIFACT-CACHE-EVICTION-CI-WIRING-LOCAL-001`; the package remains open until exact-head native -CI supplies all-six full-size retention/eviction metrics. Exact-head +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — bounded disconnected +cache gate closed; auditing the next packaged-manifest/cache-root boundary, 2026-07-15, Codex +implementation owner.** Exact-head run +[29464742446](https://github.com/stablyai/orca/actions/runs/29464742446) passes the lease/eviction +source contracts and exact full-size active-retention/release/eviction lifecycle on Linux, macOS, +and Windows, x64 and arm64, under `E-M5-ARTIFACT-CACHE-EVICTION-CI-001`. Retention is 11.14–47.88ms +and 0–1.99 MiB incremental RSS; eviction is 19.02–92.90ms and 0–2.08 MiB, reclaiming each exact +118.4–161.3 MB entry. PR Checks +[29464742361](https://github.com/stablyai/orca/actions/runs/29464742361) and Golden E2E +[29464742368](https://github.com/stablyai/orca/actions/runs/29464742368) pass. Prior exact-head run [29462394311](https://github.com/stablyai/orca/actions/runs/29462394311) passes all 17 cache-entry contracts and exact full-size cold/warm measurements on Linux, macOS, and Windows, x64 and arm64, under `E-M5-ARTIFACT-CACHE-ENTRY-CI-001`. Cold publication is 1.17–6.17s and 34.5–48.8 MiB @@ -318,12 +322,12 @@ production keys/environment/seed, and merge to `main` remain disconnected. and memory budgets under `E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001`; all-six native execution is closed under `E-M5-ARTIFACT-EXTRACTION-CI-001`. Packaged signature loading remains open. -- [ ] Add exclusive staging, atomic publication, quarantine, locking, and the 2 GiB cache policy. +- [x] Add exclusive staging, atomic publication, quarantine, locking, and the 2 GiB cache policy. Locking and immutable publication/lookup/quarantine are closed locally and on all six native clients under `E-M5-ARTIFACT-CACHE-LOCK-CI-001` and `E-M5-ARTIFACT-CACHE-ENTRY-CI-001`. In-use leases, recency, exact byte accounting, and bounded - eviction pass locally under `E-M5-ARTIFACT-CACHE-EVICTION-LOCAL-001`; all-six native/full-size - eviction evidence remains the only open part of this combined item. + eviction pass locally and on all six native/full-size clients under + `E-M5-ARTIFACT-CACHE-EVICTION-LOCAL-001` and `E-M5-ARTIFACT-CACHE-EVICTION-CI-001`. - [ ] Prove verified cached bytes can be transferred while the client is offline. - [ ] Preserve `ORCA_RELAY_PATH` behind the official-build trust boundary. 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 2bded9c897a..cc0594d2ddb 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 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: wire and prove disconnected in-use lease/recency/2 GiB eviction on all six native runners**. The purpose-named RED is recorded, 14 focused contracts and the 126-test artifact source suite are locally green, and the next gate is full-size eviction measurement plus workflow-contract and all-six native evidence. Keep every Electron, downloader, SSH, setting, tuple, publication, and default-path consumer disconnected.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — user authority covers end-to-end PR-contained implementation, commits, pushes, CI, rehearsals, and release writes; merging to `main` remains explicitly prohibited. The active uncommitted package implements only cross-process in-use leases, persistent recency, exact logical-byte accounting, and bounded eviction. It has no Electron, downloader, SSH, setting, tuple, publication, or default-path consumer. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the next disconnected packaged-manifest/cache-root boundary**. Exact-head `b79bcbd04` closes the bounded cache gate on all six native clients under E-M5-ARTIFACT-CACHE-EVICTION-CI-001. Before adding a consumer, audit official-build manifest/key loading, app-owned cache-root selection, proxy/certificate behavior, and downloader/cache orchestration; add purpose-named RED contracts for only the next coherent package. Keep SSH, settings, tuples, publication, and default behavior disconnected.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — user authority covers end-to-end PR-contained implementation, commits, pushes, CI, rehearsals, and release writes; merging to `main` remains explicitly prohibited. Commit `b79bcbd04` passes all six native source/full-size cache jobs, PR Checks, and Golden E2E. The artifact workflow is red only for the retained hosted Windows arm64 build-26200 versus required-26100 floor rejection after its primary proof. No Electron, downloader, SSH, setting, tuple, publication, or default-path consumer exists. 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) @@ -137,11 +137,15 @@ same change as the work it records. E-M5-PORTABLE-ARCHIVE-PARENT-LOCAL-001, and E-M5-PORTABLE-ARCHIVE-CI-001 prove the selected bounded archive, correction, all-six primary jobs, and both Linux oldest-userland supplements. Windows ZIP and upstream Node `.tar.xz` remain unchanged. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — checkpoint and collect - exact-head all-six Node 24 evidence for the disconnected content lock closed locally by - E-M5-ARTIFACT-CACHE-LOCK-LOCAL-001 and wired by - E-M5-ARTIFACT-CACHE-LOCK-CI-WIRING-LOCAL-001. Keep cache publication/quarantine/eviction, desktop - call sites, SSH behavior, mode wiring, tuple enablement, release publication, and defaults intact. +- Completed Work Package 4 cache gate: content locking, immutable entry publication/lookup, + corruption quarantine, cross-process in-use leases, persistent recency, exact logical-byte + accounting, and bounded 2 GiB eviction pass locally and on all six native clients under + E-M5-ARTIFACT-CACHE-LOCK-CI-001, E-M5-ARTIFACT-CACHE-ENTRY-CI-001, and + E-M5-ARTIFACT-CACHE-EVICTION-CI-001. No production consumer exists. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit the smallest + disconnected official-build packaged-manifest/key and app-owned cache-root boundary before adding + purpose-named RED contracts. Keep downloader orchestration, SSH behavior, mode wiring, tuple + enablement, release publication, and defaults intact until their separate gates. - 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 @@ -1106,7 +1110,11 @@ Suggested concrete modules: required. E-M5-ARTIFACT-CACHE-ENTRY-LOCAL-001 and E-M5-ARTIFACT-CACHE-ENTRY-CI-001 require a strict proof/archive/full-tree rehash on every disconnected lookup; warm-launch proof trust is a later remote-install contract and is not inferred here. -- [ ] Implement bounded eviction that never removes an in-use entry. +- [x] Implement bounded eviction that never removes an in-use entry. + E-M5-ARTIFACT-CACHE-EVICTION-LOCAL-001 and E-M5-ARTIFACT-CACHE-EVICTION-CI-001 prove + cross-process references, conservative ambiguous-owner retention, persistent recency, exact + logical-byte accounting, deterministic LRU, and full-size active retention/release/eviction on + all six native client tuples. - [ ] Handle read-only cache, permission failures, disk full, quota, inode exhaustion, cancellation, crash, corruption, and concurrent download deterministically. - [ ] Preserve `ORCA_RELAY_PATH` for explicit development/test use with an official-build safety @@ -1567,16 +1575,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 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-CI-001; E-M4-AGGREGATE-READBACK-CI-001 | -| 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 | Disconnected contracts green; protected signing/publication gates open | E-M4-RELEASE-DAG-CI-001; E-M4-AGGREGATE-READBACK-CI-001 | +| 4. Desktop resolver/cache | Verified download, extraction, cache, offline behavior | None/forced mode only | **In progress — 2026-07-15, Codex implementation owner; bounded cache green** | E-M5-ARTIFACT-CACHE-EVICTION-CI-001 | +| 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: @@ -13654,6 +13662,76 @@ src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts`. runner images/architectures, per-tuple retention/eviction timings and RSS, exact reclaimed bytes, cleanup results, and residual floor gaps under a new CI evidence ID. +### E-M5-ARTIFACT-CACHE-EVICTION-CI-001 — All-six native in-use and exact full-size eviction gate + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Exact source: commit `b79bcbd04e50b86186645bcba04d811671eb3035` on draft PR #8741. +- Artifact run: [29464742446](https://github.com/stablyai/orca/actions/runs/29464742446). +- Native source-contract results under Node 24.18.0: + - Linux x64/arm64 and macOS x64/arm64 each pass 67 files / 460 tests; + - Windows x64/arm64 each pass 68 files / 451 tests with 13 declared platform-inapplicable skips; + - all six include the 14 lease/recency/eviction contracts and complete deterministic artifact + build, archive/tree/Node/PTTY/watcher smoke, and exact full-size measurement before success. +- Exact primary jobs and runner identities: + - Linux x64 job [87515421304](https://github.com/stablyai/orca/actions/runs/29464742446/job/87515421304), + `ubuntu-24.04`, image `ubuntu24` `20260714.240.1`, X64 GitHub-hosted, passes in 2m46s; + - Linux arm64 job [87515421305](https://github.com/stablyai/orca/actions/runs/29464742446/job/87515421305), + `ubuntu-24.04-arm`, image `ubuntu24-arm64` `20260706.52.2`, ARM64 GitHub-hosted, passes in + 2m48s; + - macOS x64 job [87515421313](https://github.com/stablyai/orca/actions/runs/29464742446/job/87515421313), + `macos-15-intel`, image `macos15` `20260629.0276.1`, X64 GitHub-hosted, passes in 4m02s; + - macOS arm64 job [87515421317](https://github.com/stablyai/orca/actions/runs/29464742446/job/87515421317), + `macos-15`, image `macos15` `20260706.0213.1`, ARM64 GitHub-hosted, passes in 1m50s; + - Windows x64 job [87515421346](https://github.com/stablyai/orca/actions/runs/29464742446/job/87515421346), + `windows-2022`, image `win22` `20260714.244.1`, X64 GitHub-hosted, passes in 6m04s; + - Windows arm64 job [87515421308](https://github.com/stablyai/orca/actions/runs/29464742446/job/87515421308), + `windows-11-arm`, image `win11-arm64` `20260706.102.1`, ARM64 GitHub-hosted, passes in 9m57s. +- Exact full-size cache measurements (bytes are logical complete-entry bytes and are fully reclaimed): + + | Tuple | Retain ms | Retain RSS | Evict ms | Evict RSS | Initial/reclaimed bytes | + | ----------------- | --------: | ---------: | -------: | --------: | ----------------------: | + | linux-x64-glibc | 11.14 | 262,144 | 32.79 | 0 | 161,259,308 | + | linux-arm64-glibc | 18.65 | 0 | 36.87 | 0 | 159,228,899 | + | darwin-x64 | 36.39 | 81,920 | 64.32 | 90,112 | 157,239,968 | + | darwin-arm64 | 11.57 | 32,768 | 19.02 | 49,152 | 153,804,850 | + | win32-x64 | 27.37 | 991,232 | 54.78 | 417,792 | 133,696,372 | + | win32-arm64 | 47.88 | 2,088,960 | 92.90 | 2,183,168 | 118,418,686 | + +- Each retention transaction passes only by reporting zero reclaimed bytes, the exact content ID as + blocked, and a still-present entry. Each subsequent release/eviction passes only by reporting exact + initial equals reclaimed bytes, zero final bytes, that content ID as evicted, no blocked IDs, exact + entry absence, and complete workflow-level measurement-root cleanup. +- Existing cold/warm fields remain regression-comparable. In this run cold publication is + 1.46–5.17s with 40,120,320–57,077,760 incremental RSS; warm full verification is 130.05–648.88ms + with 131,072–5,906,432 incremental RSS. All remain below the fixed 5m/80 MiB per-operation budgets. +- Repository gates: PR Checks run + [29464742361](https://github.com/stablyai/orca/actions/runs/29464742361), job + [87515420888](https://github.com/stablyai/orca/actions/runs/29464742361/job/87515420888), passes in + 13m30s. Golden E2E run + [29464742368](https://github.com/stablyai/orca/actions/runs/29464742368) passes Linux job + [87515420922](https://github.com/stablyai/orca/actions/runs/29464742368/job/87515420922) in 4m35s + and macOS job + [87515420928](https://github.com/stablyai/orca/actions/runs/29464742368/job/87515420928) in 3m54s. +- Supplemental/floor result: both Linux oldest-userland jobs and Windows x64 build-20348 job + [87516657331](https://github.com/stablyai/orca/actions/runs/29464742446/job/87516657331) pass. + Windows arm64 primary source/runtime/cache proof passes; its separate oldest-floor job + [87516657323](https://github.com/stablyai/orca/actions/runs/29464742446/job/87516657323) reconstructs + and executes the exact bytes, then rejects observed `10.0.26200` with `osBuild: false` against the + required build 26100. Therefore the overall artifact workflow is expected red and no Windows arm64 + tuple is enabled. +- Closure: disconnected cross-process leases, bounded recency, exact accounting, LRU/protection, + cancellation settlement, full-size active retention, owner release, atomic eviction, and cleanup + are proven on all six native client families. This closes only the combined bounded local-cache + implementation item. +- Residual gaps: read-only/disk-full/quota/inode faults, abrupt crash/power-loss durability, network + filesystems/PID namespaces, packaged manifest/key and cache-root loading, Electron proxy/ + certificate behavior, downloader/cache orchestration, client-offline transfer, SSH install/launch, + native trust, per-target Beta wiring, and every tuple/default path remain open. No release asset is + published and no production consumer exists. +- Follow-up: checkpoint this evidence, then audit the smallest disconnected packaged-manifest/ + official-build/cache-root boundary and add purpose-named RED contracts before implementation. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -13712,10 +13790,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the disconnected in-use lease/recency/2 GiB eviction package, then require both -purpose suites and exact full-size active-retention/release/eviction measurements on all six native -jobs. Do not add a desktop call site, SSH transfer/install, mode wiring, tuple enablement, release -publication, or default behavior until that CI evidence is complete. +Audit official-build manifest/key loading, app-owned cache-root selection, proxy/certificate behavior, +and downloader/cache orchestration. Select only the next coherent disconnected capability, record its +boundary and purpose-named RED, then implement it without adding SSH transfer/install, mode wiring, +tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. From ffc7947a4afe823fe56c4972cca4af8c509eca83 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 22:20:20 -0400 Subject: [PATCH 177/267] feat: load signed packaged relay manifest Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 16 +- ...github-release-implementation-checklist.md | 153 ++++++++++- .../ssh/ssh-relay-packaged-manifest.test.ts | 253 ++++++++++++++++++ src/main/ssh/ssh-relay-packaged-manifest.ts | 147 ++++++++++ 6 files changed, 563 insertions(+), 9 deletions(-) create mode 100644 src/main/ssh/ssh-relay-packaged-manifest.test.ts create mode 100644 src/main/ssh/ssh-relay-packaged-manifest.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 289adc424d3..86b521deead 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -268,6 +268,7 @@ jobs: src/main/ssh/ssh-relay-artifact-cache-entry.test.ts \ src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts \ src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts \ + src/main/ssh/ssh-relay-packaged-manifest.test.ts \ src/main/ssh/ssh-relay-runtime-identity.test.ts \ config/scripts/ssh-relay-runtime-compatibility.test.mjs \ config/scripts/ssh-relay-node-release-verification.test.mjs \ @@ -802,6 +803,7 @@ jobs: src/main/ssh/ssh-relay-artifact-cache-entry.test.ts ` src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts ` src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts ` + src/main/ssh/ssh-relay-packaged-manifest.test.ts ` src/main/ssh/ssh-relay-runtime-identity.test.ts ` config/scripts/ssh-relay-runtime-compatibility.test.mjs ` config/scripts/ssh-relay-node-release-verification.test.mjs ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 0e9b36bf790..a23b0ae2a07 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -126,6 +126,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-artifact-cache-entry.test.ts', 'ssh-relay-artifact-cache-in-use-lease.test.ts', 'ssh-relay-artifact-cache-eviction.test.ts', + 'ssh-relay-packaged-manifest.test.ts', 'ssh-relay-runtime-identity.test.ts' ]) { // Why: portable desktop selection contracts need proof on every native runner family, not 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 7fec2fe4857..0be24ebfbc0 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 @@ -9,9 +9,13 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — bounded disconnected -cache gate closed; auditing the next packaged-manifest/cache-root boundary, 2026-07-15, Codex -implementation owner.** Exact-head run +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — disconnected +official-build manifest loader locally proven; isolated commit/push and all-six native Node 24 proof +next, 2026-07-15, Codex implementation owner.** `E-M5-PACKAGED-MANIFEST-LOCAL-001` passes 6/6 +purpose-named tests, 300/300 non-full-size SSH relay tests, 279/279 release-script tests, typecheck, +lint, format, max-lines, and diff gates. `E-M5-PACKAGED-MANIFEST-CI-WIRING-LOCAL-001` pins the suite +into both native job families. No packaged resource, production key, cache root, downloader, SSH +consumer, Beta mode, tuple, publication, or default behavior is connected. Exact-head run [29464742446](https://github.com/stablyai/orca/actions/runs/29464742446) passes the lease/eviction source contracts and exact full-size active-retention/release/eviction lifecycle on Linux, macOS, and Windows, x64 and arm64, under `E-M5-ARTIFACT-CACHE-EVICTION-CI-001`. Retention is 11.14–47.88ms @@ -312,7 +316,11 @@ production keys/environment/seed, and merge to `main` remain disconnected. `E-M5-OFFLINE-SELECTION-LOCAL-001`. The first post-push audit found that native jobs omitted these source suites; both job families are now locally pinned under `E-M5-OFFLINE-SELECTION-CI-WIRING-LOCAL-001`, and exact-head all-six native proof is closed by - `E-M5-OFFLINE-SELECTION-CI-001`. The actual packaged embedded-manifest loader remains open. + `E-M5-OFFLINE-SELECTION-CI-001`. The disconnected fixed-resource manifest loader is locally + green under `E-M5-PACKAGED-MANIFEST-LOCAL-001`, and both native workflow command families are + pinned under `E-M5-PACKAGED-MANIFEST-CI-WIRING-LOCAL-001`. Exact-head all-six native proof, + production keys, signed embedded bytes, builder mapping, packaged-app smoke, and a consumer + remain open, so this item is not complete. - [ ] Stream bounded downloads; verify signature, size, archive hash, and extracted tree. The disconnected Electron downloader is locally green under `E-M5-ARTIFACT-DOWNLOAD-LOCAL-RED-001` and `E-M5-ARTIFACT-DOWNLOAD-LOCAL-001`; exact-head 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 cc0594d2ddb..12b76fe00d8 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    -Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the next disconnected packaged-manifest/cache-root boundary**. Exact-head `b79bcbd04` closes the bounded cache gate on all six native clients under E-M5-ARTIFACT-CACHE-EVICTION-CI-001. Before adding a consumer, audit official-build manifest/key loading, app-owned cache-root selection, proxy/certificate behavior, and downloader/cache orchestration; add purpose-named RED contracts for only the next coherent package. Keep SSH, settings, tuples, publication, and default behavior disconnected.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: checkpoint and obtain all-six native evidence for the locally proven disconnected official-build manifest loader**. `E-M5-PACKAGED-MANIFEST-LOCAL-001` and `E-M5-PACKAGED-MANIFEST-CI-WIRING-LOCAL-001` prove the fixed-resource loader and its native source-test wiring locally. Keep resource embedding, production keys, Electron/cache-root/downloader/SSH/settings/tuples/publication/default behavior disconnected until the exact-head native jobs pass.
    Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — user authority covers end-to-end PR-contained implementation, commits, pushes, CI, rehearsals, and release writes; merging to `main` remains explicitly prohibited. Commit `b79bcbd04` passes all six native source/full-size cache jobs, PR Checks, and Golden E2E. The artifact workflow is red only for the retained hosted Windows arm64 build-26200 versus required-26100 floor rejection after its primary proof. No Electron, downloader, SSH, setting, tuple, publication, or default-path consumer exists. 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) @@ -13732,6 +13732,148 @@ src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts`. - Follow-up: checkpoint this evidence, then audit the smallest disconnected packaged-manifest/ official-build/cache-root boundary and add purpose-named RED contracts before implementation. +### E-M5-PACKAGED-MANIFEST-AUDIT-001 — Fixed official-build manifest loading boundary + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed evidence head `20728605330b0320e2ea6ec3c5a4380e68d1a0d8` on draft PR + #8741. +- Exact audit commands: complete reads of the desktop manifest schema/signature/release-identity + modules, artifact selector/downloader, relay path resolution, Electron builder config and tests, + release required-assets/manifest composition, application startup/version seeding, and Electron + proxy bridge; targeted `rg -n` over `app.isPackaged`, `process.resourcesPath`, `app.getVersion()`, + `extraResources`, `ORCA_RELAY_PATH`, manifest consumers, accepted keys, `net.fetch`, proxy, and + certificate behavior. +- Findings: + - no packaged relay-runtime manifest resource, production accepted-key set, manifest filesystem + loader, Electron adapter, cache root, or product consumer exists. Existing manifest verification + is pure and branded/frozen; all callers are tests or release scripts; + - `config/` is intentionally excluded from `app.asar`, while real runtime resources use explicit + `extraResources` and `process.resourcesPath`. The future official manifest must therefore be a + fixed real resource, never a mutable user-data file, environment-selected path, GitHub lookup, or + `latest` result; + - the smallest safe next capability is a dependency-injected official-build loader. It derives + exactly `/ssh-relay-runtime/orca-ssh-relay-runtime-manifest.json`, requires an + absolute resources path and real non-link resource directory/file, bounds the file at 1 MiB, + reads through a stable file handle, parses JSON, and verifies the existing strict schema and + accepted-key Ed25519 signatures; + - only after signature verification may it compare manifest tag `v`, version, + channel, and relay protocol to the caller-supplied compiled desktop identity. Stable, RC, and perf + identities use the already-proven release-tag parser. Missing, malformed, replaced, unknown-key, + invalid-signature, build-mismatch, or protocol-mismatch state is an explicit fail-closed error; + - accepted keys remain compile-time inputs to this capability. The real production public key, + protected signing seed/environment, signed manifest bytes, builder `extraResources` mapping, and + release required-assets composition are absent and remain blocking gates before any official + client or tuple can use it; + - current Electron `net.fetch` already uses Chromium's certificate/session stack and the app has a + separate tested system/settings/environment proxy bridge. Network orchestration is deliberately + excluded from this filesystem/trust package and will be audited separately; + - existing `ORCA_RELAY_PATH` legacy development behavior remains untouched. This package adds no + development environment override and no product call site. +- Required purpose-named contracts: exact fixed cross-platform path; packaged-only guard; absolute + resources path; stable regular resource directory/file; 1 MiB bound; partial-read loop; accepted + signature and deep-frozen result; empty/unknown/mismatched key; malformed/oversized/replaced/link + resource; exact stable/RC/perf identity; tag/version/channel/protocol mismatch; and proof that no + network, cache, Electron startup, SSH, mode, tuple, or default caller is introduced. +- Deliberately separate: production key generation/provisioning/rotation, signed release manifest, + builder/resource embedding, packaged-app smoke, release required-assets composition, app.getPath + cache-root selection, proxy/certificate/live download, offline behavior, ORCA_RELAY_PATH trust + changes, SSH transfer/install, fallback classification, settings/Beta UI, tuple enablement, + publication, and default behavior. +- Follow-up: add `ssh-relay-packaged-manifest.test.ts` while the production loader is absent, run and + record the missing-module RED, then implement only this audited loader and require focused, broad, + static, and all-six native source evidence before resource embedding or a consumer. + +### E-M5-PACKAGED-MANIFEST-LOCAL-RED-001 — Official-build manifest loader is absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: purpose-named `src/main/ssh/ssh-relay-packaged-manifest.test.ts` atop exact pushed evidence + head `20728605330b0320e2ea6ec3c5a4380e68d1a0d8`; production loader deliberately absent. +- Exact command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-packaged-manifest.test.ts`. +- Environment: macOS 26.2 arm64 build 25C56, Node v26.0.0, pnpm 10.24.0, Vitest 4.1.5. +- Result: required RED; one failed suite / zero collected tests in 168ms Vitest / 0.99s wall, + 131,743,744-byte maximum RSS, zero swaps. Import fails because + `./ssh-relay-packaged-manifest` does not exist. +- Contracts encoded before implementation: fixed absolute resource path and 1 MiB limit; + packaged-only guard; accepted signature and deep-frozen stable/RC/perf identities; missing, + malformed, oversized, or linked resource rejection; empty/unknown/mismatched/invalid key state; + and signed tag/version/channel/protocol drift rejection. +- Consumer-disconnection oracle: the test imports only the absent source capability and uses isolated + temporary resource roots and test keys. It adds no Electron, builder, cache, downloader, SSH, + setting, tuple, release, or default call site. +- Follow-up: implement only the audited fixed-resource loader and make this suite green without + adding a production key, resource mapping, or consumer. + +### E-M5-PACKAGED-MANIFEST-LOCAL-001 — Fixed official-build manifest loader + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: six-file working package atop exact pushed evidence head + `20728605330b0320e2ea6ec3c5a4380e68d1a0d8`; implementation and purpose-named tests are + `src/main/ssh/ssh-relay-packaged-manifest.ts` and + `src/main/ssh/ssh-relay-packaged-manifest.test.ts`. +- Exact focused command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-packaged-manifest.test.ts`. +- Environment: macOS 26.2 arm64 build 25C56, Node v26.0.0, pnpm 10.24.0, Vitest 4.1.5. +- Result: 1 file / 6 tests pass in 348ms Vitest / 1.06s wall with 131,989,504-byte maximum RSS, + 96,179,600-byte peak memory footprint, and zero swaps. The repository requires Node 24; final + native workflow proof therefore remains required and is not inferred from this local Node 26 run. +- Proven behavior: packaged-only and absolute-path guards; one fixed cross-platform resource path; + real non-link directory/file checks; non-empty 1 MiB bound; stable-handle identity checks; + deterministic seven-byte partial reads; mutation rejection before JSON/signature processing; + strict JSON/schema and accepted-key Ed25519 verification; deep-frozen branded output; and exact + stable/RC/perf tag, version, channel, and relay-protocol binding after signature verification. +- Fail-closed cases: missing, malformed, oversized, linked, partial/mutated resources; + empty/unknown/mismatched accepted keys; invalid signatures; and signed build/protocol drift. +- Broad exact commands and results on the final source/test state: + - `pnpm exec vitest run --config config/vitest.config.ts +src/main/ssh/ssh-relay-*.test.ts`: 22 files / 300 tests pass, with three declared full-size + fixture suites / three tests skipped, in 28.04s; + - `pnpm exec vitest run --config config/vitest.config.ts +config/scripts/ssh-relay-runtime-*.test.mjs`: 50 files / 279 tests pass in 24.75s; + - `pnpm typecheck`: passes all Node, CLI, and web projects; + - `pnpm lint`: passes oxlint, type-aware switch exhaustiveness, reliability gates (41), max-lines + ratchet (355 grandfathered, no new bypass), bundled-skill guides, and localization gates. Only + pre-existing warnings outside this package are reported; + - `pnpm exec oxfmt --check src/main/ssh/ssh-relay-packaged-manifest.ts +src/main/ssh/ssh-relay-packaged-manifest.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs +.github/workflows/ssh-relay-runtime-artifacts.yml +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` + and `git diff --check`: pass. +- Consumer-disconnection oracle: repository `rg` finds the production module imported only by its + purpose-named test. The loader imports no Electron, network, cache, SSH, settings, or relay-path + module. The protected Node/npm resolver pair has zero diff. No product call site, accepted + production key, resource bytes, builder mapping, cache root, mode, tuple, publication, or default + behavior is present. +- Residual gaps: all-six Node 24 native execution, production accepted-key lifecycle, signed + embedded bytes, builder `extraResources`, release required-assets composition, packaged-app + smoke, cache-root/downloader orchestration, offline transfer, SSH install/launch, per-target Beta, + fallback, tuple enablement, publication, and default behavior remain open. + +### E-M5-PACKAGED-MANIFEST-CI-WIRING-LOCAL-001 — Native source-test wiring + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: same six-file working package as `E-M5-PACKAGED-MANIFEST-LOCAL-001`. +- Implementation: add `ssh-relay-packaged-manifest.test.ts` to the POSIX and PowerShell native build + command lists in `.github/workflows/ssh-relay-runtime-artifacts.yml`; add it to the workflow + occurrence oracle, which requires exactly two workflow occurrences. +- Exact command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-packaged-manifest.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs`. +- Result: 2 files / 13 tests pass in 646ms. The standalone workflow suite is also included in the + 50-file / 279-test release-script result above. +- Closure rule: this proves wiring shape only. Do not close the native gate until the exact pushed + implementation head executes the source suite successfully on Linux x64/arm64, macOS x64/arm64, + and Windows x64/arm64 under Node 24. +- Follow-up: format/diff checkpoint, commit and push the isolated package, then record each native + job/run ID and result under `E-M5-PACKAGED-MANIFEST-CI-001` before adding resource embedding, + production keys, cache-root selection, or a consumer. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -13790,10 +13932,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Audit official-build manifest/key loading, app-owned cache-root selection, proxy/certificate behavior, -and downloader/cache orchestration. Select only the next coherent disconnected capability, record its -boundary and purpose-named RED, then implement it without adding SSH transfer/install, mode wiring, -tuple enablement, release publication, or default behavior. +Add and run the purpose-named official-build manifest-loader RED fixed by +E-M5-PACKAGED-MANIFEST-AUDIT-001, then implement only stable fixed-resource loading, accepted-key +signature verification, and exact desktop build/protocol identity. Do not add resource embedding, +production keys, cache-root/downloader orchestration, SSH transfer/install, mode wiring, tuple +enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. diff --git a/src/main/ssh/ssh-relay-packaged-manifest.test.ts b/src/main/ssh/ssh-relay-packaged-manifest.test.ts new file mode 100644 index 00000000000..20f6f879cbc --- /dev/null +++ b/src/main/ssh/ssh-relay-packaged-manifest.test.ts @@ -0,0 +1,253 @@ +import { mkdir, mkdtemp, open, rm, symlink, utimes, writeFile } from 'node:fs/promises' +import type { FileHandle } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import nacl from 'tweetnacl' +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { createSshRelayArtifactTestManifest } from './ssh-relay-artifact-test-manifest' +import { + loadSshRelayPackagedManifest, + sshRelayPackagedManifestPath, + SSH_RELAY_PACKAGED_MANIFEST_LIMITS +} from './ssh-relay-packaged-manifest' +import { + signSshRelayArtifactManifest, + sshRelayManifestKeyId, + type SshRelayManifestAcceptedKey +} from './ssh-relay-manifest-signature' + +const temporaryDirectories: string[] = [] +const keyPair = nacl.sign.keyPair.fromSeed(Uint8Array.from({ length: 32 }, (_, index) => index)) +const acceptedKey: SshRelayManifestAcceptedKey = { + keyId: sshRelayManifestKeyId(keyPair.publicKey), + publicKey: keyPair.publicKey +} + +type FileHandleBufferRead = ( + this: FileHandle, + buffer: TBuffer, + offset?: number, + length?: number, + position?: number | null +) => Promise<{ bytesRead: number; buffer: TBuffer }> + +async function resourcesRoot(): Promise { + const root = await mkdtemp(join(tmpdir(), 'orca-relay-packaged-manifest-')) + temporaryDirectories.push(root) + return join(root, 'Resources') +} + +function signedManifest({ + version = '1.4.140-rc.1', + channel = 'rc', + relayProtocolVersion = 1 +}: { + version?: string + channel?: 'stable' | 'rc' | 'perf' + relayProtocolVersion?: number +} = {}) { + const manifest = createSshRelayArtifactTestManifest() + manifest.build = { + tag: `v${version}`, + version, + channel, + relayProtocolVersion + } + manifest.signatures = [signSshRelayArtifactManifest(manifest, keyPair.secretKey)] + return manifest +} + +async function writeManifest(resourcesPath: string, manifest = signedManifest()): Promise { + const path = sshRelayPackagedManifestPath(resourcesPath) + await mkdir(join(resourcesPath, 'ssh-relay-runtime'), { recursive: true }) + await writeFile(path, JSON.stringify(manifest)) +} + +function loadOptions(resourcesPath: string) { + return { + packaged: true, + resourcesPath, + appVersion: '1.4.140-rc.1', + relayProtocolVersion: 1, + acceptedKeys: [acceptedKey] + } +} + +async function limitFileHandleReads( + maximumBytes: number, + afterFirstRead?: () => Promise +): Promise> { + const resourcesPath = await resourcesRoot() + const probePath = join(resourcesPath, 'probe') + await mkdir(resourcesPath, { recursive: true }) + await writeFile(probePath, 'probe') + const probe = await open(probePath, 'r') + const prototype = Object.getPrototypeOf(probe) as { read: FileHandleBufferRead } + await probe.close() + const originalRead = prototype.read + let pendingAfterFirstRead = afterFirstRead + return vi.spyOn(prototype, 'read').mockImplementation(async function ( + this: FileHandle, + buffer: TBuffer, + offset = 0, + length = buffer.byteLength - offset, + position: number | null = null + ) { + const result = await originalRead.call( + this, + buffer, + offset, + Math.min(length, maximumBytes), + position + ) + if (result.bytesRead > 0 && pendingAfterFirstRead) { + const callback = pendingAfterFirstRead + pendingAfterFirstRead = undefined + await callback() + } + return result + }) +} + +afterEach(async () => { + vi.restoreAllMocks() + await Promise.all( + temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true })) + ) +}) + +describe('SSH relay packaged manifest', () => { + it('pins one fixed absolute resource path and a bounded stable read', async () => { + const resourcesPath = await resourcesRoot() + expect(sshRelayPackagedManifestPath(resourcesPath)).toBe( + join(resourcesPath, 'ssh-relay-runtime', 'orca-ssh-relay-runtime-manifest.json') + ) + expect(SSH_RELAY_PACKAGED_MANIFEST_LIMITS).toEqual({ maximumBytes: 1024 * 1024 }) + expect(Object.isFrozen(SSH_RELAY_PACKAGED_MANIFEST_LIMITS)).toBe(true) + expect(() => sshRelayPackagedManifestPath('relative/resources')).toThrow(/absolute/i) + await expect( + loadSshRelayPackagedManifest({ ...loadOptions(resourcesPath), packaged: false }) + ).rejects.toThrow(/packaged/i) + }) + + it('verifies an accepted signature and exact stable, RC, and perf desktop identities', async () => { + const resourcesPath = await resourcesRoot() + for (const value of [ + { version: '1.4.140', channel: 'stable' as const }, + { version: '1.4.140-rc.1', channel: 'rc' as const }, + { version: '1.4.140-rc.1.perf', channel: 'perf' as const } + ]) { + await writeManifest(resourcesPath, signedManifest(value)) + const manifest = await loadSshRelayPackagedManifest({ + ...loadOptions(resourcesPath), + appVersion: value.version + }) + expect(manifest.build).toEqual({ + tag: `v${value.version}`, + version: value.version, + channel: value.channel, + relayProtocolVersion: 1 + }) + expect(Object.isFrozen(manifest)).toBe(true) + expect(Object.isFrozen(manifest.tuples[0].entries)).toBe(true) + } + }) + + it('completes partial reads and detects mutation before verification', async () => { + const resourcesPath = await resourcesRoot() + await writeManifest(resourcesPath) + + const partialRead = await limitFileHandleReads(7) + await expect(loadSshRelayPackagedManifest(loadOptions(resourcesPath))).resolves.toMatchObject({ + build: { tag: 'v1.4.140-rc.1' } + }) + partialRead.mockRestore() + + const manifestPath = sshRelayPackagedManifestPath(resourcesPath) + const mutateAfterFirstRead = await limitFileHandleReads(7, async () => { + await writeFile(manifestPath, JSON.stringify(signedManifest())) + const future = new Date(Date.now() + 60_000) + await utimes(manifestPath, future, future) + }) + await expect(loadSshRelayPackagedManifest(loadOptions(resourcesPath))).rejects.toThrow( + /changed while it was read/i + ) + mutateAfterFirstRead.mockRestore() + }) + + it('fails closed for missing, malformed, oversized, and linked resources', async () => { + const resourcesPath = await resourcesRoot() + await expect(loadSshRelayPackagedManifest(loadOptions(resourcesPath))).rejects.toThrow( + /manifest|resource/i + ) + + await mkdir(join(resourcesPath, 'ssh-relay-runtime'), { recursive: true }) + await writeFile(sshRelayPackagedManifestPath(resourcesPath), '{not-json') + await expect(loadSshRelayPackagedManifest(loadOptions(resourcesPath))).rejects.toThrow(/json/i) + + await writeFile( + sshRelayPackagedManifestPath(resourcesPath), + Buffer.alloc(SSH_RELAY_PACKAGED_MANIFEST_LIMITS.maximumBytes + 1) + ) + await expect(loadSshRelayPackagedManifest(loadOptions(resourcesPath))).rejects.toThrow(/size/i) + + await rm(join(resourcesPath, 'ssh-relay-runtime'), { recursive: true }) + const linkedTarget = join(resourcesPath, 'linked-manifest') + await mkdir(linkedTarget, { recursive: true }) + await writeFile( + join(linkedTarget, 'orca-ssh-relay-runtime-manifest.json'), + JSON.stringify(signedManifest()) + ) + await symlink(linkedTarget, join(resourcesPath, 'ssh-relay-runtime'), 'junction') + await expect(loadSshRelayPackagedManifest(loadOptions(resourcesPath))).rejects.toThrow( + /directory|link/i + ) + }) + + it('rejects empty, unknown, mismatched, and invalid accepted-key state', async () => { + const resourcesPath = await resourcesRoot() + await writeManifest(resourcesPath) + await expect( + loadSshRelayPackagedManifest({ ...loadOptions(resourcesPath), acceptedKeys: [] }) + ).rejects.toThrow(/key|signature/i) + + const other = nacl.sign.keyPair() + await expect( + loadSshRelayPackagedManifest({ + ...loadOptions(resourcesPath), + acceptedKeys: [ + { keyId: sshRelayManifestKeyId(other.publicKey), publicKey: other.publicKey } + ] + }) + ).rejects.toThrow(/unknown signing key/i) + await expect( + loadSshRelayPackagedManifest({ + ...loadOptions(resourcesPath), + acceptedKeys: [{ keyId: acceptedKey.keyId, publicKey: other.publicKey }] + }) + ).rejects.toThrow(/key id/i) + + const invalid = signedManifest() + invalid.signatures[0].signature = Buffer.alloc(64, 1).toString('base64') + await writeManifest(resourcesPath, invalid) + await expect(loadSshRelayPackagedManifest(loadOptions(resourcesPath))).rejects.toThrow( + /invalid.*signature/i + ) + }) + + it('rejects signed tag, version, channel, and protocol drift from the desktop', async () => { + const resourcesPath = await resourcesRoot() + for (const manifest of [ + signedManifest({ version: '1.4.141-rc.1' }), + signedManifest({ version: '1.4.140', channel: 'stable' }), + signedManifest({ relayProtocolVersion: 2 }) + ]) { + await writeManifest(resourcesPath, manifest) + await expect(loadSshRelayPackagedManifest(loadOptions(resourcesPath))).rejects.toThrow( + /build|identity|protocol|release tag/i + ) + } + }) +}) diff --git a/src/main/ssh/ssh-relay-packaged-manifest.ts b/src/main/ssh/ssh-relay-packaged-manifest.ts new file mode 100644 index 00000000000..1d9ffb94cfb --- /dev/null +++ b/src/main/ssh/ssh-relay-packaged-manifest.ts @@ -0,0 +1,147 @@ +import { lstat, open } from 'node:fs/promises' +import { isAbsolute, join } from 'node:path' + +import { parseSshRelayReleaseTag } from './ssh-relay-release-asset' +import { + verifySshRelayArtifactManifest, + type SshRelayManifestAcceptedKey, + type VerifiedSshRelayArtifactManifest +} from './ssh-relay-manifest-signature' + +const MAXIMUM_BYTES = 1024 * 1024 +const RESOURCE_DIRECTORY = 'ssh-relay-runtime' +const MANIFEST_NAME = 'orca-ssh-relay-runtime-manifest.json' + +type StableIdentity = { + dev: bigint + ino: bigint + size: bigint + mtimeNs: bigint + ctimeNs: bigint +} + +function sameStableIdentity(left: StableIdentity, right: StableIdentity): boolean { + return ( + left.dev === right.dev && + left.ino === right.ino && + left.size === right.size && + left.mtimeNs === right.mtimeNs && + left.ctimeNs === right.ctimeNs + ) +} + +export function sshRelayPackagedManifestPath(resourcesPath: string): string { + if (!isAbsolute(resourcesPath)) { + throw new Error('SSH relay packaged resources path must be absolute') + } + return join(resourcesPath, RESOURCE_DIRECTORY, MANIFEST_NAME) +} + +async function readStableManifestBytes(resourcesPath: string): Promise { + const directoryPath = join(resourcesPath, RESOURCE_DIRECTORY) + const directoryBefore = await lstat(directoryPath, { bigint: true }) + if (!directoryBefore.isDirectory() || directoryBefore.isSymbolicLink()) { + throw new Error('SSH relay packaged manifest resource directory must not be a link') + } + + const manifestPath = sshRelayPackagedManifestPath(resourcesPath) + const before = await lstat(manifestPath, { bigint: true }) + if ( + !before.isFile() || + before.isSymbolicLink() || + before.size <= 0n || + before.size > BigInt(MAXIMUM_BYTES) + ) { + throw new Error('SSH relay packaged manifest resource has an invalid file type or size') + } + + const handle = await open(manifestPath, 'r') + try { + const opened = await handle.stat({ bigint: true }) + if (!opened.isFile() || !sameStableIdentity(before, opened)) { + throw new Error('SSH relay packaged manifest resource changed before it was opened') + } + const bytes = Buffer.alloc(Number(opened.size)) + let offset = 0 + while (offset < bytes.length) { + const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, offset) + if (bytesRead <= 0) { + throw new Error('SSH relay packaged manifest resource ended before its declared size') + } + offset += bytesRead + } + const after = await handle.stat({ bigint: true }) + const directoryAfter = await lstat(directoryPath, { bigint: true }) + if ( + !sameStableIdentity(opened, after) || + !directoryAfter.isDirectory() || + directoryAfter.isSymbolicLink() || + directoryAfter.dev !== directoryBefore.dev || + directoryAfter.ino !== directoryBefore.ino || + directoryAfter.mtimeNs !== directoryBefore.mtimeNs || + directoryAfter.ctimeNs !== directoryBefore.ctimeNs + ) { + throw new Error('SSH relay packaged manifest resource changed while it was read') + } + return bytes + } finally { + await handle.close() + } +} + +function parseManifestJson(bytes: Buffer): unknown { + try { + return JSON.parse(bytes.toString('utf8')) as unknown + } catch (error) { + throw new Error('SSH relay packaged manifest resource is not valid JSON', { cause: error }) + } +} + +function assertDesktopIdentity( + manifest: VerifiedSshRelayArtifactManifest, + appVersion: string, + relayProtocolVersion: number +): void { + const release = parseSshRelayReleaseTag(`v${appVersion}`) + if ( + manifest.build.tag !== release.tag || + manifest.build.version !== release.version || + manifest.build.channel !== release.channel + ) { + throw new Error('SSH relay packaged manifest build identity does not match this desktop') + } + if ( + !Number.isSafeInteger(relayProtocolVersion) || + relayProtocolVersion <= 0 || + manifest.build.relayProtocolVersion !== relayProtocolVersion + ) { + throw new Error('SSH relay packaged manifest protocol does not match this desktop') + } +} + +export async function loadSshRelayPackagedManifest({ + packaged, + resourcesPath, + appVersion, + relayProtocolVersion, + acceptedKeys +}: { + packaged: boolean + resourcesPath: string + appVersion: string + relayProtocolVersion: number + acceptedKeys: readonly SshRelayManifestAcceptedKey[] +}): Promise { + if (!packaged) { + // Why: mutable developer paths must never become an implicit official-build trust source. + throw new Error('SSH relay packaged manifest is available only in a packaged build') + } + const bytes = await readStableManifestBytes(resourcesPath) + const manifest = verifySshRelayArtifactManifest(parseManifestJson(bytes), acceptedKeys) + assertDesktopIdentity(manifest, appVersion, relayProtocolVersion) + return manifest +} + +export const SSH_RELAY_PACKAGED_MANIFEST_LIMITS = Object.freeze({ + maximumBytes: MAXIMUM_BYTES +}) From 6358fb466b2b2099f4c9add9b8e2f772664ec3ea Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 22:38:27 -0400 Subject: [PATCH 178/267] docs: record packaged relay manifest evidence Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 18 +++-- ...github-release-implementation-checklist.md | 69 +++++++++++++++++-- 2 files changed, 76 insertions(+), 11 deletions(-) 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 0be24ebfbc0..a43ea2973c0 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 @@ -10,12 +10,18 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — disconnected -official-build manifest loader locally proven; isolated commit/push and all-six native Node 24 proof +official-build manifest loader proven on all six native clients; accepted-key/resource-input audit next, 2026-07-15, Codex implementation owner.** `E-M5-PACKAGED-MANIFEST-LOCAL-001` passes 6/6 purpose-named tests, 300/300 non-full-size SSH relay tests, 279/279 release-script tests, typecheck, lint, format, max-lines, and diff gates. `E-M5-PACKAGED-MANIFEST-CI-WIRING-LOCAL-001` pins the suite -into both native job families. No packaged resource, production key, cache root, downloader, SSH -consumer, Beta mode, tuple, publication, or default behavior is connected. Exact-head run +into both native job families; exact-head run +[29466359518](https://github.com/stablyai/orca/actions/runs/29466359518) passes the contract step and +complete primary job on Linux, macOS, and Windows, x64 and arm64, under +`E-M5-PACKAGED-MANIFEST-CI-001`. PR Checks +[29466359581](https://github.com/stablyai/orca/actions/runs/29466359581) and Golden E2E +[29466359541](https://github.com/stablyai/orca/actions/runs/29466359541) pass. No packaged resource, +production key, cache root, downloader, SSH consumer, Beta mode, tuple, publication, or default +behavior is connected. Prior exact-head run [29464742446](https://github.com/stablyai/orca/actions/runs/29464742446) passes the lease/eviction source contracts and exact full-size active-retention/release/eviction lifecycle on Linux, macOS, and Windows, x64 and arm64, under `E-M5-ARTIFACT-CACHE-EVICTION-CI-001`. Retention is 11.14–47.88ms @@ -318,9 +324,9 @@ production keys/environment/seed, and merge to `main` remain disconnected. `E-M5-OFFLINE-SELECTION-CI-WIRING-LOCAL-001`, and exact-head all-six native proof is closed by `E-M5-OFFLINE-SELECTION-CI-001`. The disconnected fixed-resource manifest loader is locally green under `E-M5-PACKAGED-MANIFEST-LOCAL-001`, and both native workflow command families are - pinned under `E-M5-PACKAGED-MANIFEST-CI-WIRING-LOCAL-001`. Exact-head all-six native proof, - production keys, signed embedded bytes, builder mapping, packaged-app smoke, and a consumer - remain open, so this item is not complete. + pinned under `E-M5-PACKAGED-MANIFEST-CI-WIRING-LOCAL-001`. Exact-head all-six native source + proof is closed by `E-M5-PACKAGED-MANIFEST-CI-001`. Production keys, signed embedded bytes, + builder mapping, packaged-app smoke, and a consumer remain open, so this item is not complete. - [ ] Stream bounded downloads; verify signature, size, archive hash, and extracted tree. The disconnected Electron downloader is locally green under `E-M5-ARTIFACT-DOWNLOAD-LOCAL-RED-001` and `E-M5-ARTIFACT-DOWNLOAD-LOCAL-001`; exact-head 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 12b76fe00d8..5b551679a0b 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    -Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: checkpoint and obtain all-six native evidence for the locally proven disconnected official-build manifest loader**. `E-M5-PACKAGED-MANIFEST-LOCAL-001` and `E-M5-PACKAGED-MANIFEST-CI-WIRING-LOCAL-001` prove the fixed-resource loader and its native source-test wiring locally. Keep resource embedding, production keys, Electron/cache-root/downloader/SSH/settings/tuples/publication/default behavior disconnected until the exact-head native jobs pass.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: all-six native packaged-manifest source gate closed; audit the next disconnected accepted-key/resource-embedding prerequisite**. `E-M5-PACKAGED-MANIFEST-CI-001` proves the fixed-resource loader on Linux, macOS, and Windows, x64 and arm64, under Node 24.18.0. Keep production keys/secrets, signed embedded bytes, Electron/cache-root/downloader/SSH/settings/tuples/publication/default behavior disconnected while selecting the next purpose-named RED.
    Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — user authority covers end-to-end PR-contained implementation, commits, pushes, CI, rehearsals, and release writes; merging to `main` remains explicitly prohibited. Commit `b79bcbd04` passes all six native source/full-size cache jobs, PR Checks, and Golden E2E. The artifact workflow is red only for the retained hosted Windows arm64 build-26200 versus required-26100 floor rejection after its primary proof. No Electron, downloader, SSH, setting, tuple, publication, or default-path consumer exists. 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) @@ -13874,6 +13874,65 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs`. job/run ID and result under `E-M5-PACKAGED-MANIFEST-CI-001` before adding resource embedding, production keys, cache-root selection, or a consumer. +### E-M5-PACKAGED-MANIFEST-CI-001 — All-six native fixed-resource loader proof + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed implementation commit + `ffc7947a4afe823fe56c4972cca4af8c509eca83` on draft PR #8741. +- Workflow: [SSH Relay Runtime Artifacts run + 29466359518](https://github.com/stablyai/orca/actions/runs/29466359518). The exact workflow source + and `E-M5-PACKAGED-MANIFEST-CI-WIRING-LOCAL-001` require the purpose-named loader suite inside the + `Run runtime artifact contract tests` step for both POSIX and PowerShell job families. +- Exact native primary results; every listed contract-test step and complete primary job passes + under Node 24.18.0: + - Linux x64, `ubuntu-24.04`, job + [87520232532](https://github.com/stablyai/orca/actions/runs/29466359518/job/87520232532): + contract step 02:21:10–02:21:26Z; job 02:20:48–02:23:11Z; + - Linux arm64, `ubuntu-24.04-arm`, job + [87520232493](https://github.com/stablyai/orca/actions/runs/29466359518/job/87520232493): + contract step 02:21:29–02:21:40Z; job 02:20:51–02:23:34Z; + - macOS x64, `macos-15-intel`, job + [87520232509](https://github.com/stablyai/orca/actions/runs/29466359518/job/87520232509): + contract step 02:22:02–02:22:36Z; job 02:20:49–02:24:26Z; + - macOS arm64, `macos-15`, job + [87520232533](https://github.com/stablyai/orca/actions/runs/29466359518/job/87520232533): + contract step 02:21:45–02:22:04Z; job 02:20:49–02:23:06Z; + - Windows x64, `windows-2022`, job + [87520232476](https://github.com/stablyai/orca/actions/runs/29466359518/job/87520232476): + contract step 02:22:01–02:22:25Z; job 02:20:48–02:26:38Z; + - Windows arm64, `windows-11-arm`, job + [87520232497](https://github.com/stablyai/orca/actions/runs/29466359518/job/87520232497): + contract step 02:24:36–02:25:10Z; job 02:20:49–02:30:39Z. +- Repository gates: [PR Checks run + 29466359581](https://github.com/stablyai/orca/actions/runs/29466359581), job + [87520232413](https://github.com/stablyai/orca/actions/runs/29466359581/job/87520232413), passes + in 14m33s. [Golden E2E run + 29466359541](https://github.com/stablyai/orca/actions/runs/29466359541) passes Linux job + [87520232284](https://github.com/stablyai/orca/actions/runs/29466359541/job/87520232284) in + 4m30s and macOS job + [87520232283](https://github.com/stablyai/orca/actions/runs/29466359541/job/87520232283) in + 5m08s. +- Supplemental result: both Linux oldest-userland jobs and Windows x64 oldest-floor job + [87521425899](https://github.com/stablyai/orca/actions/runs/29466359518/job/87521425899) pass. + Windows arm64 primary source/build/runtime proof passes; its separate oldest-floor job + [87521425836](https://github.com/stablyai/orca/actions/runs/29466359518/job/87521425836) executes + the exact runtime successfully, including Node v24.18.0, PTY, watcher, 2s settlement, and + 49,745,920-byte RSS, then rejects observed build `10.0.26200` against required build 26100 + (`osBuild: false`). The artifact workflow is therefore expected red solely for the previously + declared floor gap; no Windows arm64 tuple is enabled. +- Closure: the source-only packaged loader's path, stable-read, mutation, accepted-key signature, + and exact build/protocol contracts execute on all six native client families. This closes the + loader's native source gate, not packaged-app embedding or product use. +- Residual gaps: no production accepted-key file or protected seed/environment exists; no signed + manifest bytes are embedded; builder `extraResources`, release required-assets composition, + packaged-app extraction/smoke, Electron adapter, cache root, downloader orchestration, offline + transfer, SSH install/launch, per-target Beta, fallback, tuple, publication, and default behavior + remain open. +- Follow-up: commit this evidence-only checkpoint, then audit the smallest PR-contained accepted-key + and immutable resource-input contract. External protected-environment/secret provisioning remains + a separate repository-administrator gate and must not be inferred from source tests. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -13932,10 +13991,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Add and run the purpose-named official-build manifest-loader RED fixed by -E-M5-PACKAGED-MANIFEST-AUDIT-001, then implement only stable fixed-resource loading, accepted-key -signature verification, and exact desktop build/protocol identity. Do not add resource embedding, -production keys, cache-root/downloader orchestration, SSH transfer/install, mode wiring, tuple +Audit the smallest PR-contained accepted-key and immutable resource-input boundary now that +`E-M5-PACKAGED-MANIFEST-CI-001` closes the all-six native source-loader gate. Add a purpose-named RED +before implementation. Do not provision production secrets/environments, embed unreviewed keys or +manifest bytes, add cache-root/downloader orchestration, SSH transfer/install, mode wiring, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, From 11f367e6613b0cae61a1153b77a5be332fd4e763 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 15 Jul 2026 22:44:36 -0400 Subject: [PATCH 179/267] feat: validate relay manifest accepted keys Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 9 +- ...github-release-implementation-checklist.md | 77 ++++++++++++++- .../ssh-relay-manifest-accepted-keys.test.ts | 88 +++++++++++++++++ .../ssh/ssh-relay-manifest-accepted-keys.ts | 99 +++++++++++++++++++ 6 files changed, 272 insertions(+), 4 deletions(-) create mode 100644 src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts create mode 100644 src/main/ssh/ssh-relay-manifest-accepted-keys.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 86b521deead..cff5861094d 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -268,6 +268,7 @@ jobs: src/main/ssh/ssh-relay-artifact-cache-entry.test.ts \ src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts \ src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts \ + src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts \ src/main/ssh/ssh-relay-packaged-manifest.test.ts \ src/main/ssh/ssh-relay-runtime-identity.test.ts \ config/scripts/ssh-relay-runtime-compatibility.test.mjs \ @@ -803,6 +804,7 @@ jobs: src/main/ssh/ssh-relay-artifact-cache-entry.test.ts ` src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts ` src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts ` + src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts ` src/main/ssh/ssh-relay-packaged-manifest.test.ts ` src/main/ssh/ssh-relay-runtime-identity.test.ts ` config/scripts/ssh-relay-runtime-compatibility.test.mjs ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index a23b0ae2a07..5807d274477 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -126,6 +126,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-artifact-cache-entry.test.ts', 'ssh-relay-artifact-cache-in-use-lease.test.ts', 'ssh-relay-artifact-cache-eviction.test.ts', + 'ssh-relay-manifest-accepted-keys.test.ts', 'ssh-relay-packaged-manifest.test.ts', 'ssh-relay-runtime-identity.test.ts' ]) { 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 a43ea2973c0..ad6c2ad4e1e 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 @@ -9,9 +9,12 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — disconnected -official-build manifest loader proven on all six native clients; accepted-key/resource-input audit -next, 2026-07-15, Codex implementation owner.** `E-M5-PACKAGED-MANIFEST-LOCAL-001` passes 6/6 +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — disconnected desktop +accepted-public-key parser locally proven and wired; isolated commit/all-six native proof next, +2026-07-15, Codex implementation owner.** `E-M5-ACCEPTED-KEYS-AUDIT-001` and its RED precede +`E-M5-ACCEPTED-KEYS-LOCAL-001`: 4/4 focused, 19/19 trust, 304/304 non-full-size SSH relay, 12/12 +workflow/aggregate, typecheck, and lint gates pass. No production public key file or secret exists, +and no resource or consumer is connected. The prior `E-M5-PACKAGED-MANIFEST-LOCAL-001` passes 6/6 purpose-named tests, 300/300 non-full-size SSH relay tests, 279/279 release-script tests, typecheck, lint, format, max-lines, and diff gates. `E-M5-PACKAGED-MANIFEST-CI-WIRING-LOCAL-001` pins the suite into both native job families; exact-head run 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 5b551679a0b..4fd3f1e74fc 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    -Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: all-six native packaged-manifest source gate closed; audit the next disconnected accepted-key/resource-embedding prerequisite**. `E-M5-PACKAGED-MANIFEST-CI-001` proves the fixed-resource loader on Linux, macOS, and Windows, x64 and arm64, under Node 24.18.0. Keep production keys/secrets, signed embedded bytes, Electron/cache-root/downloader/SSH/settings/tuples/publication/default behavior disconnected while selecting the next purpose-named RED.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: checkpoint and obtain all-six native evidence for the locally proven disconnected accepted-public-key parser**. `E-M5-ACCEPTED-KEYS-LOCAL-001` proves strict protected-aggregate document parity and both native workflow command lists locally. Keep the production key file/seed/environment, signed embedded bytes, builder/resource/consumer/cache/downloader/SSH/settings/tuples/publication/default behavior absent until exact-head native proof passes.
    Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — user authority covers end-to-end PR-contained implementation, commits, pushes, CI, rehearsals, and release writes; merging to `main` remains explicitly prohibited. Commit `b79bcbd04` passes all six native source/full-size cache jobs, PR Checks, and Golden E2E. The artifact workflow is red only for the retained hosted Windows arm64 build-26200 versus required-26100 floor rejection after its primary proof. No Electron, downloader, SSH, setting, tuple, publication, or default-path consumer exists. 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) @@ -13933,6 +13933,81 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs`. and immutable resource-input contract. External protected-environment/secret provisioning remains a separate repository-administrator gate and must not be inferred from source tests. +### E-M5-ACCEPTED-KEYS-AUDIT-001 — Desktop accepted-public-key document parity + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed evidence head `6358fb466b2b2099f4c9add9b8e2f772664ec3ea`. +- Audit: complete reads of the desktop signature/loader modules and tests, protected manifest + workflow, aggregate command/signing handoff and tests, Electron builder config/tests, package + release scripts, and Linux/Windows/macOS release workflow build/package boundaries; targeted `rg` + over accepted keys, public-key encoding, `extraResources`, manifest signing, desktop build inputs, + and release packaging. +- Findings: + - the protected aggregate already defines the authoritative source format: exact + `{schemaVersion, keys}` fields, schema version 1, one to four keys, exact + `{keyId, publicKeyBase64}` entries, canonical base64 for exactly 32 Ed25519 public-key bytes, + derived SHA-256 key IDs, uniqueness, key-ID sorting, and a SHA-256 fingerprint over canonical + JSON; + - the protected workflow deliberately references an absent + `config/ssh-relay-runtime-manifest-accepted-keys.json` and tests ENOENT so no placeholder can be + mistaken for a reviewed trust root. Release-cut and macOS release workflows remain disconnected; + - the desktop signature verifier accepts decoded key bytes but has no strict parser for the + aggregate document. Adding that parser with deterministic test keys is the smallest useful + PR-contained parity boundary before production key review or immutable resource embedding; + - accepted keys must ultimately be compile-time desktop inputs. Loading a mutable adjacent key + file beside the manifest would let the trust root move with untrusted bytes and is not allowed; + - builder mapping, aggregate-to-desktop artifact download, tag checkout dependencies, byte-identical + packaged extraction, real public key review, and protected seed/environment provisioning remain + separate gates. +- Selected package: pure desktop parser returning cloned/sorted accepted key bytes plus the canonical + accepted-key document fingerprint; strict hostile-input tests and release-format parity; no file + I/O, Electron, resource mapping, manifest, signer, secret, cache, network, SSH, mode, or consumer. + +### E-M5-ACCEPTED-KEYS-LOCAL-RED-001 — Desktop accepted-key parser is absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: purpose-named `src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts` atop exact pushed + evidence head `6358fb466b2b2099f4c9add9b8e2f772664ec3ea`; production parser deliberately absent. +- Exact command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts`. +- Environment: macOS 26.2 arm64 build 25C56, Node v26.0.0, pnpm 10.24.0, Vitest 4.1.5. +- Result: required RED; one failed suite / zero collected tests in 174ms Vitest / 1.16s wall, + 131,989,504-byte maximum RSS, 96,179,600-byte peak memory footprint, and zero swaps. Import fails + because `./ssh-relay-manifest-accepted-keys` does not exist. +- Contracts encoded before implementation: canonical sort/fingerprint, cloned key bytes, frozen + container/records, one-to-four bound, exact fields/schema, canonical base64/32-byte keys, derived + identity, duplicate rejection, and input-order independence. +- Consumer-disconnection oracle: only the purpose-named test imports the absent module; fixtures use + deterministic test seeds. No production key file/seed/environment, resource mapping, Electron, + cache, network, SSH, setting, tuple, publication, or default call site is added. + +### E-M5-ACCEPTED-KEYS-LOCAL-001 — Desktop accepted-key parser and native wiring + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: working package atop exact pushed evidence head + `6358fb466b2b2099f4c9add9b8e2f772664ec3ea`; implementation/test are + `src/main/ssh/ssh-relay-manifest-accepted-keys.ts` and its purpose-named test. +- Focused command/result: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts` passes + 1 file / 4 tests in 301ms Vitest / 1.23s wall, 131,874,816-byte maximum RSS, + 96,048,504-byte peak memory footprint, and zero swaps. +- Proven: exact schema/fields; one-to-four bound; canonical 32-byte base64; derived SHA-256 IDs; + duplicate rejection; input-order-independent sorting; cloned bytes; frozen container/records; and + the exact canonical JSON fingerprint used by the protected aggregate. +- Regression commands/results: accepted-keys/signature/packaged-loader trust suites pass 19/19; + complete `src/main/ssh/ssh-relay-*.test.ts` passes 23 files / 304 tests with three declared + full-size skips; workflow plus aggregate-command suites pass 12/12; full `pnpm typecheck` and + `pnpm lint` pass, including 41 reliability gates and no new max-lines bypass. +- Native wiring: the purpose-named suite is present once in each POSIX/PowerShell command list; the + workflow oracle requires exactly two occurrences. Exact-head all-six Node 24 execution remains + required before this source gate closes. +- Consumer-disconnection oracle: repository consumers remain test-only. The protected workflow still + requires the production accepted-key file to be absent; no key/seed/environment, resource bytes, + builder mapping, Electron/cache/network/SSH/mode/tuple/publication/default caller is added. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts b/src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts new file mode 100644 index 00000000000..49f69eaf442 --- /dev/null +++ b/src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts @@ -0,0 +1,88 @@ +import { createHash } from 'node:crypto' + +import nacl from 'tweetnacl' +import { describe, expect, it } from 'vitest' + +import { parseSshRelayManifestAcceptedKeyDocument } from './ssh-relay-manifest-accepted-keys' +import { sshRelayManifestKeyId } from './ssh-relay-manifest-signature' + +const first = nacl.sign.keyPair.fromSeed(Uint8Array.from({ length: 32 }, (_, index) => index)) +const second = nacl.sign.keyPair.fromSeed(Uint8Array.from({ length: 32 }, (_, index) => 31 - index)) + +function record(publicKey: Uint8Array) { + return { + keyId: sshRelayManifestKeyId(publicKey), + publicKeyBase64: Buffer.from(publicKey).toString('base64') + } +} + +function document(keys = [record(first.publicKey)]) { + return { schemaVersion: 1, keys } +} + +describe('SSH relay manifest accepted keys', () => { + it('parses, clones, sorts, freezes, and fingerprints the canonical release document', () => { + const input = document([record(second.publicKey), record(first.publicKey)]) + const parsed = parseSshRelayManifestAcceptedKeyDocument(input) + const sortedRecords = [...input.keys].sort((left, right) => + left.keyId < right.keyId ? -1 : left.keyId > right.keyId ? 1 : 0 + ) + const canonicalBytes = Buffer.from( + JSON.stringify({ schemaVersion: 1, keys: sortedRecords }), + 'utf8' + ) + + expect(parsed.acceptedKeys.map(({ keyId }) => keyId)).toEqual( + sortedRecords.map(({ keyId }) => keyId) + ) + expect(parsed.acceptedKeys[0].publicKey).not.toBe(first.publicKey) + expect(parsed.sha256).toBe( + `sha256:${createHash('sha256').update(canonicalBytes).digest('hex')}` + ) + expect(Object.isFrozen(parsed)).toBe(true) + expect(Object.isFrozen(parsed.acceptedKeys)).toBe(true) + expect(parsed.acceptedKeys.every(Object.isFrozen)).toBe(true) + }) + + it('accepts one to four unique keys independent of input order', () => { + const records = [record(first.publicKey), record(second.publicKey)] + const forward = parseSshRelayManifestAcceptedKeyDocument(document(records)) + const reverse = parseSshRelayManifestAcceptedKeyDocument(document(records.toReversed())) + + expect(forward.sha256).toBe(reverse.sha256) + expect(forward.acceptedKeys.map(({ keyId }) => keyId)).toEqual( + reverse.acceptedKeys.map(({ keyId }) => keyId) + ) + }) + + it('rejects malformed document shape and key counts', () => { + for (const input of [ + null, + [], + {}, + { schemaVersion: 2, keys: [record(first.publicKey)] }, + { schemaVersion: 1, keys: [], extra: true }, + { schemaVersion: 1, keys: [] }, + { schemaVersion: 1, keys: Array.from({ length: 5 }, () => record(first.publicKey)) } + ]) { + expect(() => parseSshRelayManifestAcceptedKeyDocument(input)).toThrow(/accepted|key|field/i) + } + }) + + it('rejects extra fields, non-canonical base64, wrong sizes, identity drift, and duplicates', () => { + const valid = record(first.publicKey) + for (const key of [ + { ...valid, extra: true }, + { ...valid, publicKeyBase64: `${valid.publicKeyBase64}\n` }, + { ...valid, publicKeyBase64: Buffer.alloc(31).toString('base64') }, + { ...valid, keyId: `sha256:${'0'.repeat(64)}` } + ]) { + expect(() => + parseSshRelayManifestAcceptedKeyDocument({ schemaVersion: 1, keys: [key] }) + ).toThrow(/accepted|key|base64|field|32/i) + } + expect(() => + parseSshRelayManifestAcceptedKeyDocument(document([valid, structuredClone(valid)])) + ).toThrow(/duplicate/i) + }) +}) diff --git a/src/main/ssh/ssh-relay-manifest-accepted-keys.ts b/src/main/ssh/ssh-relay-manifest-accepted-keys.ts new file mode 100644 index 00000000000..f6e5b82ae0c --- /dev/null +++ b/src/main/ssh/ssh-relay-manifest-accepted-keys.ts @@ -0,0 +1,99 @@ +import { createHash } from 'node:crypto' + +import { + sshRelayManifestKeyId, + type SshRelayManifestAcceptedKey +} from './ssh-relay-manifest-signature' +import type { SshRelayDigest } from './ssh-relay-runtime-identity' + +const BASE64 = /^[A-Za-z0-9+/]+={0,2}$/u +const MAXIMUM_KEYS = 4 + +type AcceptedKeyRecord = { + keyId: string + publicKeyBase64: string +} + +function assertObject(value: unknown, label: string): asserts value is Record { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`SSH relay manifest ${label} must be an object`) + } +} + +function assertExactFields(value: Record, fields: string[], label: string): void { + const actual = Object.keys(value).sort() + const expected = [...fields].sort() + if ( + actual.length !== expected.length || + actual.some((field, index) => field !== expected[index]) + ) { + throw new Error(`SSH relay manifest ${label} has unexpected or missing fields`) + } +} + +function decodePublicKey(value: unknown): { bytes: Buffer; base64: string } { + if (typeof value !== 'string' || !BASE64.test(value)) { + throw new Error('SSH relay manifest accepted public key is not canonical base64') + } + const bytes = Buffer.from(value, 'base64') + if (bytes.byteLength !== 32 || bytes.toString('base64') !== value) { + throw new Error('SSH relay manifest accepted public key must contain exactly 32 bytes') + } + return { bytes, base64: value } +} + +function sha256(bytes: Uint8Array): SshRelayDigest { + return `sha256:${createHash('sha256').update(bytes).digest('hex')}` +} + +export function parseSshRelayManifestAcceptedKeyDocument(input: unknown): Readonly<{ + acceptedKeys: readonly Readonly[] + sha256: SshRelayDigest +}> { + assertObject(input, 'accepted-key document') + assertExactFields(input, ['keys', 'schemaVersion'], 'accepted-key document') + if ( + input.schemaVersion !== 1 || + !Array.isArray(input.keys) || + input.keys.length === 0 || + input.keys.length > MAXIMUM_KEYS + ) { + throw new Error('SSH relay manifest accepted keys must be a bounded schema-version-1 array') + } + + const seen = new Set() + const parsed = input.keys + .map((value, index) => { + assertObject(value, `accepted key ${index}`) + assertExactFields(value, ['keyId', 'publicKeyBase64'], `accepted key ${index}`) + const decoded = decodePublicKey(value.publicKeyBase64) + const publicKey = decoded.bytes + const keyId = sshRelayManifestKeyId(publicKey) + if (value.keyId !== keyId) { + throw new Error('SSH relay manifest accepted key ID disagrees with its public key bytes') + } + if (seen.has(keyId)) { + throw new Error(`Duplicate accepted SSH relay manifest key: ${keyId}`) + } + seen.add(keyId) + return { keyId, publicKeyBase64: decoded.base64, publicKey } + }) + .sort((left, right) => (left.keyId < right.keyId ? -1 : left.keyId > right.keyId ? 1 : 0)) + + // Why: the protected aggregate fingerprints this exact sorted projection, so desktop build + // evidence can prove it embedded the reviewed public-key set without trusting input order. + const canonicalRecords: AcceptedKeyRecord[] = parsed.map(({ keyId, publicKeyBase64 }) => ({ + keyId, + publicKeyBase64 + })) + const canonicalBytes = Buffer.from( + JSON.stringify({ schemaVersion: 1, keys: canonicalRecords }), + 'utf8' + ) + const acceptedKeys = Object.freeze( + parsed.map(({ keyId, publicKey }) => Object.freeze({ keyId, publicKey })) + ) + return Object.freeze({ acceptedKeys, sha256: sha256(canonicalBytes) }) +} + +export const SSH_RELAY_MANIFEST_ACCEPTED_KEY_LIMITS = Object.freeze({ maximumKeys: MAXIMUM_KEYS }) From 4c09902c040af2c29d207aa9b6ea8e03421607a3 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 00:00:26 -0400 Subject: [PATCH 180/267] feat: add compiled relay manifest trust boundary Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 18 ++- ...github-release-implementation-checklist.md | 127 +++++++++++++++++- electron.vite.config.ts | 5 +- .../ssh-relay-compiled-manifest-trust.test.ts | 63 +++++++++ .../ssh/ssh-relay-compiled-manifest-trust.ts | 31 +++++ src/types/build-constants.d.ts | 4 + 8 files changed, 237 insertions(+), 14 deletions(-) create mode 100644 src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts create mode 100644 src/main/ssh/ssh-relay-compiled-manifest-trust.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index cff5861094d..3dabfc706b3 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -268,6 +268,7 @@ jobs: src/main/ssh/ssh-relay-artifact-cache-entry.test.ts \ src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts \ src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts \ + src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts \ src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts \ src/main/ssh/ssh-relay-packaged-manifest.test.ts \ src/main/ssh/ssh-relay-runtime-identity.test.ts \ @@ -804,6 +805,7 @@ jobs: src/main/ssh/ssh-relay-artifact-cache-entry.test.ts ` src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts ` src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts ` + src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts ` src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts ` src/main/ssh/ssh-relay-packaged-manifest.test.ts ` src/main/ssh/ssh-relay-runtime-identity.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 5807d274477..2c18781a2cf 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -126,6 +126,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-artifact-cache-entry.test.ts', 'ssh-relay-artifact-cache-in-use-lease.test.ts', 'ssh-relay-artifact-cache-eviction.test.ts', + 'ssh-relay-compiled-manifest-trust.test.ts', 'ssh-relay-manifest-accepted-keys.test.ts', 'ssh-relay-packaged-manifest.test.ts', 'ssh-relay-runtime-identity.test.ts' 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 ad6c2ad4e1e..0c64cea81fa 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 @@ -9,12 +9,18 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — disconnected desktop -accepted-public-key parser locally proven and wired; isolated commit/all-six native proof next, -2026-07-15, Codex implementation owner.** `E-M5-ACCEPTED-KEYS-AUDIT-001` and its RED precede -`E-M5-ACCEPTED-KEYS-LOCAL-001`: 4/4 focused, 19/19 trust, 304/304 non-full-size SSH relay, 12/12 -workflow/aggregate, typecheck, and lint gates pass. No production public key file or secret exists, -and no resource or consumer is connected. The prior `E-M5-PACKAGED-MANIFEST-LOCAL-001` passes 6/6 +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — disconnected compile-time +manifest trust-root provider locally green and wired; isolated commit/all-six native proof next, +2026-07-15, Codex implementation owner.** `E-M5-COMPILED-TRUST-AUDIT-001` and its RED precede +`E-M5-COMPILED-TRUST-LOCAL-001`: 3/3 focused, 22/22 trust, 307/307 non-full-size SSH relay, 279/279 +release-script, workflow-oracle, typecheck, lint, format, max-lines, and diff gates pass. The build +constant is literal `null`; no production key file, resource bytes, or consumer exists. Prior exact +head `11f367e66` passes all six primary native accepted-key jobs under +`E-M5-ACCEPTED-KEYS-CI-001` and Golden E2E; the artifact workflow is red only for the retained +Windows arm64 hosted build-26200 versus required-26100 floor gap. PR Checks attempt 1 failed one +renderer PTY ownership race in untouched code after 30,100 passes; attempt 2 has passed the complete +job, including tests, unpacked build, and packaged CLI smoke. Real Apple/SignPath rehearsal remains +a late explicit gate. The prior `E-M5-PACKAGED-MANIFEST-LOCAL-001` passes 6/6 purpose-named tests, 300/300 non-full-size SSH relay tests, 279/279 release-script tests, typecheck, lint, format, max-lines, and diff gates. `E-M5-PACKAGED-MANIFEST-CI-WIRING-LOCAL-001` pins the suite into both native job families; exact-head run 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 4fd3f1e74fc..683078759e2 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: checkpoint and obtain all-six native evidence for the locally proven disconnected accepted-public-key parser**. `E-M5-ACCEPTED-KEYS-LOCAL-001` proves strict protected-aggregate document parity and both native workflow command lists locally. Keep the production key file/seed/environment, signed embedded bytes, builder/resource/consumer/cache/downloader/SSH/settings/tuples/publication/default behavior absent until exact-head native proof passes.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — user authority covers end-to-end PR-contained implementation, commits, pushes, CI, rehearsals, and release writes; merging to `main` remains explicitly prohibited. Commit `b79bcbd04` passes all six native source/full-size cache jobs, PR Checks, and Golden E2E. The artifact workflow is red only for the retained hosted Windows arm64 build-26200 versus required-26100 floor rejection after its primary proof. No Electron, downloader, SSH, setting, tuple, publication, or default-path consumer exists. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: checkpoint and obtain all-six native proof for the locally green, disconnected compile-time manifest trust-root provider**. The build constant remains hardcoded to `null`; keep the production key file/seed/environment, signed embedded bytes, builder/resource/consumer/cache/downloader/SSH/settings/tuples/publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — `E-M5-ACCEPTED-KEYS-CI-001` closes all-six native accepted-key parser proof at exact head `11f367e6613b0cae61a1153b77a5be332fd4e763`; Golden E2E passes and the artifact workflow is red only for the retained hosted Windows arm64 build-26200 versus required-26100 floor rejection. The first PR Checks attempt reached 30,100 passing tests and failed one unrelated renderer PTY ownership race test in untouched code; attempt 2 passes the complete job, including full tests, unpacked build, and packaged CLI smoke. `E-M5-COMPILED-TRUST-LOCAL-001` is green locally and wired into both native job families. User authority covers end-to-end PR-contained implementation, commits, pushes, CI, rehearsals, and release writes; merging to `main` remains explicitly prohibited. No Electron resource, downloader, SSH, setting, tuple, publication, or default-path consumer exists. 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) @@ -14008,6 +14008,119 @@ config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-manifest-accepted- requires the production accepted-key file to be absent; no key/seed/environment, resource bytes, builder mapping, Electron/cache/network/SSH/mode/tuple/publication/default caller is added. +### E-M5-ACCEPTED-KEYS-CI-001 — All-six native accepted-key document proof + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed implementation head `11f367e6613b0cae61a1153b77a5be332fd4e763` on draft PR + #8741. +- Workflow: [SSH Relay Runtime Artifacts run + 29467318532](https://github.com/stablyai/orca/actions/runs/29467318532). The purpose-named accepted-key + suite runs in `Run runtime artifact contract tests` under Node 24.18.0 on both native shell + families. All six primary contract steps and complete jobs pass: + - Linux x64, `ubuntu-24.04`, job + [87523112196](https://github.com/stablyai/orca/actions/runs/29467318532/job/87523112196), + contract step 02:45:28–02:45:43Z; job 02:45:00–02:47:39Z; + - Linux arm64, `ubuntu-24.04-arm`, job + [87523112194](https://github.com/stablyai/orca/actions/runs/29467318532/job/87523112194), + contract step 02:45:39–02:45:52Z; job 02:45:03–02:47:45Z; + - macOS x64, `macos-15-intel`, job + [87523112227](https://github.com/stablyai/orca/actions/runs/29467318532/job/87523112227), + contract step 02:46:19–02:47:07Z; job 02:45:02–02:49:48Z; + - macOS arm64, `macos-15`, job + [87523112181](https://github.com/stablyai/orca/actions/runs/29467318532/job/87523112181), + contract step 02:45:39–02:45:52Z; job 02:45:00–02:46:31Z; + - Windows x64, `windows-2022`, job + [87523112207](https://github.com/stablyai/orca/actions/runs/29467318532/job/87523112207), + contract step 02:46:18–02:46:43Z; job 02:45:00–02:50:59Z; + - Windows arm64, `windows-11-arm`, job + [87523112204](https://github.com/stablyai/orca/actions/runs/29467318532/job/87523112204), + contract step 02:48:32–02:49:06Z; job 02:44:59–02:54:31Z. +- Supplemental results: both Linux oldest-userland jobs and Windows x64 oldest-floor job pass. + Windows arm64 primary proof passes; its separate job + [87524225012](https://github.com/stablyai/orca/actions/runs/29467318532/job/87524225012) + executes the exact runtime successfully and then rejects hosted build 26200 against required build 26100. The overall artifact run is expected red only for that declared floor gap; no tuple is + enabled. +- Adjacent gates: [Golden E2E run + 29467318535](https://github.com/stablyai/orca/actions/runs/29467318535) passes Linux job + [87523101812](https://github.com/stablyai/orca/actions/runs/29467318535/job/87523101812) and macOS + job [87523101778](https://github.com/stablyai/orca/actions/runs/29467318535/job/87523101778). + [PR Checks run 29467318537](https://github.com/stablyai/orca/actions/runs/29467318537) attempt 1 + fails after 30,100 passed / 57 skipped tests only in untouched + `src/renderer/src/components/terminal-pane/pty-connection.test.ts` (“drops confirmed idle exit + when a different hook owner appears between null samples”). Failed-job attempt 2, job + [87530011046](https://github.com/stablyai/orca/actions/runs/29467318537/job/87530011046), passes + 03:43:02–03:57:22Z, including the full test step, unpacked build, and packaged CLI smoke. This + classifies the first failure as unrelated/flaky rather than an accepted product regression. +- Closure: the strict accepted-key format, canonical fingerprint, key cloning/freezing, and hostile + input behavior execute on all six supported native client families. This does not provision a + production key, embed a manifest, run native signing, publish assets, connect SSH, or enable a + tuple/default path. + +### E-M5-COMPILED-TRUST-AUDIT-001 — Immutable desktop trust-root boundary selection + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: exact pushed head `11f367e6613b0cae61a1153b77a5be332fd4e763`; draft PR #8741. +- Audit: complete reads of the packaged manifest loader, accepted-key parser, manifest signature + verifier, Electron Vite compile-time constant boundary, Electron builder resource mapping and + tests, protected aggregate accepted-key reader, and Milestone 4/5 embedding contracts. +- Findings: the manifest loader correctly receives accepted keys from its caller, but no immutable + desktop provider exists. A mutable adjacent accepted-key resource would let untrusted manifest + bytes move their own trust root and remains prohibited. The smallest safe package is a main-bundle + compile-time constant that is `null` in all current builds plus a strict provider that reuses the + proven accepted-key parser. Runtime environment variables, filesystem lookup, Electron resources, + production keys, signed manifest bytes, and consumers remain outside this package. +- Selected RED: a purpose-named source test requires unprovisioned builds to return unavailable even + when a similarly named runtime environment variable is set; valid build-injected documents must + yield cloned/frozen keys and the canonical fingerprint; malformed input must fail closed. + +### E-M5-COMPILED-TRUST-LOCAL-RED-001 — Compile-time desktop trust provider is absent + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: purpose-named `src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts` atop exact pushed + head `11f367e6613b0cae61a1153b77a5be332fd4e763`; provider deliberately absent. +- Exact command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts`. +- Result: required RED; one failed suite / zero collected tests in 504ms because + `./ssh-relay-compiled-manifest-trust` does not exist. No production key, resource, consumer, SSH + behavior, mode, tuple, publication, or default path changed. + +### E-M5-COMPILED-TRUST-LOCAL-001 — Unprovisioned compile-time trust boundary + +- Date: 2026-07-15 +- Owner: Codex implementation owner +- Source: working package atop exact pushed head + `11f367e6613b0cae61a1153b77a5be332fd4e763`; implementation/test are + `src/main/ssh/ssh-relay-compiled-manifest-trust.ts` and its purpose-named test. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts` — PASS, 1 file / 3 tests in + 2.00s Vitest / 7.39s wall, 132,743,168-byte maximum RSS, 97,031,712-byte peak footprint, zero + swaps. +- Proven behavior: the main-bundle constant is exactly literal `null`; an unprovisioned build stays + unavailable even when a similarly named runtime environment variable is set; an explicitly + supplied compile-time document reuses the strict parser and returns cloned/frozen accepted keys + plus its canonical SHA-256 fingerprint; malformed injected state fails closed. +- Regression commands/results on the final local state: + - compiled/accepted/signature/packaged-loader trust suites pass 4 files / 22 tests in 14.71s; + - complete `src/main/ssh/ssh-relay-*.test.ts` passes 24 files / 307 tests with three declared + full-size suites/tests skipped in 26.34s Vitest / 30.43s wall, 261,341,184-byte maximum RSS and + zero swaps; + - complete `config/scripts/ssh-relay-runtime-*.test.mjs` passes 50 files / 279 tests in 50.75s + Vitest / 54.71s wall, 189,218,816-byte maximum RSS and zero swaps; + - the workflow oracle passes 7/7; full `pnpm typecheck` and `pnpm lint` pass, including 41 + reliability gates, 355 grandfathered max-lines suppressions with no new bypass, and localization + parity/coverage; focused `oxfmt --check`, `git diff --check`, and protected resolver zero-diff + checks pass. +- Native wiring: the purpose suite appears exactly once in each POSIX/PowerShell command list and + the workflow oracle requires exactly two occurrences. Exact-head all-six Node 24 execution remains + required before this source gate closes. +- Consumer-disconnection oracle: the provider is imported only by its purpose test; repository + search and `test ! -e config/ssh-relay-runtime-manifest-accepted-keys.json` confirm that no real + key file, mutable trust lookup, resource bytes/mapping, manifest consumer, cache/network/SSH/mode, + tuple, publication, or default behavior exists. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -14066,11 +14179,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Audit the smallest PR-contained accepted-key and immutable resource-input boundary now that -`E-M5-PACKAGED-MANIFEST-CI-001` closes the all-six native source-loader gate. Add a purpose-named RED -before implementation. Do not provision production secrets/environments, embed unreviewed keys or -manifest bytes, add cache-root/downloader orchestration, SSH transfer/install, mode wiring, tuple -enablement, release publication, or default behavior. +Checkpoint the disconnected compile-time trust-root package and require exact-head Node 24 proof on +Linux, macOS, and Windows x64/arm64. After that proof, audit the next smallest non-signing desktop +orchestration boundary; do not provision production secrets/environments, embed unreviewed keys or +manifest bytes, add SSH transfer/install, mode wiring, tuple enablement, release publication, or +default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 7b8c9cb8318..812bcf77b6f 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -199,7 +199,10 @@ export default defineConfig({ define: { ORCA_BUILD_IDENTITY: ORCA_BUILD_IDENTITY_LITERAL, ORCA_POSTHOG_WRITE_KEY: ORCA_POSTHOG_WRITE_KEY_LITERAL, - ORCA_DIAGNOSTICS_TOKEN_URL: ORCA_DIAGNOSTICS_TOKEN_URL_LITERAL + ORCA_DIAGNOSTICS_TOKEN_URL: ORCA_DIAGNOSTICS_TOKEN_URL_LITERAL, + // Why: leave the bundled-runtime trust root unavailable until reviewed production keys and + // immutable resource inputs are separately provisioned; this is never a runtime env lookup. + ORCA_SSH_RELAY_MANIFEST_ACCEPTED_KEYS: 'null' }, // Why: @xterm/headless declares "exports": null in package.json, which // prevents Vite's default resolver from finding the CJS entry. Point diff --git a/src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts b/src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts new file mode 100644 index 00000000000..ec392001678 --- /dev/null +++ b/src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts @@ -0,0 +1,63 @@ +import nacl from 'tweetnacl' +import { describe, expect, it } from 'vitest' + +import electronViteConfig from '../../../electron.vite.config' +import { + loadSshRelayCompiledManifestTrust, + parseSshRelayCompiledManifestTrust +} from './ssh-relay-compiled-manifest-trust' +import { sshRelayManifestKeyId } from './ssh-relay-manifest-signature' + +const keyPair = nacl.sign.keyPair.fromSeed(Uint8Array.from({ length: 32 }, (_, index) => index)) + +function acceptedKeyDocument() { + return { + schemaVersion: 1, + keys: [ + { + keyId: sshRelayManifestKeyId(keyPair.publicKey), + publicKeyBase64: Buffer.from(keyPair.publicKey).toString('base64') + } + ] + } +} + +describe('SSH relay compiled manifest trust', () => { + it('keeps unprovisioned builds unavailable without a runtime environment fallback', () => { + const original = process.env.ORCA_SSH_RELAY_MANIFEST_ACCEPTED_KEYS + process.env.ORCA_SSH_RELAY_MANIFEST_ACCEPTED_KEYS = JSON.stringify(acceptedKeyDocument()) + try { + expect(loadSshRelayCompiledManifestTrust()).toBeNull() + expect(parseSshRelayCompiledManifestTrust(null)).toBeNull() + } finally { + if (original === undefined) { + delete process.env.ORCA_SSH_RELAY_MANIFEST_ACCEPTED_KEYS + } else { + process.env.ORCA_SSH_RELAY_MANIFEST_ACCEPTED_KEYS = original + } + } + const config = electronViteConfig as { + main?: { define?: Record } + } + expect(config.main?.define?.ORCA_SSH_RELAY_MANIFEST_ACCEPTED_KEYS).toBe('null') + }) + + it('parses a build-injected accepted-key document into immutable trust state', () => { + const document = acceptedKeyDocument() + const trust = parseSshRelayCompiledManifestTrust(document) + + expect(trust).not.toBeNull() + expect(trust?.acceptedKeys).toHaveLength(1) + expect(trust?.acceptedKeys[0].keyId).toBe(document.keys[0].keyId) + expect(trust?.acceptedKeys[0].publicKey).not.toBe(keyPair.publicKey) + expect(trust?.acceptedKeysSha256).toMatch(/^sha256:[0-9a-f]{64}$/u) + expect(Object.isFrozen(trust)).toBe(true) + expect(Object.isFrozen(trust?.acceptedKeys)).toBe(true) + }) + + it('fails closed for malformed injected trust state', () => { + expect(() => parseSshRelayCompiledManifestTrust({ schemaVersion: 1, keys: [] })).toThrow( + /accepted|key/i + ) + }) +}) diff --git a/src/main/ssh/ssh-relay-compiled-manifest-trust.ts b/src/main/ssh/ssh-relay-compiled-manifest-trust.ts new file mode 100644 index 00000000000..24370818273 --- /dev/null +++ b/src/main/ssh/ssh-relay-compiled-manifest-trust.ts @@ -0,0 +1,31 @@ +import { parseSshRelayManifestAcceptedKeyDocument } from './ssh-relay-manifest-accepted-keys' +import type { SshRelayManifestAcceptedKey } from './ssh-relay-manifest-signature' +import type { SshRelayDigest } from './ssh-relay-runtime-identity' + +export type SshRelayCompiledManifestTrust = Readonly<{ + acceptedKeys: readonly Readonly[] + acceptedKeysSha256: SshRelayDigest +}> + +export function parseSshRelayCompiledManifestTrust( + document: unknown | null +): SshRelayCompiledManifestTrust | null { + if (document === null) { + return null + } + const parsed = parseSshRelayManifestAcceptedKeyDocument(document) + return Object.freeze({ + acceptedKeys: parsed.acceptedKeys, + acceptedKeysSha256: parsed.sha256 + }) +} + +export function loadSshRelayCompiledManifestTrust(): SshRelayCompiledManifestTrust | null { + // Why: official trust roots must be immutable main-bundle inputs; never consult a runtime env or + // a mutable file beside the untrusted manifest when the build has not provisioned one. + const document = + typeof ORCA_SSH_RELAY_MANIFEST_ACCEPTED_KEYS === 'undefined' + ? null + : ORCA_SSH_RELAY_MANIFEST_ACCEPTED_KEYS + return parseSshRelayCompiledManifestTrust(document) +} diff --git a/src/types/build-constants.d.ts b/src/types/build-constants.d.ts index 560de3420c6..ed166407b77 100644 --- a/src/types/build-constants.d.ts +++ b/src/types/build-constants.d.ts @@ -20,3 +20,7 @@ declare const ORCA_POSTHOG_WRITE_KEY: string | null // point a packaged build at a staging server without re-running the // release pipeline. declare const ORCA_DIAGNOSTICS_TOKEN_URL: string | null + +// Parsed only by the main bundle. Current builds inject null until the relay +// runtime manifest trust root is separately reviewed and provisioned. +declare const ORCA_SSH_RELAY_MANIFEST_ACCEPTED_KEYS: unknown | null From bb74936148c851a65da43e50364a7afa52c9eba1 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 00:12:14 -0400 Subject: [PATCH 181/267] fix: settle relay draft upload streams Co-authored-by: Orca --- .../ssh-relay-runtime-draft-upload.mjs | 13 ++++ .../ssh-relay-runtime-draft-upload.test.mjs | 17 +++++ ...elease-implementation-checklist-summary.md | 10 ++- ...github-release-implementation-checklist.md | 66 ++++++++++++++++++- 4 files changed, 104 insertions(+), 2 deletions(-) diff --git a/config/scripts/ssh-relay-runtime-draft-upload.mjs b/config/scripts/ssh-relay-runtime-draft-upload.mjs index 70bfa23bf6d..0dc80f371cf 100644 --- a/config/scripts/ssh-relay-runtime-draft-upload.mjs +++ b/config/scripts/ssh-relay-runtime-draft-upload.mjs @@ -295,6 +295,13 @@ async function uploadAsset(context, asset, signal, delayImpl) { let failure let response const body = createReadStream(asset.path) + let bodyFailure + const bodyClosed = new Promise((resolve) => { + body.once('error', (error) => { + bodyFailure = error + }) + body.once('close', resolve) + }) try { response = await context.fetchImpl( `https://uploads.github.com/repos/${context.repo}/releases/${context.releaseId}/assets?name=${encodeURIComponent(asset.name)}`, @@ -317,6 +324,12 @@ async function uploadAsset(context, asset, signal, delayImpl) { } finally { // Why: injected failures and early HTTP responses may not consume the request stream. body.destroy() + await bodyClosed + } + if (bodyFailure) { + throw new Error(`SSH relay runtime draft upload local asset stream failed: ${asset.name}`, { + cause: bodyFailure + }) } if (response?.status === 201) { const result = await response.json() diff --git a/config/scripts/ssh-relay-runtime-draft-upload.test.mjs b/config/scripts/ssh-relay-runtime-draft-upload.test.mjs index bc85d711b79..86a4b09478e 100644 --- a/config/scripts/ssh-relay-runtime-draft-upload.test.mjs +++ b/config/scripts/ssh-relay-runtime-draft-upload.test.mjs @@ -121,6 +121,23 @@ describe('SSH relay runtime draft upload', () => { }) }) + it('settles an unconsumed upload stream before returning', async () => { + let uploadBody + const fetchImpl = vi.fn(async (url, options = {}) => { + if (options.method === 'POST') { + uploadBody = options.body + return json(uploadedAsset(), 201) + } + return url.includes('/git/ref/tags/') ? json(tagReference()) : json(draft()) + }) + + await expect(uploadSshRelayRuntimeDraftAssets(input(fetchImpl))).resolves.toMatchObject({ + uploadedAssets: [{ name: NAME, sha256: digest(), size: BYTES.length }] + }) + expect(uploadBody).toBeDefined() + expect(uploadBody.closed).toBe(true) + }) + it('reuses only same-draft assets whose downloaded bytes match', async () => { const location = 'https://release-assets.githubusercontent.com/example/runtime?sig=signed' const fetchImpl = vi 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 0c64cea81fa..6a86ee13589 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 @@ -10,11 +10,19 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — disconnected compile-time -manifest trust-root provider locally green and wired; isolated commit/all-six native proof next, +manifest trust-root provider locally green and wired; CI RED corrected locally, replacement native proof next, 2026-07-15, Codex implementation owner.** `E-M5-COMPILED-TRUST-AUDIT-001` and its RED precede `E-M5-COMPILED-TRUST-LOCAL-001`: 3/3 focused, 22/22 trust, 307/307 non-full-size SSH relay, 279/279 release-script, workflow-oracle, typecheck, lint, format, max-lines, and diff gates pass. The build constant is literal `null`; no production key file, resource bytes, or consumer exists. Prior exact +head artifact run 29470322099 is not yet accepted: Linux arm64 job 87532052107 exposed an unawaited +draft-upload request-stream lifecycle (`E-M5-COMPILED-TRUST-CI-RED-001`), now deterministically +reproduced and locally corrected with 9/9 focused and 280/280 release-script tests under +`E-M5-DRAFT-UPLOAD-STREAM-SETTLEMENT-LOCAL-001`. Windows x64 job 87532052082 independently failed an +existing cache-lock concurrency test with `EPERM` plus teardown `ENOENT` +(`E-M5-COMPILED-TRUST-WINDOWS-CI-RED-001`). Replacement exact-head native proof is required; a +repeat Windows failure requires a separate correction. Real Apple/SignPath work remains deferred to +its late gate. Prior exact head `11f367e66` passes all six primary native accepted-key jobs under `E-M5-ACCEPTED-KEYS-CI-001` and Golden E2E; the artifact workflow is red only for the retained Windows arm64 hosted build-26200 versus required-26100 floor gap. PR Checks attempt 1 failed one 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 683078759e2..a8b24779e43 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: checkpoint and obtain all-six native proof for the locally green, disconnected compile-time manifest trust-root provider**. The build constant remains hardcoded to `null`; keep the production key file/seed/environment, signed embedded bytes, builder/resource/consumer/cache/downloader/SSH/settings/tuples/publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — `E-M5-ACCEPTED-KEYS-CI-001` closes all-six native accepted-key parser proof at exact head `11f367e6613b0cae61a1153b77a5be332fd4e763`; Golden E2E passes and the artifact workflow is red only for the retained hosted Windows arm64 build-26200 versus required-26100 floor rejection. The first PR Checks attempt reached 30,100 passing tests and failed one unrelated renderer PTY ownership race test in untouched code; attempt 2 passes the complete job, including full tests, unpacked build, and packaged CLI smoke. `E-M5-COMPILED-TRUST-LOCAL-001` is green locally and wired into both native job families. User authority covers end-to-end PR-contained implementation, commits, pushes, CI, rehearsals, and release writes; merging to `main` remains explicitly prohibited. No Electron resource, downloader, SSH, setting, tuple, publication, or default-path consumer exists. Legacy remains the production default.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — `E-M5-ACCEPTED-KEYS-CI-001` closes all-six native accepted-key parser proof at exact head `11f367e6613b0cae61a1153b77a5be332fd4e763`; Golden E2E passes and the artifact workflow is red only for the retained hosted Windows arm64 build-26200 versus required-26100 floor rejection. `E-M5-COMPILED-TRUST-LOCAL-001` is green locally and wired into both native job families. Exact-head artifact run 29470322099 is not yet accepted: Linux arm64 job 87532052107 exposed an unawaited draft-upload request-stream lifecycle (`E-M5-COMPILED-TRUST-CI-RED-001`), now reproduced and locally corrected under `E-M5-DRAFT-UPLOAD-STREAM-SETTLEMENT-LOCAL-001`; Windows x64 job 87532052082 independently failed an existing cache-lock concurrency test with `EPERM` plus teardown `ENOENT` (`E-M5-COMPILED-TRUST-WINDOWS-CI-RED-001`). Require replacement exact-head proof before recording all-six success. User authority covers end-to-end PR-contained implementation, commits, pushes, CI, rehearsals, and release writes; merging to `main` remains explicitly prohibited. Real Apple/SignPath work is deferred to its late gate. No Electron resource, downloader, SSH, setting, tuple, publication, or default-path consumer exists. 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) @@ -14121,6 +14121,70 @@ src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts`. key file, mutable trust lookup, resource bytes/mapping, manifest consumer, cache/network/SSH/mode, tuple, publication, or default behavior exists. +### E-M5-COMPILED-TRUST-CI-RED-001 — exact-head Linux arm64 upload-stream lifecycle failure + +- Date/owner: 2026-07-15, Codex implementation owner. +- Exact source: `4c09902c040af2c29d207aa9b6ea8e03421607a3`; artifact run + [29470322099](https://github.com/stablyai/orca/actions/runs/29470322099), failed primary job + [87532052107](https://github.com/stablyai/orca/actions/runs/29470322099/job/87532052107). +- Runner/runtime: GitHub-hosted `ubuntu-24.04-arm`, image `20260714.61.1`, Ubuntu 24.04.4 arm64, + Actions runner 2.335.1, Node 24.18.0. +- Result: the new compiled-manifest trust suite passed 1 file / 3 tests in 6 ms and the complete + contract command passed all 70 files / 473 tests, but Vitest then failed the step after 9.16 s + with one unhandled `ENOENT` opening the draft-upload suite's temporary local archive. Vitest + attributed the asynchronous exception to + `config/scripts/ssh-relay-runtime-draft-upload.test.mjs`, most recently while + `fails closed on unsafe draft state or existing managed bytes` ran. +- Interpretation: this is not evidence against the compiled trust contract, but the exact-head job + remains RED. The draft uploader creates a request `ReadStream`, destroys it when an injected or + early HTTP response does not consume it, and does not await terminal stream settlement before the + caller/test may remove the source directory. A purpose-named local RED and bounded lifecycle fix + are required before rerunning and accepting all-six native proof. +- Residual gap: the other exact-head jobs and workflows were still settling when recorded. Do not + issue `E-M5-COMPILED-TRUST-CI-001` until every primary native contract step, PR Checks, and Golden + E2E result is inspected and this failure is replaced by green exact-head evidence. + +### E-M5-DRAFT-UPLOAD-STREAM-SETTLEMENT-LOCAL-001 — request stream closes before caller cleanup + +- Date/owner: 2026-07-15, Codex implementation owner. +- Purpose-named RED command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +config/scripts/ssh-relay-runtime-draft-upload.test.mjs` — FAIL before the correction, 1 failed / 8 + passed in 547 ms; `settles an unconsumed upload stream before returning` observed + `ReadStream.closed === false` after the uploader resolved. +- Correction: attach the file-stream error/close observers before invoking the injectable fetch, + destroy an unconsumed request body, await its terminal close, and fail closed on a source-stream + error. This prevents caller/test staging cleanup from racing a delayed file open and preserves the + existing bounded retry, cancellation, exact-byte verification, and draft-reconciliation policy. +- Focused GREEN: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 config/scripts/ssh-relay-runtime-draft-upload.test.mjs` — PASS, 1 file / 9 tests in + 639 ms Vitest / 4.10 s wall, 132,366,336-byte maximum RSS, 96,572,840-byte peak footprint, zero + swaps. +- Regression GREEN: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 42.16 s + Vitest / 45.49 s wall, 189,628,416-byte maximum RSS, 96,179,528-byte peak footprint, zero swaps. + `pnpm typecheck`, `pnpm lint`, focused `oxfmt --check`, `git diff --check`, and protected resolver + zero-diff checks pass; lint includes 41 reliability gates, the 355-entry max-lines ratchet with no + new bypass, bundled-skill verification, and localization parity/coverage. +- Does not prove: replacement native CI, a real GitHub release write, publication, signing, desktop + manifest consumption, cache/download behavior, SSH transfer, mode wiring, or tuple enablement. + +### E-M5-COMPILED-TRUST-WINDOWS-CI-RED-001 — exact-head Windows x64 cache-lock race + +- Date/owner: 2026-07-15, Codex implementation owner. +- Exact source/job: `4c09902c040af2c29d207aa9b6ea8e03421607a3`, artifact run 29470322099, + [Windows x64 job 87532052082](https://github.com/stablyai/orca/actions/runs/29470322099/job/87532052082). +- Runner/runtime: GitHub-hosted Windows Server 2022 10.0.20348 x64, `windows-2022` image + `20260706.237.1`, Actions runner 2.335.1, Node 24.18.0. +- Result: the new compiled-trust suite passed 3/3. The combined command then ended with 1 failed, + 463 passed, 13 skipped, and one unhandled error in 17.80 s. The untouched + `serializes concurrent writers and transfers ownership only after owner release` cache-lock test + received `EPERM` renaming the released lock; its still-running waiter then received `ENOENT` after + test cleanup removed the lock root. +- Disposition: prior all-six cache-lock evidence exists, but this exact-head failure is not waived. + The replacement run must settle green without a test-only relaxation. If the `EPERM` repeats, + diagnose Windows handle ownership and implement a separately evidenced cache-lock correction + before accepting the compiled-trust checkpoint. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 749f775f15cbd92354fe43e4c142900d4b921614 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 00:39:52 -0400 Subject: [PATCH 182/267] feat: compose official relay manifest trust Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 26 +++- ...github-release-implementation-checklist.md | 97 +++++++++++- .../ssh/ssh-relay-official-manifest.test.ts | 141 ++++++++++++++++++ src/main/ssh/ssh-relay-official-manifest.ts | 31 ++++ 6 files changed, 284 insertions(+), 14 deletions(-) create mode 100644 src/main/ssh/ssh-relay-official-manifest.test.ts create mode 100644 src/main/ssh/ssh-relay-official-manifest.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 3dabfc706b3..2a368fc15b7 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -269,6 +269,7 @@ jobs: src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts \ src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts \ src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts \ + src/main/ssh/ssh-relay-official-manifest.test.ts \ src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts \ src/main/ssh/ssh-relay-packaged-manifest.test.ts \ src/main/ssh/ssh-relay-runtime-identity.test.ts \ @@ -806,6 +807,7 @@ jobs: src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts ` src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts ` src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts ` + src/main/ssh/ssh-relay-official-manifest.test.ts ` src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts ` src/main/ssh/ssh-relay-packaged-manifest.test.ts ` src/main/ssh/ssh-relay-runtime-identity.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 2c18781a2cf..c3869c10038 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -127,6 +127,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-artifact-cache-in-use-lease.test.ts', 'ssh-relay-artifact-cache-eviction.test.ts', 'ssh-relay-compiled-manifest-trust.test.ts', + 'ssh-relay-official-manifest.test.ts', 'ssh-relay-manifest-accepted-keys.test.ts', 'ssh-relay-packaged-manifest.test.ts', 'ssh-relay-runtime-identity.test.ts' 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 6a86ee13589..a5c7aeafcc9 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 @@ -9,20 +9,30 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — disconnected compile-time -manifest trust-root provider locally green and wired; CI RED corrected locally, replacement native proof next, -2026-07-15, Codex implementation owner.** `E-M5-COMPILED-TRUST-AUDIT-001` and its RED precede +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — disconnected official +manifest composition locally green and wired; commit/all-six native proof next, 2026-07-15, Codex +implementation owner.** `E-M5-OFFICIAL-MANIFEST-COMPOSITION-LOCAL-001` passes 4/4 focused, 26/26 +trust, 311/311 non-full-size SSH relay, 280/280 release-script, workflow-oracle, typecheck, lint, +format, max-lines, and diff gates. An unprovisioned build returns unavailable before resource access; +provisioned trust is the only accepted-key source and binds the verified fixed-resource manifest to +its canonical key fingerprint. No resource or product consumer exists. `E-M5-COMPILED-TRUST-CI-001` +closes the prior compiled trust and stream-settlement checkpoint at exact +head `bb7493614`: all six primary native artifact jobs, both Linux supplements, Windows x64 +baseline, PR Checks, Golden E2E, and unchanged computer-use retry pass. The artifact workflow is +red only because hosted Windows arm64 build 26200 correctly fails the required 26100 floor. The +next slice may compose immutable trust with the fixed-resource loader, but must add no resource, +production key/manifest, Electron/product consumer, cache/downloader, SSH, Beta mode, tuple, +publication, or default behavior. `E-M5-COMPILED-TRUST-AUDIT-001` and its RED precede `E-M5-COMPILED-TRUST-LOCAL-001`: 3/3 focused, 22/22 trust, 307/307 non-full-size SSH relay, 279/279 release-script, workflow-oracle, typecheck, lint, format, max-lines, and diff gates pass. The build -constant is literal `null`; no production key file, resource bytes, or consumer exists. Prior exact -head artifact run 29470322099 is not yet accepted: Linux arm64 job 87532052107 exposed an unawaited +constant is literal `null`; no production key file, resource bytes, or consumer exists. Superseded +artifact run 29470322099 exposed an unawaited draft-upload request-stream lifecycle (`E-M5-COMPILED-TRUST-CI-RED-001`), now deterministically reproduced and locally corrected with 9/9 focused and 280/280 release-script tests under `E-M5-DRAFT-UPLOAD-STREAM-SETTLEMENT-LOCAL-001`. Windows x64 job 87532052082 independently failed an existing cache-lock concurrency test with `EPERM` plus teardown `ENOENT` -(`E-M5-COMPILED-TRUST-WINDOWS-CI-RED-001`). Replacement exact-head native proof is required; a -repeat Windows failure requires a separate correction. Real Apple/SignPath work remains deferred to -its late gate. Prior exact +(`E-M5-COMPILED-TRUST-WINDOWS-CI-RED-001`); neither failure repeated at the accepted replacement +head. Real Apple/SignPath work remains deferred to its late gate. Prior exact head `11f367e66` passes all six primary native accepted-key jobs under `E-M5-ACCEPTED-KEYS-CI-001` and Golden E2E; the artifact workflow is red only for the retained Windows arm64 hosted build-26200 versus required-26100 floor gap. PR Checks attempt 1 failed one 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 a8b24779e43..0d202fb13d4 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: checkpoint and obtain all-six native proof for the locally green, disconnected compile-time manifest trust-root provider**. The build constant remains hardcoded to `null`; keep the production key file/seed/environment, signed embedded bytes, builder/resource/consumer/cache/downloader/SSH/settings/tuples/publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — `E-M5-ACCEPTED-KEYS-CI-001` closes all-six native accepted-key parser proof at exact head `11f367e6613b0cae61a1153b77a5be332fd4e763`; Golden E2E passes and the artifact workflow is red only for the retained hosted Windows arm64 build-26200 versus required-26100 floor rejection. `E-M5-COMPILED-TRUST-LOCAL-001` is green locally and wired into both native job families. Exact-head artifact run 29470322099 is not yet accepted: Linux arm64 job 87532052107 exposed an unawaited draft-upload request-stream lifecycle (`E-M5-COMPILED-TRUST-CI-RED-001`), now reproduced and locally corrected under `E-M5-DRAFT-UPLOAD-STREAM-SETTLEMENT-LOCAL-001`; Windows x64 job 87532052082 independently failed an existing cache-lock concurrency test with `EPERM` plus teardown `ENOENT` (`E-M5-COMPILED-TRUST-WINDOWS-CI-RED-001`). Require replacement exact-head proof before recording all-six success. User authority covers end-to-end PR-contained implementation, commits, pushes, CI, rehearsals, and release writes; merging to `main` remains explicitly prohibited. Real Apple/SignPath work is deferred to its late gate. No Electron resource, downloader, SSH, setting, tuple, publication, or default-path consumer exists. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: checkpoint and obtain all-six native proof for the locally green disconnected official-manifest composition**. Keep the build constant hardcoded to `null` and the production key file/seed/environment, signed embedded bytes, builder/resource/product consumer/cache/downloader/SSH/settings/tuples/publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — `E-M5-COMPILED-TRUST-CI-001` closes the prior provider/stream package at exact head `bb74936148c851a65da43e50364a7afa52c9eba1`. `E-M5-OFFICIAL-MANIFEST-COMPOSITION-LOCAL-001` is now locally green and wired into both native job families: absent trust does not inspect resources; present trust is the only signature authority and binds the verified manifest to its canonical key fingerprint. Commit/push and require replacement all-six Node 24 proof before advancing. Merging to `main` remains prohibited and SignPath is deferred. No Electron resource, downloader, SSH, setting, tuple, publication, or default-path consumer exists. 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) @@ -142,10 +142,10 @@ same change as the work it records. accounting, and bounded 2 GiB eviction pass locally and on all six native clients under E-M5-ARTIFACT-CACHE-LOCK-CI-001, E-M5-ARTIFACT-CACHE-ENTRY-CI-001, and E-M5-ARTIFACT-CACHE-EVICTION-CI-001. No production consumer exists. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit the smallest - disconnected official-build packaged-manifest/key and app-owned cache-root boundary before adding - purpose-named RED contracts. Keep downloader orchestration, SSH behavior, mode wiring, tuple - enablement, release publication, and defaults intact until their separate gates. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — checkpoint the locally + green immutable-trust/fixed-resource composition and require all-six Node 24 native proof. Keep + every Electron/product caller, resource mapping, production key/manifest, + cache/downloader/SSH/mode/tuple/publication/default intact until separate gates. - 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 @@ -14185,6 +14185,91 @@ config/scripts/ssh-relay-runtime-draft-upload.test.mjs` — FAIL before the corr diagnose Windows handle ownership and implement a separately evidenced cache-lock correction before accepting the compiled-trust checkpoint. +### E-M5-COMPILED-TRUST-CI-001 — replacement all-six native trust and stream proof + +- Date/owner: 2026-07-15, Codex implementation owner. +- Exact source: `bb74936148c851a65da43e50364a7afa52c9eba1`; artifact run + [29470805823](https://github.com/stablyai/orca/actions/runs/29470805823). +- Primary native jobs: Windows x64 87533468657 (5m58s), Windows arm64 87533468659 (11m16s), Linux + x64 87533468688 (2m31s), Linux arm64 87533468710 (6m25s), macOS x64 87533468740 (5m06s), and + macOS arm64 87533468700 (1m51s) all pass. Every contract step runs the purpose-named compiled + trust suite 3/3 and corrected draft-upload suite 9/9; combined contract files are 70/70 on POSIX + and 71/71 on Windows. Full-size extraction/cache measurements and unpublished artifact upload + also pass in every primary job. +- Supplemental result: Linux x64/arm64 oldest-userland jobs 87534233310/87534233331 and Windows x64 + baseline job 87534771591 pass. Windows arm64 baseline job 87534771618 correctly rejects hosted + Windows 11 build 26200 against the required exact build 26100 (`osBuild: false`), leaving the + workflow deliberately red and that tuple disabled. +- Adjacent proof: PR Checks run + [29470805820](https://github.com/stablyai/orca/actions/runs/29470805820), job 87533468500, passes in + 15m00s including lint/static gates, typecheck, Git 2.25 matrix, full tests, unpacked app build, and + packaged CLI smoke. Golden E2E run + [29470805841](https://github.com/stablyai/orca/actions/runs/29470805841) passes Linux job + 87533468519 and macOS job 87533468514. Computer-use run 29470805828 attempt 1 timed out an + untouched Windows SSH-launcher test at 5.137s against its 5s bound; unchanged attempt 2 passes + Windows native smoke/E2E in job 87534149625, so no source relaxation was made. +- Closure: the compile-time trust provider and stream-settlement correction are proven on all six + native Node 24 families. No production accepted key, manifest resource, Electron/product caller, + cache/downloader, SSH path, mode, tuple, release write, or default behavior was added. + +### E-M5-OFFICIAL-MANIFEST-COMPOSITION-AUDIT-001 — disconnected trusted-loader boundary + +- Date/owner: 2026-07-15, Codex implementation owner. +- Audit: complete reads of `ssh-relay-compiled-manifest-trust.ts`, + `ssh-relay-packaged-manifest.ts`, their tests, the accepted-key parser/signature verifier, build + constant declaration/injection, workflow wiring, and all non-test imports/callers. +- Findings: the immutable provider and fixed-resource loader are individually proven but have no + composition. The loader still requires caller-supplied accepted keys, and neither module has a + non-test consumer. The smallest safe next capability is a concrete + `ssh-relay-official-manifest.ts` boundary that obtains trust only from + `loadSshRelayCompiledManifestTrust`, returns unavailable before filesystem access when the build + constant is `null`, otherwise invokes the real packaged loader, and returns a frozen verified + manifest plus the canonical accepted-key fingerprint. +- Required RED: unavailable trust does not inspect a resource path; provisioned test trust verifies + an exact signed packaged manifest and desktop identity; unknown signatures, malformed resources, + and unpackaged use fail closed; callers cannot supply alternate accepted keys. Wire the suite into + both native job families. +- Deliberately absent: Electron `app`/`process.resourcesPath` adapter, `extraResources`, production + key/manifest bytes, cache root/orchestration, downloader, `ORCA_RELAY_PATH`, SSH, Beta setting, + fallback, tuple enablement, publication, and every default-path change. + +### E-M5-OFFICIAL-MANIFEST-COMPOSITION-LOCAL-RED-001 — composition module is absent + +- Date/owner: 2026-07-15, Codex implementation owner. +- Command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-official-manifest.test.ts`. +- Result: expected FAIL, 1 failed suite / 0 tests in 3.26 s because + `./ssh-relay-official-manifest` did not exist. The purpose-named suite defines unavailable-trust, + real signed-resource/fingerprint, hostile caller-key override, malformed resource, and unpackaged + fail-closed contracts before implementation. +- Consumer-disconnection oracle: only the purpose test imports the absent module; there is no + Electron/product/resource/cache/downloader/SSH/mode/tuple/publication/default call site. + +### E-M5-OFFICIAL-MANIFEST-COMPOSITION-LOCAL-001 — compiled trust owns packaged verification + +- Date/owner: 2026-07-15, Codex implementation owner. +- Implementation: `src/main/ssh/ssh-relay-official-manifest.ts` owns only the official trust/loader + composition. It returns `null` before filesystem access when compiled trust is unavailable; + otherwise it passes only the compiled accepted keys to the proven fixed-resource loader and + returns a frozen verified manifest plus canonical accepted-key fingerprint. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-official-manifest.test.ts` — PASS, 1 file / 4 tests in 656 ms + Vitest / 2.73 s wall, 133,300,224-byte maximum RSS, 96,097,800-byte peak footprint, zero swaps. +- Trust regression: compiled trust, official composition, packaged loader, accepted-key parser, and + signature suites pass 5 files / 26 tests in 9.87 s. Complete non-full-size SSH relay tests pass 25 + files / 311 tests with three declared full-size skips in 20.55 s Vitest / 22.52 s wall, + 263,864,320-byte maximum RSS, 95,802,624-byte peak footprint, zero swaps. +- Release/static regression: 50 release-script files / 280 tests pass in 32.28 s Vitest / 38.56 s + wall, 193,921,024-byte maximum RSS, 97,146,304-byte peak footprint, zero swaps; workflow oracle + passes 7/7. `pnpm typecheck`, full `pnpm lint`, focused `oxfmt --check`, `git diff --check`, and + protected resolver zero-diff checks pass. Lint includes 41 reliability gates, 355 grandfathered + max-lines entries with no new bypass, bundled-skill verification, and localization parity/coverage. +- Native wiring: the purpose suite appears once in each POSIX/PowerShell command family and the + workflow oracle requires both occurrences. Exact-head all-six Node 24 proof remains required. +- Consumer disconnection: outside the purpose test/workflow references, repository search finds only + the new module's export. There is no Electron/app adapter, resource mapping, production key or + manifest, cache/downloader/SSH/mode/tuple/publication/default consumer. Legacy remains unchanged. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/src/main/ssh/ssh-relay-official-manifest.test.ts b/src/main/ssh/ssh-relay-official-manifest.test.ts new file mode 100644 index 00000000000..d0c1fa8ba3b --- /dev/null +++ b/src/main/ssh/ssh-relay-official-manifest.test.ts @@ -0,0 +1,141 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import nacl from 'tweetnacl' +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { createSshRelayArtifactTestManifest } from './ssh-relay-artifact-test-manifest' +import { parseSshRelayCompiledManifestTrust } from './ssh-relay-compiled-manifest-trust' +import type * as CompiledManifestTrustModule from './ssh-relay-compiled-manifest-trust' +import { + loadSshRelayOfficialManifest, + type SshRelayOfficialManifestLoadOptions +} from './ssh-relay-official-manifest' +import { sshRelayPackagedManifestPath } from './ssh-relay-packaged-manifest' +import { signSshRelayArtifactManifest, sshRelayManifestKeyId } from './ssh-relay-manifest-signature' + +const trustMocks = vi.hoisted(() => ({ load: vi.fn() })) + +vi.mock('./ssh-relay-compiled-manifest-trust', async (importOriginal) => { + const actual = await importOriginal() + return { ...actual, loadSshRelayCompiledManifestTrust: trustMocks.load } +}) + +const temporaryDirectories: string[] = [] +const trustedKeyPair = nacl.sign.keyPair.fromSeed( + Uint8Array.from({ length: 32 }, (_, index) => index) +) +const untrustedKeyPair = nacl.sign.keyPair.fromSeed( + Uint8Array.from({ length: 32 }, (_, index) => 31 - index) +) + +function compiledTrust() { + return parseSshRelayCompiledManifestTrust({ + schemaVersion: 1, + keys: [ + { + keyId: sshRelayManifestKeyId(trustedKeyPair.publicKey), + publicKeyBase64: Buffer.from(trustedKeyPair.publicKey).toString('base64') + } + ] + }) +} + +function signedManifest(keyPair = trustedKeyPair) { + const manifest = createSshRelayArtifactTestManifest() + manifest.build = { + tag: 'v1.4.140-rc.1', + version: '1.4.140-rc.1', + channel: 'rc', + relayProtocolVersion: 1 + } + manifest.signatures = [signSshRelayArtifactManifest(manifest, keyPair.secretKey)] + return manifest +} + +async function resourcesRoot(): Promise { + const root = await mkdtemp(join(tmpdir(), 'orca-relay-official-manifest-')) + temporaryDirectories.push(root) + return join(root, 'Resources') +} + +async function writeManifest(resourcesPath: string, value: unknown): Promise { + await mkdir(join(resourcesPath, 'ssh-relay-runtime'), { recursive: true }) + await writeFile(sshRelayPackagedManifestPath(resourcesPath), JSON.stringify(value)) +} + +function loadOptions(resourcesPath: string): SshRelayOfficialManifestLoadOptions { + return { + packaged: true, + resourcesPath, + appVersion: '1.4.140-rc.1', + relayProtocolVersion: 1 + } +} + +afterEach(async () => { + trustMocks.load.mockReset() + await Promise.all( + temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true })) + ) +}) + +describe('SSH relay official manifest', () => { + it('returns unavailable without inspecting resources when compiled trust is absent', async () => { + trustMocks.load.mockReturnValue(null) + const resourcesPath = join(tmpdir(), 'missing-orca-relay-resources') + + await expect(loadSshRelayOfficialManifest(loadOptions(resourcesPath))).resolves.toBeNull() + expect(trustMocks.load).toHaveBeenCalledTimes(1) + }) + + it('binds a verified packaged manifest to the compiled accepted-key fingerprint', async () => { + const resourcesPath = await resourcesRoot() + const trust = compiledTrust() + trustMocks.load.mockReturnValue(trust) + await writeManifest(resourcesPath, signedManifest()) + + const result = await loadSshRelayOfficialManifest(loadOptions(resourcesPath)) + + expect(result).toMatchObject({ + acceptedKeysSha256: trust?.acceptedKeysSha256, + manifest: { build: { tag: 'v1.4.140-rc.1', relayProtocolVersion: 1 } } + }) + expect(Object.isFrozen(result)).toBe(true) + expect(Object.isFrozen(result?.manifest)).toBe(true) + }) + + it('cannot be redirected to caller-supplied accepted keys', async () => { + const resourcesPath = await resourcesRoot() + trustMocks.load.mockReturnValue(compiledTrust()) + await writeManifest(resourcesPath, signedManifest(untrustedKeyPair)) + const hostileOptions = { + ...loadOptions(resourcesPath), + acceptedKeys: [ + { + keyId: sshRelayManifestKeyId(untrustedKeyPair.publicKey), + publicKey: untrustedKeyPair.publicKey + } + ] + } as SshRelayOfficialManifestLoadOptions + + await expect(loadSshRelayOfficialManifest(hostileOptions)).rejects.toThrow( + /unknown signing key/i + ) + }) + + it('propagates packaged-resource and official-build failures closed', async () => { + const resourcesPath = await resourcesRoot() + trustMocks.load.mockReturnValue(compiledTrust()) + await mkdir(join(resourcesPath, 'ssh-relay-runtime'), { recursive: true }) + await writeFile(sshRelayPackagedManifestPath(resourcesPath), '{malformed manifest shape') + + await expect(loadSshRelayOfficialManifest(loadOptions(resourcesPath))).rejects.toThrow( + /manifest|schema|field/i + ) + await expect( + loadSshRelayOfficialManifest({ ...loadOptions(resourcesPath), packaged: false }) + ).rejects.toThrow(/packaged/i) + }) +}) diff --git a/src/main/ssh/ssh-relay-official-manifest.ts b/src/main/ssh/ssh-relay-official-manifest.ts new file mode 100644 index 00000000000..39c6b5bd429 --- /dev/null +++ b/src/main/ssh/ssh-relay-official-manifest.ts @@ -0,0 +1,31 @@ +import { loadSshRelayCompiledManifestTrust } from './ssh-relay-compiled-manifest-trust' +import { loadSshRelayPackagedManifest } from './ssh-relay-packaged-manifest' +import type { VerifiedSshRelayArtifactManifest } from './ssh-relay-manifest-signature' +import type { SshRelayDigest } from './ssh-relay-runtime-identity' + +export type SshRelayOfficialManifestLoadOptions = Readonly<{ + packaged: boolean + resourcesPath: string + appVersion: string + relayProtocolVersion: number +}> + +export type SshRelayOfficialManifest = Readonly<{ + manifest: VerifiedSshRelayArtifactManifest + acceptedKeysSha256: SshRelayDigest +}> + +export async function loadSshRelayOfficialManifest( + options: SshRelayOfficialManifestLoadOptions +): Promise { + const trust = loadSshRelayCompiledManifestTrust() + if (trust === null) { + // Why: an unprovisioned build has no authority to inspect or classify mutable manifest bytes. + return null + } + const manifest = await loadSshRelayPackagedManifest({ + ...options, + acceptedKeys: trust.acceptedKeys + }) + return Object.freeze({ manifest, acceptedKeysSha256: trust.acceptedKeysSha256 }) +} From aefcaa9a938710e275e3b49ab71446f73b01e075 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 01:00:43 -0400 Subject: [PATCH 183/267] feat: define relay artifact cache root Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 19 +++- ...github-release-implementation-checklist.md | 101 ++++++++++++++++-- .../ssh/ssh-relay-artifact-cache-root.test.ts | 49 +++++++++ src/main/ssh/ssh-relay-artifact-cache-root.ts | 18 ++++ 6 files changed, 181 insertions(+), 9 deletions(-) create mode 100644 src/main/ssh/ssh-relay-artifact-cache-root.test.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-root.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 2a368fc15b7..3577813a4ec 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -268,6 +268,7 @@ jobs: src/main/ssh/ssh-relay-artifact-cache-entry.test.ts \ src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts \ src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts \ + src/main/ssh/ssh-relay-artifact-cache-root.test.ts \ src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts \ src/main/ssh/ssh-relay-official-manifest.test.ts \ src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts \ @@ -806,6 +807,7 @@ jobs: src/main/ssh/ssh-relay-artifact-cache-entry.test.ts ` src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts ` src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts ` + src/main/ssh/ssh-relay-artifact-cache-root.test.ts ` src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts ` src/main/ssh/ssh-relay-official-manifest.test.ts ` src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index c3869c10038..ebda96bf749 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -126,6 +126,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-artifact-cache-entry.test.ts', 'ssh-relay-artifact-cache-in-use-lease.test.ts', 'ssh-relay-artifact-cache-eviction.test.ts', + 'ssh-relay-artifact-cache-root.test.ts', 'ssh-relay-compiled-manifest-trust.test.ts', 'ssh-relay-official-manifest.test.ts', 'ssh-relay-manifest-accepted-keys.test.ts', 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 a5c7aeafcc9..e4c1d48fd94 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 @@ -9,9 +9,22 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — disconnected official -manifest composition locally green and wired; commit/all-six native proof next, 2026-07-15, Codex -implementation owner.** `E-M5-OFFICIAL-MANIFEST-COMPOSITION-LOCAL-001` passes 4/4 focused, 26/26 +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — disconnected app-owned +cache-root locally green and wired; commit/all-six native proof next, 2026-07-15, Codex +implementation owner.** `E-M5-ARTIFACT-CACHE-ROOT-LOCAL-001` passes 3/3 purpose tests, the native +workflow oracle, 314/314 non-full-size SSH relay tests, 280/280 release-script tests, typecheck, +lint, format, reliability, max-lines, localization, and diff gates. Both POSIX and PowerShell native +artifact commands now run the purpose suite. The pure function accepts only an absolute +caller-supplied native user-data path and returns a fixed `ssh-relay-runtime-cache/v1` namespace; +an environment variable cannot redirect it. No Electron caller, filesystem I/O, cache operation, +downloader, SSH/Beta mode/tuple/publication/default behavior exists. +`E-M5-OFFICIAL-MANIFEST-COMPOSITION-CI-001` closes the official-manifest composition at exact head +`749f775f1`: all six primary native jobs, both Linux supplements, Windows x64 baseline, PR Checks, +Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. +The next slice may derive only a pure fixed cache root from an absolute caller-supplied native +user-data path; it must add no Electron caller, filesystem I/O, cache orchestration, environment +override, downloader/SSH/Beta mode/tuple/publication/default behavior. +`E-M5-OFFICIAL-MANIFEST-COMPOSITION-LOCAL-001` passes 4/4 focused, 26/26 trust, 311/311 non-full-size SSH relay, 280/280 release-script, workflow-oracle, typecheck, lint, format, max-lines, and diff gates. An unprovisioned build returns unavailable before resource access; provisioned trust is the only accepted-key source and binds the verified fixed-resource manifest to 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 0d202fb13d4..a24590f087f 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: checkpoint and obtain all-six native proof for the locally green disconnected official-manifest composition**. Keep the build constant hardcoded to `null` and the production key file/seed/environment, signed embedded bytes, builder/resource/product consumer/cache/downloader/SSH/settings/tuples/publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — `E-M5-COMPILED-TRUST-CI-001` closes the prior provider/stream package at exact head `bb74936148c851a65da43e50364a7afa52c9eba1`. `E-M5-OFFICIAL-MANIFEST-COMPOSITION-LOCAL-001` is now locally green and wired into both native job families: absent trust does not inspect resources; present trust is the only signature authority and binds the verified manifest to its canonical key fingerprint. Commit/push and require replacement all-six Node 24 proof before advancing. Merging to `main` remains prohibited and SignPath is deferred. No Electron resource, downloader, SSH, setting, tuple, publication, or default-path consumer exists. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the smallest disconnected app-owned relay artifact cache-root contract**. Keep Electron/product consumers, cache orchestration, the build constant, production key/manifest/resources, downloader/SSH/settings/tuples/publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — `E-M5-OFFICIAL-MANIFEST-COMPOSITION-CI-001` closes the immutable-trust/fixed-resource composition at exact head `749f775f15cbd92354fe43e4c142900d4b921614`: all six primary native jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. Hosted Windows arm64 build 26200 remains correctly rejected against required 26100. Begin only the disconnected cache-root audit/RED; merging to `main` remains prohibited and SignPath is deferred. No Electron resource, downloader, SSH, setting, tuple, publication, or default-path consumer exists. 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) @@ -142,10 +142,10 @@ same change as the work it records. accounting, and bounded 2 GiB eviction pass locally and on all six native clients under E-M5-ARTIFACT-CACHE-LOCK-CI-001, E-M5-ARTIFACT-CACHE-ENTRY-CI-001, and E-M5-ARTIFACT-CACHE-EVICTION-CI-001. No production consumer exists. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — checkpoint the locally - green immutable-trust/fixed-resource composition and require all-six Node 24 native proof. Keep - every Electron/product caller, resource mapping, production key/manifest, - cache/downloader/SSH/mode/tuple/publication/default intact until separate gates. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit a pure app-owned + relay artifact cache-root derivation from a caller-supplied native user-data path. Keep Electron + `app.getPath`, directory creation, cache lookup/publication/eviction orchestration, environment + overrides, downloader, SSH, mode, tuple, publication, and defaults separately gated. - 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 @@ -14270,6 +14270,95 @@ src/main/ssh/ssh-relay-official-manifest.test.ts`. the new module's export. There is no Electron/app adapter, resource mapping, production key or manifest, cache/downloader/SSH/mode/tuple/publication/default consumer. Legacy remains unchanged. +### E-M5-OFFICIAL-MANIFEST-COMPOSITION-CI-001 — all-six native official-manifest proof + +- Date/owner: 2026-07-15, Codex implementation owner. +- Exact source/workflow: `749f775f15cbd92354fe43e4c142900d4b921614`, artifact run + [29471918803](https://github.com/stablyai/orca/actions/runs/29471918803). +- Primary jobs: Windows x64 87536613611 (6m11s), Windows arm64 87536613560 (9m25s), Linux x64 + 87536613622 (2m31s), Linux arm64 87536613598 (7m13s), macOS x64 87536613625 (6m37s), and macOS + arm64 87536613572 (1m38s) all pass completely. The purpose suite passes 4/4 on every native Node + 24.18.0 client; combined contract files pass 71/71 on POSIX and 72/72 on Windows. Full-size + extraction/cache measurement and unpublished artifact upload also pass in every primary job. +- Supplements: Linux x64/arm64 jobs 87537465974/87537465975 and Windows x64 baseline job + 87537719676 pass. Windows arm64 baseline job 87537719631 verifies and executes the exact bytes, + then correctly rejects hosted Windows 11 build 26200 against exact required build 26100 + (`osBuild: false`); the tuple remains disabled and the overall workflow deliberately red. +- Adjacent proof: PR Checks run + [29471918846](https://github.com/stablyai/orca/actions/runs/29471918846), job 87536613410, passes in + 13m12s including full tests, unpacked app, and packaged CLI smoke. Golden E2E run + [29471918827](https://github.com/stablyai/orca/actions/runs/29471918827) passes macOS job + 87536613405 and Linux job 87536613436. Computer-use run 29471918802 passes on its first attempt. +- Closure: immutable compiled trust now owns the fixed-resource signature verifier on all six native + families. The build constant is still `null`; no production key/manifest/resource, Electron or + product caller, cache/downloader orchestration, SSH behavior, Beta mode, tuple, publication, or + default-path change exists. + +### E-M5-ARTIFACT-CACHE-ROOT-AUDIT-001 — app-owned path boundary selection + +- Date/owner: 2026-07-15, Codex implementation owner. +- Audit: targeted search over Electron `app.getPath('userData')`, all relay cache APIs/callers, prior + cache audits/evidence, packaged-manifest boundaries, and `ORCA_RELAY_PATH`; complete reads of cache + entry and eviction root handling. +- Findings: every proven cache primitive still requires an explicit `cacheRoot`; no relay cache-root + provider or product caller exists. Cache entry code already creates and physically validates the + root and owned namespaces, so this slice must not duplicate filesystem ownership checks. The + smallest safe capability is a pure native-path derivation from an absolute caller-supplied + user-data path into one fixed schema-versioned relay cache namespace. It must reject empty/relative + inputs and ignore environment overrides. Electron `app.getPath`, directory I/O, and orchestration + remain later adapters. +- Required RED: fixed `path.join` result on every native client, absolute-path guard, frozen namespace + constants, and proof that a similarly named runtime environment variable cannot redirect the root. +- Deliberately absent: Electron import/caller, filesystem writes, manifest/selector/downloader/cache + operations, `ORCA_RELAY_PATH` changes, SSH, Beta setting, fallback, tuple, publication, and defaults. + +### E-M5-ARTIFACT-CACHE-ROOT-LOCAL-RED-001 — cache-root provider is absent + +- Date/owner: 2026-07-15, Codex implementation owner. +- Command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-artifact-cache-root.test.ts`. +- Result: expected FAIL, one failed suite / zero tests in 251 ms because + `./ssh-relay-artifact-cache-root` does not exist. The purpose suite fixes the native joined path, + schema-versioned namespace, absolute input guard, frozen metadata, and no-runtime-environment- + override contract before implementation. +- Consumer disconnection: only the purpose test imports the absent module; no Electron, filesystem, + cache operation, downloader, SSH, mode, tuple, publication, or default caller exists. + +### E-M5-ARTIFACT-CACHE-ROOT-LOCAL-001 — pure cache-root contract and native CI wiring + +- Date/owner: 2026-07-15, Codex implementation owner. +- Implementation: `src/main/ssh/ssh-relay-artifact-cache-root.ts` accepts only an absolute + caller-supplied native user-data path and derives the fixed + `ssh-relay-runtime-cache/v1` namespace with `node:path`. The exported namespace metadata is frozen. + There is no environment lookup, Electron import, or filesystem access. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-artifact-cache-root.test.ts` — PASS, 1 file / 3 tests in + 197 ms Vitest, 1.09 s wall, 131,809,280-byte maximum RSS. Fixed native joining, absolute-input + rejection, frozen schema metadata, and environment non-redirection all pass. +- Native workflow proof wiring: the purpose suite is present once in each POSIX and PowerShell test + command in `.github/workflows/ssh-relay-runtime-artifacts.yml`; the workflow occurrence oracle + requires both. `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-artifact-cache-root.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 2 files / 10 tests in 613 ms Vitest, + 2.66 s wall, 128,925,696-byte maximum RSS. +- Broader SSH relay command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts` — PASS, 26 files / 314 + tests; 3 declared full-size files/tests skipped; 21.67 s Vitest, 24.11 s wall, + 259,784,704-byte maximum RSS. +- Release-script command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files + / 280 tests in 23.66 s Vitest, 26.32 s wall, 190,021,632-byte maximum RSS. +- Repository gates: targeted `oxfmt --write` completed on six files in 2.308 s; `pnpm typecheck` and + `pnpm lint` pass locally. Lint includes switch exhaustiveness, 41 reliability gates, max-lines + ratchet with no new bypass, bundled skill-guide verification, 9,837 localization references and + locale parity, and zero localization coverage candidates. Local Node 26.0.0 emits only the + expected package-engine warning for required Node 24; exact Node 24 native proof is pending. +- Boundary: no production manifest/key/resource, Electron caller, filesystem/cache orchestration, + proxy/downloader, SSH transfer/install/launch, `ORCA_RELAY_PATH` change, Beta mode, tuple, + publication, or default-path change. Legacy behavior remains unchanged. +- Remaining proof for this slice: commit the coherent package and require all six primary native + Node 24 jobs plus the existing adjacent PR/E2E gates before advancing to another desktop adapter. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/src/main/ssh/ssh-relay-artifact-cache-root.test.ts b/src/main/ssh/ssh-relay-artifact-cache-root.test.ts new file mode 100644 index 00000000000..f4f877e1028 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-root.test.ts @@ -0,0 +1,49 @@ +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, describe, expect, it } from 'vitest' + +import { + sshRelayArtifactCacheRoot, + SSH_RELAY_ARTIFACT_CACHE_ROOT_NAMESPACE +} from './ssh-relay-artifact-cache-root' + +const originalEnvironmentRoot = process.env.ORCA_SSH_RELAY_ARTIFACT_CACHE_ROOT + +afterEach(() => { + if (originalEnvironmentRoot === undefined) { + delete process.env.ORCA_SSH_RELAY_ARTIFACT_CACHE_ROOT + } else { + process.env.ORCA_SSH_RELAY_ARTIFACT_CACHE_ROOT = originalEnvironmentRoot + } +}) + +describe('SSH relay artifact cache root', () => { + it('derives one fixed schema-versioned namespace below native user data', () => { + const userDataPath = join(tmpdir(), 'Orca User Data') + + expect(sshRelayArtifactCacheRoot(userDataPath)).toBe( + join(userDataPath, 'ssh-relay-runtime-cache', 'v1') + ) + expect(SSH_RELAY_ARTIFACT_CACHE_ROOT_NAMESPACE).toEqual({ + directoryName: 'ssh-relay-runtime-cache', + schemaVersion: 'v1' + }) + expect(Object.isFrozen(SSH_RELAY_ARTIFACT_CACHE_ROOT_NAMESPACE)).toBe(true) + }) + + it('rejects empty and relative user-data paths', () => { + for (const value of ['', '.', 'relative/user-data']) { + expect(() => sshRelayArtifactCacheRoot(value)).toThrow(/absolute|user.data/i) + } + }) + + it('cannot be redirected through a runtime environment variable', () => { + const userDataPath = join(tmpdir(), 'Orca User Data') + process.env.ORCA_SSH_RELAY_ARTIFACT_CACHE_ROOT = join(tmpdir(), 'hostile-cache-root') + + expect(sshRelayArtifactCacheRoot(userDataPath)).toBe( + join(userDataPath, 'ssh-relay-runtime-cache', 'v1') + ) + }) +}) diff --git a/src/main/ssh/ssh-relay-artifact-cache-root.ts b/src/main/ssh/ssh-relay-artifact-cache-root.ts new file mode 100644 index 00000000000..6a0326ad6d9 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-root.ts @@ -0,0 +1,18 @@ +import { isAbsolute, join } from 'node:path' + +const DIRECTORY_NAME = 'ssh-relay-runtime-cache' +const SCHEMA_VERSION = 'v1' + +export const SSH_RELAY_ARTIFACT_CACHE_ROOT_NAMESPACE = Object.freeze({ + directoryName: DIRECTORY_NAME, + schemaVersion: SCHEMA_VERSION +}) + +export function sshRelayArtifactCacheRoot(userDataPath: string): string { + if (typeof userDataPath !== 'string' || !isAbsolute(userDataPath)) { + throw new Error('SSH relay artifact cache user-data path must be absolute') + } + + // Why: one app-owned versioned root prevents environment overrides and permits future migrations. + return join(userDataPath, DIRECTORY_NAME, SCHEMA_VERSION) +} From 22031fa686d2a7c96e687bc02c16fdb08bc987d7 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 01:32:29 -0400 Subject: [PATCH 184/267] feat: resolve verified relay cache entries Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 20 +- ...github-release-implementation-checklist.md | 141 +++++++++- ...sh-relay-artifact-cache-resolution.test.ts | 248 ++++++++++++++++++ .../ssh-relay-artifact-cache-resolution.ts | 99 +++++++ 6 files changed, 502 insertions(+), 9 deletions(-) create mode 100644 src/main/ssh/ssh-relay-artifact-cache-resolution.test.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-resolution.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 3577813a4ec..f88fca55d2c 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -269,6 +269,7 @@ jobs: src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts \ src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts \ src/main/ssh/ssh-relay-artifact-cache-root.test.ts \ + src/main/ssh/ssh-relay-artifact-cache-resolution.test.ts \ src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts \ src/main/ssh/ssh-relay-official-manifest.test.ts \ src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts \ @@ -808,6 +809,7 @@ jobs: src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts ` src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts ` src/main/ssh/ssh-relay-artifact-cache-root.test.ts ` + src/main/ssh/ssh-relay-artifact-cache-resolution.test.ts ` src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts ` src/main/ssh/ssh-relay-official-manifest.test.ts ` src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index ebda96bf749..e8457a3f6fa 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -127,6 +127,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-artifact-cache-in-use-lease.test.ts', 'ssh-relay-artifact-cache-eviction.test.ts', 'ssh-relay-artifact-cache-root.test.ts', + 'ssh-relay-artifact-cache-resolution.test.ts', 'ssh-relay-compiled-manifest-trust.test.ts', 'ssh-relay-official-manifest.test.ts', 'ssh-relay-manifest-accepted-keys.test.ts', 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 e4c1d48fd94..eab5ec9b61e 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 @@ -9,9 +9,23 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — disconnected app-owned -cache-root locally green and wired; commit/all-six native proof next, 2026-07-15, Codex -implementation owner.** `E-M5-ARTIFACT-CACHE-ROOT-LOCAL-001` passes 3/3 purpose tests, the native +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — disconnected warm-cache +resolution locally green and wired; commit/all-six native proof next, 2026-07-15, Codex +implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, +16/16 focused+workflow-oracle tests, 323/323 non-full-size SSH relay tests, 280/280 release-script +tests, typecheck, lint, format, reliability, max-lines, localization, and diff gates. Missing trust +and compatibility legacy results perform no cache I/O; a miss returns only the immutable selected +artifact; a verified hit acquires an in-use lease before exposure; corruption and lease failures +propagate closed. No download, Electron/startup, SSH/Beta mode/tuple/publication/default caller +exists. +`E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: +all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden +E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The +startup-boundary audit found that a late direct `app.getPath('userData')` adapter would be unsafe +because `app.setName()` can change that path. The eventual startup caller must supply Orca's +pre-`app.setName()` canonical path. The next slice may only compose explicit resolver/cache +dependencies; it must add no proxy, SSH/Beta mode/tuple/publication/default behavior. +`E-M5-ARTIFACT-CACHE-ROOT-LOCAL-001` passes 3/3 purpose tests, the native workflow oracle, 314/314 non-full-size SSH relay tests, 280/280 release-script tests, typecheck, lint, format, reliability, max-lines, localization, and diff gates. Both POSIX and PowerShell native artifact commands now run the purpose suite. The pure function accepts only an absolute 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 a24590f087f..ffbf5746947 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the smallest disconnected app-owned relay artifact cache-root contract**. Keep Electron/product consumers, cache orchestration, the build constant, production key/manifest/resources, downloader/SSH/settings/tuples/publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — `E-M5-OFFICIAL-MANIFEST-COMPOSITION-CI-001` closes the immutable-trust/fixed-resource composition at exact head `749f775f15cbd92354fe43e4c142900d4b921614`: all six primary native jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. Hosted Windows arm64 build 26200 remains correctly rejected against required 26100. Begin only the disconnected cache-root audit/RED; merging to `main` remains prohibited and SignPath is deferred. No Electron resource, downloader, SSH, setting, tuple, publication, or default-path consumer exists. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the smallest disconnected resolver/cache composition**. Require the eventual startup caller to supply Orca's pre-`app.setName()` canonical user-data path; keep filesystem orchestration outside import time and production manifest/resources, proxy, SSH/settings/tuples/publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a938710e275e3b49ab71446f73b01e075`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. `E-M5-ARTIFACT-CACHE-STARTUP-BOUNDARY-AUDIT-001` rejects a late direct Electron adapter because `app.setName()` can change `app.getPath('userData')`; the eventual startup caller must pass `getCanonicalUserDataPath()`. Begin only the disconnected resolver/cache composition audit; merging to `main` remains prohibited and SignPath is deferred. No SSH, setting, tuple, publication, or default-path consumer exists. 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) @@ -142,10 +142,15 @@ same change as the work it records. accounting, and bounded 2 GiB eviction pass locally and on all six native clients under E-M5-ARTIFACT-CACHE-LOCK-CI-001, E-M5-ARTIFACT-CACHE-ENTRY-CI-001, and E-M5-ARTIFACT-CACHE-EVICTION-CI-001. No production consumer exists. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit a pure app-owned - relay artifact cache-root derivation from a caller-supplied native user-data path. Keep Electron - `app.getPath`, directory creation, cache lookup/publication/eviction orchestration, environment - overrides, downloader, SSH, mode, tuple, publication, and defaults separately gated. +- Completed package: pure app-owned relay artifact cache-root derivation. An absolute + caller-supplied native user-data path, fixed versioned namespace, environment non-redirection, + local gates, and all-six native Node 24 proof are green under + E-M5-ARTIFACT-CACHE-ROOT-LOCAL-RED-001, E-M5-ARTIFACT-CACHE-ROOT-LOCAL-001, and + E-M5-ARTIFACT-CACHE-ROOT-CI-001. No Electron or product caller exists. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit the smallest + disconnected resolver/cache composition over explicit dependencies. The eventual startup caller, + not an SSH module, must supply `getCanonicalUserDataPath()` captured before `app.setName()`. + Keep proxy integration, SSH, mode, tuple, publication, and defaults separately gated. - 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 @@ -14359,6 +14364,130 @@ config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.m - Remaining proof for this slice: commit the coherent package and require all six primary native Node 24 jobs plus the existing adjacent PR/E2E gates before advancing to another desktop adapter. +### E-M5-ARTIFACT-CACHE-ROOT-CI-001 — all-six native cache-root proof + +- Date/owner: 2026-07-15, Codex implementation owner. +- Exact source/workflow: `aefcaa9a938710e275e3b49ab71446f73b01e075`, artifact run + [29472780636](https://github.com/stablyai/orca/actions/runs/29472780636). The purpose suite passes + within the complete runtime contract step under Node 24.18.0 on all six primary native clients. +- Primary jobs: Linux arm64 87539171165 passes in 3m02s; Linux x64 87539171175 in 2m26s; Windows + x64 87539171177 in 5m53s; macOS arm64 87539171184 in 1m48s; Windows arm64 87539171191 in 10m53s; + and macOS x64 87539171195 in 6m15s. Every job completes deterministic artifact build, exact runtime + smoke, full-size extraction/cache measurement, and unpublished artifact upload after the contract + step. +- Supplements/floors: Linux arm64/x64 supplement jobs 87540022894/87540022920 pass. Windows x64 + baseline job 87540654596 passes. Windows arm64 baseline job 87540654549 verifies and executes the + exact 85,213,511-byte expanded runtime with Node 24.18.0, PTY, watcher, and resource settlement, + then correctly rejects observed build `10.0.26200` against required build 26100 + (`qualified: false`, `osBuild: false`). The tuple remains disabled and this retained floor gate is + the artifact workflow's sole expected failure. +- Adjacent gates: PR Checks run + [29472780621](https://github.com/stablyai/orca/actions/runs/29472780621), job 87539170670, passes in + 14m48s including lint, typecheck, Git compatibility, full tests, unpacked app, and packaged CLI + smoke. Golden E2E run + [29472780625](https://github.com/stablyai/orca/actions/runs/29472780625) passes Linux job + 87539170813 in 4m55s and macOS job 87539170863 in 5m06s. Computer-use run + [29472780628](https://github.com/stablyai/orca/actions/runs/29472780628) passes Windows job + 87539170947 in 4m30s and Ubuntu job 87539170974 in 3m04s. +- Closure: native `node:path` semantics, the fixed versioned namespace, absolute-path rejection, + frozen metadata, and environment non-redirection are proven on Linux, macOS, and Windows x64/ + arm64. No Electron/product caller, filesystem/cache operation, proxy/downloader, SSH, setting, + tuple, publication, or default-path behavior exists; legacy remains unchanged. +- Residual gaps: Electron user-data adaptation, filesystem/cache orchestration, read-only/disk-full/ + quota/inode faults, proxy/certificate/live download, offline transfer, SSH install/launch, native + trust, oldest Windows arm64/macOS/Linux-kernel floors, per-target Beta wiring, and every tuple + remain open. + +### E-M5-ARTIFACT-CACHE-STARTUP-BOUNDARY-AUDIT-001 — canonical user-data path correction + +- Date/owner: 2026-07-15, Codex implementation owner. +- Audit: complete reads of `configureDevUserDataPath`, `initDataPath`, and + `getCanonicalUserDataPath`; startup-order search over `configureDevUserDataPath`, `app.setName`, + and all existing `app.getPath('userData')` consumers; import/caller search for the new cache-root + module. No HTML-plan text prescribes a direct Electron adapter, so the clean primary plan requires + no content correction. +- Finding: Orca deliberately captures canonical user data after dev/E2E `app.setPath` handling but + before `app.setName()`, because a late `app.getPath('userData')` can resolve to a different + directory on case-sensitive filesystems. A new SSH module that calls Electron directly would + therefore risk split cache ownership and bypass the established startup boundary. +- Correction: do not implement the proposed late Electron adapter. Keep + `sshRelayArtifactCacheRoot(userDataPath)` dependency-injected. The eventual startup/product caller + must pass `getCanonicalUserDataPath()` after initialization; tests may pass isolated absolute + paths. No environment variable may replace that path. +- Next package: audit the smallest disconnected resolver/cache composition over the already-proven + official manifest, selector, download, extraction, cache-entry, lease, and eviction capabilities. + It must accept explicit dependencies and add no import-time I/O, Electron/startup caller, proxy, + SSH, setting, tuple, publication, or default behavior. + +### E-M5-ARTIFACT-CACHE-RESOLUTION-AUDIT-001 — warm-cache acquisition seam + +- Date/owner: 2026-07-15, Codex implementation owner. +- Audit: complete reads of official-manifest composition, platform selector, downloader, extractor, + cache entry verification/publication, in-use lease, and eviction modules; export/caller search + confirms every capability remains disconnected outside tests/fixtures. +- Findings: the smallest useful composition is selection plus verified cache lookup plus in-use + lease acquisition. Missing compiled trust and compatibility-selected legacy results must return + before cache I/O. A cache miss must return the immutable selected artifact without downloading; + a verified hit must acquire a lease before exposing a frozen entry snapshot. Relative cache roots, + cancellation, integrity/quarantine errors, and lease failures must propagate without becoming a + miss or legacy result. +- Deliberately separate: download staging, proxy/certificate integration, cold publication, + eviction, availability/fallback classification, canonical startup caller, SSH transfer/install, + `ORCA_RELAY_PATH`, Beta settings, tuples, publication, and defaults. +- Required RED: exact operation order and call counts for unavailable, legacy, miss, and hit; + immutable returned identity; absolute-root and pre-aborted guards; fail-closed lookup-integrity and + lease errors; proof that no downloader, Electron, startup, SSH, or fallback caller is introduced. + +### E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-RED-001 — cache resolution composition is absent + +- Date/owner: 2026-07-15, Codex implementation owner. +- Command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-artifact-cache-resolution.test.ts`. +- Result: expected FAIL, one failed suite / zero tests in 403 ms because + `./ssh-relay-artifact-cache-resolution` does not exist. Seven purpose cases define unavailable and + compatibility short circuits, immutable miss/hit identity, lease-before-hit exposure, + absolute-root and cancellation guards, and fail-closed integrity/lease propagation. +- Consumer disconnection: only the purpose test imports the absent module. No downloader, Electron, + startup, SSH, setting, fallback, tuple, publication, or default caller exists. + +### E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001 — disconnected warm-cache resolution + +- Date/owner: 2026-07-15, Codex implementation owner. +- Implementation: `ssh-relay-artifact-cache-resolution.ts` composes only the verified official + manifest, compatibility selector, strict cache lookup, and in-use lease. Missing trust and legacy + selections return before cache I/O; cache miss returns the immutable selected artifact; cache hit + returns a frozen verified entry snapshot only after lease acquisition. Integrity/quarantine and + lease errors are not classified or converted here. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-artifact-cache-resolution.test.ts` — PASS, 1 file / 9 tests + in 855 ms Vitest, 2.54 s wall, 131,825,664-byte maximum RSS. Cases cover unavailable/legacy zero- + I/O short circuits, immutable miss, real default cache miss, exact lookup-before-lease hit, frozen + entry identity, absolute root, pre-abort, post-acquisition cancellation release, and fail-closed + integrity/lease errors. +- Native workflow wiring/oracle: the purpose suite is present once in each POSIX and PowerShell + native artifact command. `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-artifact-cache-resolution.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 2 files / 16 tests in 1.68 s Vitest, + 3.58 s wall, 131,989,504-byte maximum RSS. +- Broader SSH relay command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS, 27 + files / 323 tests; 3 declared full-size files/tests skipped; 18.04 s Vitest, 19.47 s wall, + 301,465,600-byte maximum RSS. +- Release-script command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 --silent=true config/scripts/ssh-relay-runtime-*.test.mjs` + — PASS, 50 files / 280 tests in 19.47 s Vitest, 21.25 s wall, 188,203,008-byte maximum RSS. +- Repository gates: targeted `oxfmt --write` completes on six files in 1.895 s; `pnpm typecheck` and + `pnpm lint` pass. Lint includes switch exhaustiveness, 41 reliability gates, no new max-lines + bypass, bundled skill-guide verification, 9,837 localization references and locale parity, and + zero localization coverage candidates. Local Node 26.0.0 emits only the expected engine warning; + exact Node 24 native proof remains pending. +- Boundary: no download staging, proxy/certificate integration, cold publication, eviction, + availability/fallback classification, Electron/startup caller, SSH transfer/install/launch, + `ORCA_RELAY_PATH`, Beta setting, tuple, publication, or default-path change. Legacy remains + unchanged. +- Remaining proof for this slice: commit the coherent package and require all six primary native + Node 24 jobs plus adjacent PR/E2E gates before adding cold download/publication composition. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/src/main/ssh/ssh-relay-artifact-cache-resolution.test.ts b/src/main/ssh/ssh-relay-artifact-cache-resolution.test.ts new file mode 100644 index 00000000000..a045b84c53b --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-resolution.test.ts @@ -0,0 +1,248 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { join } from 'node:path' +import { tmpdir } from 'node:os' + +import nacl from 'tweetnacl' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { createSshRelayArtifactTestManifest } from './ssh-relay-artifact-test-manifest' +import { SshRelayArtifactCacheIntegrityError } from './ssh-relay-artifact-cache-entry' +import { + resolveSshRelayArtifactCache, + type SshRelayArtifactCacheResolutionOperations +} from './ssh-relay-artifact-cache-resolution' +import type { SshRelayOfficialManifest } from './ssh-relay-official-manifest' +import { + signSshRelayArtifactManifest, + sshRelayManifestKeyId, + verifySshRelayArtifactManifest +} from './ssh-relay-manifest-signature' +import type { SshRelayDigest } from './ssh-relay-runtime-identity' + +const keyPair = nacl.sign.keyPair.fromSeed(Uint8Array.from({ length: 32 }, (_, index) => index)) +const cacheRoot = join(tmpdir(), 'orca-relay-cache-resolution') +const compatibleHost = { + os: 'linux' as const, + architecture: 'x64' as const, + processTranslated: false, + kernelVersion: '6.8.0', + libc: { family: 'glibc' as const, version: '2.39' }, + libstdcxxVersion: '6.0.33', + glibcxxVersion: '3.4.33' +} + +function officialManifest(): SshRelayOfficialManifest { + const manifest = createSshRelayArtifactTestManifest() + manifest.signatures = [signSshRelayArtifactManifest(manifest, keyPair.secretKey)] + return Object.freeze({ + manifest: verifySshRelayArtifactManifest(manifest, [ + { keyId: sshRelayManifestKeyId(keyPair.publicKey), publicKey: keyPair.publicKey } + ]), + acceptedKeysSha256: `sha256:${'a'.repeat(64)}` as SshRelayDigest + }) +} + +function cacheEntryFor(official: SshRelayOfficialManifest) { + const tuple = official.manifest.tuples[0] + return { + contentId: tuple.contentId, + tupleId: tuple.tupleId, + entryPath: join(cacheRoot, 'entries', tuple.contentId.slice('sha256:'.length)), + archivePath: join(cacheRoot, tuple.archive.name), + runtimeRoot: join(cacheRoot, 'runtime'), + proofPath: join(cacheRoot, 'install-proof.json'), + files: tuple.entries.length, + expandedBytes: tuple.entries.reduce( + (total, value) => total + (value.type === 'file' ? value.size : 0), + 0 + ) + } +} + +const operations = { + lookup: vi.fn(), + acquireInUseLease: vi.fn() +} + +beforeEach(() => { + operations.lookup.mockReset() + operations.acquireInUseLease.mockReset() +}) + +describe('SSH relay artifact cache resolution', () => { + it('returns unavailable before cache I/O when official trust is unprovisioned', async () => { + await expect( + resolveSshRelayArtifactCache( + { officialManifest: null, host: compatibleHost, cacheRoot }, + operations + ) + ).resolves.toEqual({ kind: 'unavailable', reason: 'official-manifest-unavailable' }) + expect(operations.lookup).not.toHaveBeenCalled() + expect(operations.acquireInUseLease).not.toHaveBeenCalled() + }) + + it('returns a compatibility legacy result before cache I/O', async () => { + await expect( + resolveSshRelayArtifactCache( + { + officialManifest: officialManifest(), + host: { ...compatibleHost, processTranslated: true }, + cacheRoot + }, + operations + ) + ).resolves.toEqual({ kind: 'legacy', reason: 'translated-process' }) + expect(operations.lookup).not.toHaveBeenCalled() + expect(operations.acquireInUseLease).not.toHaveBeenCalled() + }) + + it('returns an immutable selected artifact on cache miss without acquiring a lease', async () => { + operations.lookup.mockResolvedValue({ kind: 'miss' }) + + const result = await resolveSshRelayArtifactCache( + { officialManifest: officialManifest(), host: compatibleHost, cacheRoot }, + operations + ) + + expect(result).toMatchObject({ kind: 'cache-miss', artifact: { tupleId: 'linux-x64-glibc' } }) + expect(Object.isFrozen(result)).toBe(true) + expect(operations.lookup).toHaveBeenCalledTimes(1) + expect(operations.lookup.mock.calls[0][0]).toMatchObject({ cacheRoot }) + expect(operations.acquireInUseLease).not.toHaveBeenCalled() + }) + + it('uses the real verified cache lookup on a disconnected miss', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-relay-cache-resolution-real-')) + try { + await expect( + resolveSshRelayArtifactCache({ + officialManifest: officialManifest(), + host: compatibleHost, + cacheRoot: join(root, 'cache') + }) + ).resolves.toMatchObject({ + kind: 'cache-miss', + artifact: { tupleId: 'linux-x64-glibc' } + }) + } finally { + await rm(root, { recursive: true, force: true }) + } + }) + + it('acquires a lease before exposing a frozen verified cache entry', async () => { + const official = officialManifest() + const entry = cacheEntryFor(official) + const lease = { + leasePath: join(cacheRoot, 'in-use', 'lease'), + token: 'b'.repeat(32), + assertOwned: vi.fn(async () => {}), + release: vi.fn(async () => {}) + } + operations.lookup.mockResolvedValue({ kind: 'hit', entry }) + operations.acquireInUseLease.mockResolvedValue(lease) + + const result = await resolveSshRelayArtifactCache( + { officialManifest: official, host: compatibleHost, cacheRoot }, + operations + ) + + expect(result).toMatchObject({ kind: 'cache-hit', entry, lease }) + expect(Object.isFrozen(result)).toBe(true) + expect(result.kind === 'cache-hit' && Object.isFrozen(result.entry)).toBe(true) + expect(operations.acquireInUseLease).toHaveBeenCalledTimes(1) + expect(operations.acquireInUseLease.mock.calls[0][0]).toMatchObject({ cacheRoot, entry }) + expect(operations.lookup.mock.invocationCallOrder[0]).toBeLessThan( + operations.acquireInUseLease.mock.invocationCallOrder[0] + ) + }) + + it('releases an acquired lease when cancellation wins before exposure', async () => { + const official = officialManifest() + const controller = new AbortController() + const release = vi.fn(async () => {}) + operations.lookup.mockResolvedValue({ kind: 'hit', entry: cacheEntryFor(official) }) + operations.acquireInUseLease.mockImplementation(async () => { + controller.abort(new Error('cancel after lease')) + return { + leasePath: join(cacheRoot, 'in-use', 'lease'), + token: 'c'.repeat(32), + assertOwned: vi.fn(async () => {}), + release + } + }) + + await expect( + resolveSshRelayArtifactCache( + { + officialManifest: official, + host: compatibleHost, + cacheRoot, + signal: controller.signal + }, + operations + ) + ).rejects.toThrow(/cancel after lease/i) + expect(release).toHaveBeenCalledTimes(1) + }) + + it('rejects a relative cache root before invoking cache operations', async () => { + await expect( + resolveSshRelayArtifactCache( + { + officialManifest: officialManifest(), + host: compatibleHost, + cacheRoot: 'relative/cache' + }, + operations + ) + ).rejects.toThrow(/absolute|cache root/i) + expect(operations.lookup).not.toHaveBeenCalled() + }) + + it('settles pre-aborted requests without invoking cache operations', async () => { + const controller = new AbortController() + controller.abort(new Error('cancel cache resolution')) + + await expect( + resolveSshRelayArtifactCache( + { + officialManifest: officialManifest(), + host: compatibleHost, + cacheRoot, + signal: controller.signal + }, + operations + ) + ).rejects.toThrow(/cancel cache resolution/i) + expect(operations.lookup).not.toHaveBeenCalled() + }) + + it('propagates cache integrity and lease failures without returning miss or legacy', async () => { + operations.lookup.mockRejectedValueOnce( + new SshRelayArtifactCacheIntegrityError( + 'corrupt cache entry', + join(cacheRoot, 'quarantine', 'corrupt'), + new Error('tree mismatch') + ) + ) + await expect( + resolveSshRelayArtifactCache( + { officialManifest: officialManifest(), host: compatibleHost, cacheRoot }, + operations + ) + ).rejects.toBeInstanceOf(SshRelayArtifactCacheIntegrityError) + + const official = officialManifest() + operations.lookup.mockResolvedValueOnce({ + kind: 'hit', + entry: cacheEntryFor(official) + }) + operations.acquireInUseLease.mockRejectedValueOnce(new Error('lease ownership displaced')) + await expect( + resolveSshRelayArtifactCache( + { officialManifest: official, host: compatibleHost, cacheRoot }, + operations + ) + ).rejects.toThrow(/lease ownership displaced/i) + }) +}) diff --git a/src/main/ssh/ssh-relay-artifact-cache-resolution.ts b/src/main/ssh/ssh-relay-artifact-cache-resolution.ts new file mode 100644 index 00000000000..f78296c5b75 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-resolution.ts @@ -0,0 +1,99 @@ +import { isAbsolute } from 'node:path' + +import { + lookupSshRelayArtifactCacheEntry, + type SshRelayArtifactCacheLookup +} from './ssh-relay-artifact-cache-entry' +import type { SshRelayArtifactCacheEntry } from './ssh-relay-artifact-cache-entry-verification' +import { + acquireSshRelayArtifactCacheInUseLease, + type SshRelayArtifactCacheInUseLease +} from './ssh-relay-artifact-cache-in-use-lease' +import { + selectSshRelayArtifact, + type SshRelayArtifactLegacyReason, + type SshRelayHostEvidence, + type SshRelaySelectedArtifact +} from './ssh-relay-artifact-selector' +import type { SshRelayOfficialManifest } from './ssh-relay-official-manifest' + +export type SshRelayArtifactCacheResolutionOperations = Readonly<{ + lookup: typeof lookupSshRelayArtifactCacheEntry + acquireInUseLease: typeof acquireSshRelayArtifactCacheInUseLease +}> + +const DEFAULT_OPERATIONS: SshRelayArtifactCacheResolutionOperations = Object.freeze({ + lookup: lookupSshRelayArtifactCacheEntry, + acquireInUseLease: acquireSshRelayArtifactCacheInUseLease +}) + +export type SshRelayArtifactCacheResolution = + | Readonly<{ kind: 'unavailable'; reason: 'official-manifest-unavailable' }> + | Readonly<{ kind: 'legacy'; reason: SshRelayArtifactLegacyReason }> + | Readonly<{ kind: 'cache-miss'; artifact: SshRelaySelectedArtifact }> + | Readonly<{ + kind: 'cache-hit' + artifact: SshRelaySelectedArtifact + entry: Readonly + lease: SshRelayArtifactCacheInUseLease + }> + +function frozenUnavailable(): SshRelayArtifactCacheResolution { + return Object.freeze({ kind: 'unavailable', reason: 'official-manifest-unavailable' }) +} + +function frozenLegacy(reason: SshRelayArtifactLegacyReason): SshRelayArtifactCacheResolution { + return Object.freeze({ kind: 'legacy', reason }) +} + +function frozenEntry( + lookup: Extract +): Readonly { + return Object.freeze({ ...lookup.entry }) +} + +export async function resolveSshRelayArtifactCache( + { + officialManifest, + host, + cacheRoot, + signal + }: { + officialManifest: SshRelayOfficialManifest | null + host: SshRelayHostEvidence + cacheRoot: string + signal?: AbortSignal + }, + operations: SshRelayArtifactCacheResolutionOperations = DEFAULT_OPERATIONS +): Promise { + signal?.throwIfAborted() + if (officialManifest === null) { + return frozenUnavailable() + } + + const selection = selectSshRelayArtifact(officialManifest.manifest, host) + if (selection.kind === 'legacy') { + return frozenLegacy(selection.reason) + } + if (typeof cacheRoot !== 'string' || !isAbsolute(cacheRoot)) { + throw new Error('SSH relay artifact cache resolution root must be absolute') + } + + signal?.throwIfAborted() + // Why: integrity/quarantine errors intentionally escape this boundary and can never become misses. + const lookup = await operations.lookup({ cacheRoot, artifact: selection, signal }) + signal?.throwIfAborted() + if (lookup.kind === 'miss') { + return Object.freeze({ kind: 'cache-miss', artifact: selection }) + } + + const entry = frozenEntry(lookup) + const lease = await operations.acquireInUseLease({ cacheRoot, entry, signal }) + try { + signal?.throwIfAborted() + return Object.freeze({ kind: 'cache-hit', artifact: selection, entry, lease }) + } catch (error) { + await lease.release().catch(() => {}) + throw error + } +} From 3018977c113926addcbb97b5715242ed86d820d2 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 02:07:59 -0400 Subject: [PATCH 185/267] feat: populate verified relay cache Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 4 + .../ssh-relay-runtime-workflow.test.mjs | 2 + ...elease-implementation-checklist-summary.md | 28 +- ...github-release-implementation-checklist.md | 170 ++++++++++- ...-relay-artifact-cache-in-use-lease.test.ts | 8 + .../ssh-relay-artifact-cache-in-use-lease.ts | 24 +- ...ifact-cache-population-integration.test.ts | 76 +++++ ...sh-relay-artifact-cache-population.test.ts | 283 ++++++++++++++++++ .../ssh-relay-artifact-cache-population.ts | 193 ++++++++++++ 9 files changed, 772 insertions(+), 16 deletions(-) create mode 100644 src/main/ssh/ssh-relay-artifact-cache-population-integration.test.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-population.test.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-population.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index f88fca55d2c..a9bf767ca86 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -270,6 +270,8 @@ jobs: src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts \ src/main/ssh/ssh-relay-artifact-cache-root.test.ts \ src/main/ssh/ssh-relay-artifact-cache-resolution.test.ts \ + src/main/ssh/ssh-relay-artifact-cache-population.test.ts \ + src/main/ssh/ssh-relay-artifact-cache-population-integration.test.ts \ src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts \ src/main/ssh/ssh-relay-official-manifest.test.ts \ src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts \ @@ -810,6 +812,8 @@ jobs: src/main/ssh/ssh-relay-artifact-cache-eviction.test.ts ` src/main/ssh/ssh-relay-artifact-cache-root.test.ts ` src/main/ssh/ssh-relay-artifact-cache-resolution.test.ts ` + src/main/ssh/ssh-relay-artifact-cache-population.test.ts ` + src/main/ssh/ssh-relay-artifact-cache-population-integration.test.ts ` src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts ` src/main/ssh/ssh-relay-official-manifest.test.ts ` src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index e8457a3f6fa..7fe2346c326 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -128,6 +128,8 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-artifact-cache-eviction.test.ts', 'ssh-relay-artifact-cache-root.test.ts', 'ssh-relay-artifact-cache-resolution.test.ts', + 'ssh-relay-artifact-cache-population.test.ts', + 'ssh-relay-artifact-cache-population-integration.test.ts', 'ssh-relay-compiled-manifest-trust.test.ts', 'ssh-relay-official-manifest.test.ts', 'ssh-relay-manifest-accepted-keys.test.ts', 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 eab5ec9b61e..82637b67806 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 @@ -9,15 +9,35 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — disconnected warm-cache -resolution locally green and wired; commit/all-six native proof next, 2026-07-15, Codex -implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — disconnected cold-cache +population locally green and wired; commit/all-six native proof next, 2026-07-15, Codex +implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit +`22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, +Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, 16/16 focused+workflow-oracle tests, 323/323 non-full-size SSH relay tests, 280/280 release-script tests, typecheck, lint, format, reliability, max-lines, localization, and diff gates. Missing trust and compatibility legacy results perform no cache I/O; a miss returns only the immutable selected artifact; a verified hit acquires an in-use lease before exposure; corruption and lease failures propagate closed. No download, Electron/startup, SSH/Beta mode/tuple/publication/default caller -exists. +exists. The active package composes only download, existing immutable cache-entry publication, and +in-use lease operations behind an explicit canonical cache root; it adds no Electron/startup, +proxy, SSH/Beta mode/tuple enablement/release publication/fallback/default behavior. SignPath stays +deferred to the final signing gate. +`E-M5-ARTIFACT-CACHE-POPULATION-AUDIT-001` selects cache-local exclusive download staging → existing +strict immutable publication → staging cleanup → in-use lease as the next boundary; the publisher +already owns strict extraction and complete-tree verification, so no second extractor is added. +`E-M5-ARTIFACT-CACHE-POPULATION-LOCAL-RED-001` records the expected missing-module failure before +implementation. `E-M5-ARTIFACT-CACHE-POPULATION-LOCAL-001` passes 9/9 purpose/integration tests, +16/16 focused+workflow-oracle tests, 332/332 non-full-size SSH relay tests, 280/280 release-script tests, +typecheck, lint, format, reliability, max-lines, localization, and diff gates. Every failure remains +unclassified and fail-closed at this layer; exact-head native proof is next. +The first real cold→warm assertion then exposed a logical `/var/...` versus physical +`/private/var/...` lease-root mismatch under `E-M5-ARTIFACT-CACHE-PHYSICAL-ROOT-LOCAL-RED-001`. +The primary plan already requires physical cache ownership, so its content is unchanged; the +implementation now canonicalizes lease identity/locking/recency to the existing physical root under +`E-M5-ARTIFACT-CACHE-PHYSICAL-ROOT-CORRECTION-LOCAL-001`: 7/7 correction tests and 21/21 focused+ +workflow-oracle tests pass while missing/misplaced entries remain rejected. Broader exact-head proof +is still required before checkpointing. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 ffbf5746947..8258032198f 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the smallest disconnected resolver/cache composition**. Require the eventual startup caller to supply Orca's pre-`app.setName()` canonical user-data path; keep filesystem orchestration outside import time and production manifest/resources, proxy, SSH/settings/tuples/publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a938710e275e3b49ab71446f73b01e075`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. `E-M5-ARTIFACT-CACHE-STARTUP-BOUNDARY-AUDIT-001` rejects a late direct Electron adapter because `app.setName()` can change `app.getPath('userData')`; the eventual startup caller must pass `getCanonicalUserDataPath()`. Begin only the disconnected resolver/cache composition audit; merging to `main` remains prohibited and SignPath is deferred. No SSH, setting, tuple, publication, or default-path consumer exists. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: prove disconnected cold-cache population**. Require the eventual startup caller to supply Orca's pre-`app.setName()` canonical user-data path; keep filesystem orchestration outside import time and production manifest/resources, proxy, SSH/settings/tuples/release publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa686d2a7c96e687bc02c16fdb08bc987d7` on all six primary native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. The disconnected cold population is locally green under `E-M5-ARTIFACT-CACHE-POPULATION-LOCAL-001`: exclusive cache-local download staging → strict existing immutable publication → staging cleanup → in-use lease, with failures propagating closed. Merging to `main` remains prohibited and SignPath is deferred. No Electron/startup, SSH, setting, tuple enablement, release publication, fallback, or default-path consumer exists. 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) @@ -14488,6 +14488,164 @@ config/vitest.config.ts --maxWorkers=1 --silent=true config/scripts/ssh-relay-ru - Remaining proof for this slice: commit the coherent package and require all six primary native Node 24 jobs plus adjacent PR/E2E gates before adding cold download/publication composition. +### E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001 — all-six native warm-cache resolution proof + +- Date/owner: 2026-07-15, Codex implementation owner. +- Exact source/workflow: `22031fa686d2a7c96e687bc02c16fdb08bc987d7`, artifact run + [29474175664](https://github.com/stablyai/orca/actions/runs/29474175664). The purpose suite passes + within the complete runtime contract step under Node 24.18.0 on all six primary native clients. +- Primary jobs: Windows arm64 87543508784 passes in 9m27s; macOS arm64 87543508815 in 2m17s; Linux + x64 87543508833 in 6m30s; macOS x64 87543508875 in 5m06s; Windows x64 87543508878 in 5m27s; and + Linux arm64 87543508960 in 5m19s. Every job completes deterministic artifact build, exact runtime + smoke, full-size extraction/cache measurement, and unpublished artifact upload after the contract + step. +- Supplements/floors: Linux x64/arm64 supplement jobs 87544380719/87544380722 pass in 47s/58s. + Windows x64 baseline job 87544783538 passes in 2m06s. Windows arm64 baseline job 87544783520 + verifies and executes the exact unpublished runtime, then correctly rejects observed build + `10.0.26200` against required build 26100 (`qualified: false`, `osBuild: false`) in 5m09s. The + tuple remains disabled and this retained floor gate is the artifact workflow's sole expected + failure. +- Adjacent gates: PR Checks run + [29474175599](https://github.com/stablyai/orca/actions/runs/29474175599), job 87543508131, passes in + 14m15s including lint, typecheck, Git compatibility, full tests, unpacked app, and packaged CLI + smoke. Golden E2E run + [29474175584](https://github.com/stablyai/orca/actions/runs/29474175584) passes Linux/macOS jobs + 87543508216/87543508224 in 5m17s/4m48s. Computer-use run + [29474175581](https://github.com/stablyai/orca/actions/runs/29474175581) passes Ubuntu/Windows jobs + 87543508291/87543508301 in 3m09s/5m42s. +- Closure: missing trust and compatibility short circuits, immutable cache miss, verified hit, + lookup-before-lease order, cancellation release, absolute-root rejection, and fail-closed cache + integrity/lease errors are proven on Linux, macOS, and Windows x64/arm64. No downloader, + Electron/startup/product caller, SSH, setting, fallback, tuple, release publication, or default- + path behavior is connected; legacy remains unchanged. +- Residual gaps: cold-cache population native proof, Electron/startup adaptation, proxy/certificate/ + live download, offline transfer, SSH install/launch, native trust, oldest Windows arm64/macOS/ + Linux-kernel floors, per-target Beta wiring, and every tuple remain open. + +### E-M5-ARTIFACT-CACHE-POPULATION-AUDIT-001 — cold-cache composition boundary + +- Date/owner: 2026-07-15, Codex implementation owner. +- Audit: complete reads of the disconnected downloader, strict extractor, cache entry publication/ + verification, in-use lease, eviction, and warm-cache resolution modules plus their purpose tests + and workflow wiring. Caller search confirms there is still no product consumer. +- Finding: cache entry publication already copies and re-hashes the verified archive, performs strict + archive inspection/extraction, verifies the complete extracted tree, writes the structured proof, + re-verifies the staged entry, and atomically publishes under the content lock. Calling the + extractor separately would duplicate work and widen the orchestration surface. The smallest cold + capability is therefore cache-local exclusive download staging, exact download-result identity + checking, existing immutable publication, staging cleanup, then in-use lease acquisition before + exposure. A publish-to-lease eviction race must propagate closed; later resolution may retry it. +- Required boundary: absolute explicit cache root, physically owned `downloads` namespace, unique + per-attempt directory, exact operation order, cleanup on success/failure/cancellation, immutable + returned entry, and lease release when cancellation wins after acquisition. Download, extracted- + tree/integrity, cleanup, publication, and lease errors are not fallback-classified here. +- Deliberately absent: Electron/startup caller, proxy policy, SSH transfer/install/launch, + `ORCA_RELAY_PATH`, Beta setting, fallback state machine, tuple enablement, release publication, + eviction scheduling, production resources, and default behavior. +- Implementation evidence correction: the first real cold→warm integration run exposed that cache + lookup/publication return physical entry paths while in-use acquisition compared them with the + caller's logical cache-root spelling. This is an implementation defect under a symlinked ancestor, + not a primary-plan decision change; the HTML plan already requires physically owned cache paths + and needs no content correction. Canonicalize the lease operation to the existing physical cache + root before identity comparison, locking, lease creation, and recency recording. + +### E-M5-ARTIFACT-CACHE-POPULATION-LOCAL-RED-001 — cold population composition is absent + +- Date/owner: 2026-07-15, Codex implementation owner. +- Command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-artifact-cache-population.test.ts`. +- Result: expected FAIL, one failed suite / zero tests in 1.52 s because + `./ssh-relay-artifact-cache-population` does not exist. Seven purpose cases fix exclusive staging, + download → publish → cleanup → lease ordering, absolute-root rejection, download identity, + fail-closed download/publication/lease behavior, post-lease cancellation release, and concurrent + staging isolation before implementation. +- Consumer disconnection: only the purpose test imports the absent module; no Electron/startup, + SSH, mode, fallback, tuple, release publication, or default caller exists. + +### E-M5-ARTIFACT-CACHE-PHYSICAL-ROOT-LOCAL-RED-001 — cold-to-warm lease spelling mismatch + +- Date/owner: 2026-07-15, Codex implementation owner. +- Command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-artifact-cache-population-integration.test.ts`. +- Result: expected FAIL after adding the real post-population warm lookup/lease assertion: 1 file, + 2/2 failed in 787 ms with `SSH relay artifact cache in-use entry path disagrees with its content +identity`. On macOS, `tmpdir()` uses logical `/var/...` while cache publication/lookup correctly + return physical `/private/var/...`; lease acquisition compared the physical entry to the logical + root before any I/O. Both Linux tar/Brotli and Windows ZIP fixture cases reproduce the same client- + path defect. +- Required correction: resolve the physical cache root once inside lease acquisition and use it for + entry identity, the content lock, lease creation, and recency. Do not weaken exact-entry checks or + change cache ownership, eviction, SSH, fallback, tuple, publication, or default behavior. + +### E-M5-ARTIFACT-CACHE-PHYSICAL-ROOT-CORRECTION-LOCAL-001 — exact physical lease identity + +- Date/owner: 2026-07-15, Codex implementation owner. +- Correction: `acquireSshRelayArtifactCacheInUseLease` resolves the existing cache root and existing + entry to physical paths, requires the entry to equal the exact physical content-addressed path, + and uses that physical root for locking, lease creation, and recency. A missing entry retains the + existing domain error; a different existing path remains rejected. No final-entry symlink or + alternate content path becomes acceptable. +- Evidence-gated refinement: the first root-only attempt caused 3/5 existing lease tests to reject + legitimate logical entry spellings; the root+entry attempt left 1/5 red because raw `realpath` + `ENOENT` bypassed the established missing-entry diagnostic. Neither result was accepted. The final + correction wraps only `ENOENT` and preserves all other failures. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-artifact-cache-population-integration.test.ts +src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts` — PASS, 2 files / 7 tests in 2.40 s + Vitest, 4.14 s wall, 134,299,648-byte maximum RSS. A subsequent assertion for an existing but + misplaced content path also passes while remaining rejected by exact identity. +- Focused plus native workflow oracle: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-artifact-cache-population.test.ts +src/main/ssh/ssh-relay-artifact-cache-population-integration.test.ts +src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 4 files / 21 tests in 4.20 s Vitest, + 7.89 s wall, 138,723,328-byte maximum RSS. +- Boundary: this corrects physical path identity only. It adds no network, Electron/startup caller, + SSH, setting, fallback, tuple, release publication, or default behavior. + +### E-M5-ARTIFACT-CACHE-POPULATION-LOCAL-001 — disconnected cold-cache population + +- Date/owner: 2026-07-15, Codex implementation owner. +- Implementation: `ssh-relay-artifact-cache-population.ts` creates a physically validated cache- + local `downloads` namespace and unique per-attempt directory, invokes the bounded verified + downloader, checks its exact destination/size/hash result, delegates strict copy/hash/extraction/ + complete-tree proof and atomic publication to the existing publisher, removes download staging, + then acquires an in-use lease before exposing a frozen entry. All errors propagate without + availability/fallback classification; cancellation after lease acquisition releases the lease. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-artifact-cache-population.test.ts +src/main/ssh/ssh-relay-artifact-cache-population-integration.test.ts` — PASS, 2 files / 9 tests in + 1.01 s Vitest, 1.88 s wall, 139,051,008-byte maximum RSS. Seven operation-order/failure cases + cover cleanup-before-lease exposure, absolute-root rejection, inconsistent download identity, + download/publication failure cleanup, publish-to-lease eviction failure, post-lease cancellation + release, and concurrent unique staging. Two credential-free integration cases feed exact signed + Linux tar/Brotli and Windows ZIP fixtures through an in-memory Electron response into the real + downloader, strict publisher, complete-tree verifier, and in-use lease. +- Native workflow wiring/oracle: the purpose suite is present once in each POSIX and PowerShell + native artifact command. `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-artifact-cache-population.test.ts +src/main/ssh/ssh-relay-artifact-cache-population-integration.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 3 files / 16 tests in 1.52 s Vitest, + 2.44 s wall, 132,792,320-byte maximum RSS. +- Broader SSH relay command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS, 29 + files / 332 tests; 3 declared full-size files/tests skipped; final post-correction run 25.88 s + Vitest, 27.58 s wall, 253,640,704-byte maximum RSS. +- Release-script command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 --silent=true config/scripts/ssh-relay-runtime-*.test.mjs` + — PASS, 50 files / 280 tests in a final post-correction 38.77 s Vitest, 40.68 s wall, + 189,661,184-byte maximum RSS. +- Repository gates: targeted `oxfmt --write` completes on six files in 7.640 s; `pnpm typecheck` + and `pnpm lint` pass. Lint includes switch exhaustiveness, 41 reliability gates, no new max-lines + bypass, bundled skill-guide verification, 9,837 localization references and locale parity, and + zero localization coverage candidates. Local Node 26.0.0 emits only the expected engine warning; + exact Node 24 native proof remains pending. +- Boundary: no Electron/startup caller, live network/proxy integration, eviction scheduling, SSH + transfer/install/launch, `ORCA_RELAY_PATH`, Beta setting, fallback classification, tuple + enablement, release publication, or default-path change. Legacy remains unchanged. +- Remaining proof for this slice: commit the coherent package and require all six primary native + Node 24 jobs plus adjacent PR/E2E gates before adding another desktop orchestration boundary. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -14546,11 +14704,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Checkpoint the disconnected compile-time trust-root package and require exact-head Node 24 proof on +Checkpoint the disconnected cold-cache population package and require exact-head Node 24 proof on Linux, macOS, and Windows x64/arm64. After that proof, audit the next smallest non-signing desktop -orchestration boundary; do not provision production secrets/environments, embed unreviewed keys or -manifest bytes, add SSH transfer/install, mode wiring, tuple enablement, release publication, or -default behavior. +orchestration boundary; do not provision production secrets/environments, add an Electron/startup +consumer, embed unreviewed keys or manifest bytes, add SSH transfer/install, mode wiring, fallback, +tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. diff --git a/src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts b/src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts index a92ed8cb738..377f9691260 100644 --- a/src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts +++ b/src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts @@ -170,6 +170,14 @@ describe('SSH relay artifact cache in-use lease', () => { await expect( acquireSshRelayArtifactCacheInUseLease({ cacheRoot: root, entry }) ).rejects.toThrow(/entry/i) + const misplacedEntryPath = join(root, 'entries', 'b'.repeat(64)) + await mkdir(misplacedEntryPath, { recursive: true }) + await expect( + acquireSshRelayArtifactCacheInUseLease({ + cacheRoot: root, + entry: { ...entry, entryPath: misplacedEntryPath } + }) + ).rejects.toThrow(/identity/i) await mkdir(join(root, 'in-use'), { recursive: true }) expect(sshRelayArtifactCacheInUseLeasePath(root, entry.contentId, 'd'.repeat(32))).toContain( join('in-use', 'a'.repeat(64)) diff --git a/src/main/ssh/ssh-relay-artifact-cache-in-use-lease.ts b/src/main/ssh/ssh-relay-artifact-cache-in-use-lease.ts index 4a1603d01c5..649ca1a109c 100644 --- a/src/main/ssh/ssh-relay-artifact-cache-in-use-lease.ts +++ b/src/main/ssh/ssh-relay-artifact-cache-in-use-lease.ts @@ -1,5 +1,5 @@ import { randomBytes } from 'node:crypto' -import { lstat, mkdir, open, rename, rm, type FileHandle } from 'node:fs/promises' +import { lstat, mkdir, open, realpath, rename, rm, type FileHandle } from 'node:fs/promises' import { hostname } from 'node:os' import { dirname, join, resolve } from 'node:path' @@ -217,14 +217,23 @@ export async function acquireSshRelayArtifactCacheInUseLease({ const timeout = AbortSignal.timeout(ACQUISITION_TIMEOUT_MS) const activeSignal = signal ? AbortSignal.any([signal, timeout]) : timeout activeSignal.throwIfAborted() - const expectedPath = resolve(cacheRoot, 'entries', exactContentHex(entry.contentId)) - if (resolve(entry.entryPath) !== expectedPath) { + // Why: publication returns physical entry paths, so logical aliases such as macOS /var must be + // canonicalized before exact identity checks and all lease-owned writes. + const physicalCacheRoot = await realpath(resolve(cacheRoot)) + const expectedPath = resolve(physicalCacheRoot, 'entries', exactContentHex(entry.contentId)) + const physicalEntryPath = await realpath(resolve(entry.entryPath)).catch((error) => { + if (sshRelayArtifactCacheErrorCode(error) === 'ENOENT') { + throw new Error('SSH relay artifact cache in-use entry no longer exists', { cause: error }) + } + throw error + }) + if (physicalEntryPath !== expectedPath) { throw new Error( 'SSH relay artifact cache in-use entry path disagrees with its content identity' ) } const lock = await acquireSshRelayArtifactCacheLock({ - cacheRoot, + cacheRoot: physicalCacheRoot, contentId: entry.contentId, signal: activeSignal }) @@ -238,9 +247,12 @@ export async function acquireSshRelayArtifactCacheInUseLease({ if (!metadata.isDirectory() || metadata.isSymbolicLink()) { throw new Error('SSH relay artifact cache in-use entry is not a complete final directory') } - const lease = await createLease(cacheRoot, entry.contentId) + const lease = await createLease(physicalCacheRoot, entry.contentId) try { - await recordSshRelayArtifactCacheRecency({ cacheRoot, contentId: entry.contentId }) + await recordSshRelayArtifactCacheRecency({ + cacheRoot: physicalCacheRoot, + contentId: entry.contentId + }) return lease } catch (error) { await lease.release().catch(() => {}) diff --git a/src/main/ssh/ssh-relay-artifact-cache-population-integration.test.ts b/src/main/ssh/ssh-relay-artifact-cache-population-integration.test.ts new file mode 100644 index 00000000000..633d541f0d9 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-population-integration.test.ts @@ -0,0 +1,76 @@ +import { mkdtemp, readFile, readdir, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { lookupSshRelayArtifactCacheEntry } from './ssh-relay-artifact-cache-entry' +import { createSshRelayArtifactCacheEntryFixture } from './ssh-relay-artifact-cache-entry-fixture' +import { acquireSshRelayArtifactCacheInUseLease } from './ssh-relay-artifact-cache-in-use-lease' +import { populateSshRelayArtifactCache } from './ssh-relay-artifact-cache-population' + +const { netFetchMock } = vi.hoisted(() => ({ netFetchMock: vi.fn() })) + +vi.mock('electron', () => ({ net: { fetch: netFetchMock } })) + +const temporaryDirectories: string[] = [] + +afterEach(async () => { + netFetchMock.mockReset() + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })) + ) +}) + +describe('SSH relay artifact cache cold population integration', () => { + it.each(['linux', 'win32'] as const)( + 'composes the real verified download, %s publication, and in-use lease', + async (os) => { + const root = await mkdtemp(join(tmpdir(), 'orca-relay-cache-population-integration-')) + temporaryDirectories.push(root) + const fixture = await createSshRelayArtifactCacheEntryFixture({ root, os }) + const archive = await readFile(fixture.archivePath) + netFetchMock.mockResolvedValueOnce( + new Response(archive, { + status: 200, + headers: { 'content-length': String(archive.length) } + }) + ) + const cacheRoot = join(root, 'cache') + + const result = await populateSshRelayArtifactCache({ + cacheRoot, + artifact: fixture.artifact + }) + try { + expect(result.entry).toMatchObject({ + contentId: fixture.artifact.contentId, + tupleId: fixture.artifact.tupleId, + files: fixture.artifact.archive.fileCount, + expandedBytes: fixture.artifact.archive.expandedSize + }) + for (const [path, bytes] of fixture.fileBytes) { + await expect( + readFile(join(result.entry.runtimeRoot, ...path.split('/'))) + ).resolves.toEqual(bytes) + } + await expect(readdir(join(cacheRoot, 'downloads'))).resolves.toEqual([]) + await expect(result.lease.assertOwned()).resolves.toBeUndefined() + } finally { + await result.lease.release() + } + const warm = await lookupSshRelayArtifactCacheEntry({ cacheRoot, artifact: fixture.artifact }) + if (warm.kind !== 'hit') { + throw new Error('Expected the freshly populated cache entry to remain warm') + } + const warmLease = await acquireSshRelayArtifactCacheInUseLease({ + cacheRoot, + entry: warm.entry + }) + await warmLease.release() + expect(netFetchMock).toHaveBeenCalledTimes(1) + } + ) +}) diff --git a/src/main/ssh/ssh-relay-artifact-cache-population.test.ts b/src/main/ssh/ssh-relay-artifact-cache-population.test.ts new file mode 100644 index 00000000000..dbb37a17a24 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-population.test.ts @@ -0,0 +1,283 @@ +import { mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import nacl from 'tweetnacl' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { + populateSshRelayArtifactCache, + type SshRelayArtifactCachePopulationOperations +} from './ssh-relay-artifact-cache-population' +import { createSshRelayArtifactTestManifest } from './ssh-relay-artifact-test-manifest' +import { + selectSshRelayArtifact, + type SshRelaySelectedArtifact +} from './ssh-relay-artifact-selector' +import { + signSshRelayArtifactManifest, + sshRelayManifestKeyId, + verifySshRelayArtifactManifest +} from './ssh-relay-manifest-signature' + +const keyPair = nacl.sign.keyPair.fromSeed(Uint8Array.from({ length: 32 }, (_, index) => index)) +const temporaryDirectories: string[] = [] + +function selectedArtifact(): SshRelaySelectedArtifact { + const manifest = createSshRelayArtifactTestManifest() + manifest.signatures = [signSshRelayArtifactManifest(manifest, keyPair.secretKey)] + const verified = verifySshRelayArtifactManifest(manifest, [ + { keyId: sshRelayManifestKeyId(keyPair.publicKey), publicKey: keyPair.publicKey } + ]) + const result = selectSshRelayArtifact(verified, { + os: 'linux', + architecture: 'x64', + processTranslated: false, + kernelVersion: '6.8', + libc: { family: 'glibc', version: '2.39' }, + libstdcxxVersion: '6.0.33', + glibcxxVersion: '3.4.33' + }) + if (result.kind !== 'selected') { + throw new Error('Expected a selected test artifact') + } + return result +} + +async function testCacheRoot(): Promise { + const root = await mkdtemp(join(tmpdir(), 'orca-relay-cache-population-')) + temporaryDirectories.push(root) + return join(root, 'cache') +} + +function cacheEntry(cacheRoot: string, artifact: SshRelaySelectedArtifact) { + const entryPath = join(cacheRoot, 'entries', artifact.contentId.slice('sha256:'.length)) + return { + contentId: artifact.contentId, + tupleId: artifact.tupleId, + entryPath, + archivePath: join(entryPath, artifact.archive.name), + runtimeRoot: join(entryPath, 'runtime'), + proofPath: join(entryPath, 'proof.json'), + files: artifact.archive.fileCount, + expandedBytes: artifact.archive.expandedSize + } +} + +function lease(token = 'a'.repeat(32)) { + return { + leasePath: join(tmpdir(), 'in-use', token), + token, + assertOwned: vi.fn(async () => {}), + release: vi.fn(async () => {}) + } +} + +const operations = { + download: vi.fn(), + publish: vi.fn(), + acquireInUseLease: vi.fn() +} + +beforeEach(() => { + operations.download.mockReset() + operations.publish.mockReset() + operations.acquireInUseLease.mockReset() +}) + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })) + ) +}) + +async function expectEmptyDownloadStaging(cacheRoot: string): Promise { + await expect(readdir(join(cacheRoot, 'downloads'))).resolves.toEqual([]) +} + +describe('SSH relay artifact cache cold population', () => { + it('downloads exclusively, publishes, cleans staging, and leases before exposure', async () => { + const artifact = selectedArtifact() + const cacheRoot = await testCacheRoot() + const entry = cacheEntry(cacheRoot, artifact) + const acquired = lease() + const order: string[] = [] + let stagingArchive = '' + operations.download.mockImplementation(async ({ destinationPath }) => { + order.push('download') + stagingArchive = destinationPath + expect(destinationPath).toMatch( + new RegExp(`[/\\\\]downloads[/\\\\][0-9a-f]{64}\\.pending-[^/\\\\]+[/\\\\]`) + ) + expect(destinationPath.endsWith(artifact.archive.name)).toBe(true) + await writeFile(destinationPath, 'verified download', { flag: 'wx' }) + return { + destinationPath, + finalUrl: artifact.archive.downloadUrl, + size: artifact.archive.size, + sha256: artifact.archive.sha256 + } + }) + operations.publish.mockImplementation(async ({ archivePath }) => { + order.push('publish') + expect(archivePath).toBe(stagingArchive) + await expect(readFile(archivePath, 'utf8')).resolves.toBe('verified download') + return entry + }) + operations.acquireInUseLease.mockImplementation(async () => { + order.push('lease') + await expect(stat(stagingArchive)).rejects.toMatchObject({ code: 'ENOENT' }) + return acquired + }) + + const result = await populateSshRelayArtifactCache({ cacheRoot, artifact }, operations) + + expect(order).toEqual(['download', 'publish', 'lease']) + expect(result).toEqual({ artifact, entry, lease: acquired }) + expect(Object.isFrozen(result)).toBe(true) + expect(Object.isFrozen(result.entry)).toBe(true) + await expectEmptyDownloadStaging(cacheRoot) + }) + + it('rejects a relative root before filesystem or artifact operations', async () => { + await expect( + populateSshRelayArtifactCache( + { cacheRoot: 'relative/cache', artifact: selectedArtifact() }, + operations + ) + ).rejects.toThrow(/absolute|cache root/i) + expect(operations.download).not.toHaveBeenCalled() + expect(operations.publish).not.toHaveBeenCalled() + expect(operations.acquireInUseLease).not.toHaveBeenCalled() + }) + + it('rejects inconsistent download identity and removes its staging directory', async () => { + const cacheRoot = await testCacheRoot() + const artifact = selectedArtifact() + operations.download.mockImplementation(async ({ destinationPath }) => { + await writeFile(destinationPath, 'wrong download') + return { + destinationPath, + finalUrl: artifact.archive.downloadUrl, + size: artifact.archive.size + 1, + sha256: artifact.archive.sha256 + } + }) + + await expect( + populateSshRelayArtifactCache({ cacheRoot, artifact }, operations) + ).rejects.toThrow(/download|identity|size/i) + expect(operations.publish).not.toHaveBeenCalled() + expect(operations.acquireInUseLease).not.toHaveBeenCalled() + await expectEmptyDownloadStaging(cacheRoot) + }) + + it('cleans staging and propagates download or publication failures closed', async () => { + const cacheRoot = await testCacheRoot() + const artifact = selectedArtifact() + operations.download.mockImplementationOnce(async ({ destinationPath }) => { + await writeFile(destinationPath, 'partial download') + throw new Error('certificate verification failed') + }) + await expect( + populateSshRelayArtifactCache({ cacheRoot, artifact }, operations) + ).rejects.toThrow(/certificate verification failed/i) + expect(operations.publish).not.toHaveBeenCalled() + await expectEmptyDownloadStaging(cacheRoot) + + operations.download.mockImplementationOnce(async ({ destinationPath }) => { + await writeFile(destinationPath, 'verified download') + return { + destinationPath, + finalUrl: artifact.archive.downloadUrl, + size: artifact.archive.size, + sha256: artifact.archive.sha256 + } + }) + operations.publish.mockRejectedValueOnce(new Error('extracted tree integrity failure')) + await expect( + populateSshRelayArtifactCache({ cacheRoot, artifact }, operations) + ).rejects.toThrow(/tree integrity failure/i) + expect(operations.acquireInUseLease).not.toHaveBeenCalled() + await expectEmptyDownloadStaging(cacheRoot) + }) + + it('propagates lease failure after cleaning staging', async () => { + const cacheRoot = await testCacheRoot() + const artifact = selectedArtifact() + operations.download.mockImplementation(async ({ destinationPath }) => { + await writeFile(destinationPath, 'verified download') + return { + destinationPath, + finalUrl: artifact.archive.downloadUrl, + size: artifact.archive.size, + sha256: artifact.archive.sha256 + } + }) + operations.publish.mockResolvedValue(cacheEntry(cacheRoot, artifact)) + operations.acquireInUseLease.mockRejectedValue(new Error('entry evicted before lease')) + + await expect( + populateSshRelayArtifactCache({ cacheRoot, artifact }, operations) + ).rejects.toThrow(/evicted before lease/i) + await expectEmptyDownloadStaging(cacheRoot) + }) + + it('releases a lease when cancellation wins before the result is exposed', async () => { + const cacheRoot = await testCacheRoot() + const artifact = selectedArtifact() + const controller = new AbortController() + const acquired = lease('b'.repeat(32)) + operations.download.mockImplementation(async ({ destinationPath }) => { + await writeFile(destinationPath, 'verified download') + return { + destinationPath, + finalUrl: artifact.archive.downloadUrl, + size: artifact.archive.size, + sha256: artifact.archive.sha256 + } + }) + operations.publish.mockResolvedValue(cacheEntry(cacheRoot, artifact)) + operations.acquireInUseLease.mockImplementation(async () => { + controller.abort(new Error('cancel after cold lease')) + return acquired + }) + + await expect( + populateSshRelayArtifactCache({ cacheRoot, artifact, signal: controller.signal }, operations) + ).rejects.toThrow(/cancel after cold lease/i) + expect(acquired.release).toHaveBeenCalledTimes(1) + await expectEmptyDownloadStaging(cacheRoot) + }) + + it('uses different exclusive staging directories for concurrent populations', async () => { + const cacheRoot = await testCacheRoot() + const artifact = selectedArtifact() + const destinations: string[] = [] + operations.download.mockImplementation(async ({ destinationPath }) => { + destinations.push(destinationPath) + await writeFile(destinationPath, 'verified download', { flag: 'wx' }) + return { + destinationPath, + finalUrl: artifact.archive.downloadUrl, + size: artifact.archive.size, + sha256: artifact.archive.sha256 + } + }) + operations.publish.mockResolvedValue(cacheEntry(cacheRoot, artifact)) + operations.acquireInUseLease + .mockResolvedValueOnce(lease('c'.repeat(32))) + .mockResolvedValueOnce(lease('d'.repeat(32))) + + await expect( + Promise.all([ + populateSshRelayArtifactCache({ cacheRoot, artifact }, operations), + populateSshRelayArtifactCache({ cacheRoot, artifact }, operations) + ]) + ).resolves.toHaveLength(2) + expect(new Set(destinations)).toHaveLength(2) + await expectEmptyDownloadStaging(cacheRoot) + }) +}) diff --git a/src/main/ssh/ssh-relay-artifact-cache-population.ts b/src/main/ssh/ssh-relay-artifact-cache-population.ts new file mode 100644 index 00000000000..7e902bef4c2 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-population.ts @@ -0,0 +1,193 @@ +import { lstat, mkdir, mkdtemp, realpath, rm } from 'node:fs/promises' +import { basename, dirname, isAbsolute, join, resolve } from 'node:path' + +import { publishSshRelayArtifactCacheEntry } from './ssh-relay-artifact-cache-entry' +import type { SshRelayArtifactCacheEntry } from './ssh-relay-artifact-cache-entry-verification' +import { + acquireSshRelayArtifactCacheInUseLease, + type SshRelayArtifactCacheInUseLease +} from './ssh-relay-artifact-cache-in-use-lease' +import { + downloadSshRelayArtifact, + type SshRelayArtifactDownloadResult +} from './ssh-relay-artifact-download' +import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector' + +const DOWNLOAD_DIRECTORY_NAME = 'downloads' +const CONTENT_ID = /^sha256:([0-9a-f]{64})$/ + +type DownloadStaging = Readonly<{ + cacheRoot: string + directory: string + archivePath: string +}> + +function exactContentHex(artifact: SshRelaySelectedArtifact): string { + const match = CONTENT_ID.exec(artifact.contentId) + if (!match) { + throw new Error('SSH relay artifact cache population content ID must be an exact digest') + } + return match[1] +} + +function exactArchiveName(artifact: SshRelaySelectedArtifact): string { + const name = artifact.archive.name + if (!name || name === '.' || name === '..' || basename(name) !== name || /[/\\]/.test(name)) { + throw new Error('SSH relay artifact cache population archive name must be a basename') + } + return name +} + +async function ownedDownloadDirectory(cacheRoot: string): Promise<{ + cacheRoot: string + downloads: string +}> { + const logicalRoot = resolve(cacheRoot) + await mkdir(logicalRoot, { recursive: true, mode: 0o700 }) + const rootMetadata = await lstat(logicalRoot) + if (!rootMetadata.isDirectory() || rootMetadata.isSymbolicLink()) { + throw new Error('SSH relay artifact cache population root must be an owned directory') + } + const physicalRoot = await realpath(logicalRoot) + const logicalDownloads = join(physicalRoot, DOWNLOAD_DIRECTORY_NAME) + await mkdir(logicalDownloads, { recursive: true, mode: 0o700 }) + const downloadsMetadata = await lstat(logicalDownloads) + if (!downloadsMetadata.isDirectory() || downloadsMetadata.isSymbolicLink()) { + throw new Error('SSH relay artifact cache downloads path must be an owned directory') + } + const downloads = await realpath(logicalDownloads) + if (dirname(downloads) !== physicalRoot || basename(downloads) !== DOWNLOAD_DIRECTORY_NAME) { + throw new Error('SSH relay artifact cache downloads path must not traverse a link') + } + return { cacheRoot: physicalRoot, downloads } +} + +async function createDownloadStaging( + cacheRoot: string, + artifact: SshRelaySelectedArtifact +): Promise { + const { cacheRoot: physicalRoot, downloads } = await ownedDownloadDirectory(cacheRoot) + const prefix = `${exactContentHex(artifact)}.pending-` + const logicalDirectory = await mkdtemp(join(downloads, prefix)) + try { + const metadata = await lstat(logicalDirectory) + const directory = await realpath(logicalDirectory) + if ( + !metadata.isDirectory() || + metadata.isSymbolicLink() || + dirname(directory) !== downloads || + !basename(directory).startsWith(prefix) + ) { + throw new Error('SSH relay artifact cache download staging must be an exclusive directory') + } + return Object.freeze({ + cacheRoot: physicalRoot, + directory, + archivePath: join(directory, exactArchiveName(artifact)) + }) + } catch (error) { + await rm(logicalDirectory, { recursive: true, force: true }).catch(() => {}) + throw error + } +} + +function assertDownloadResult( + result: SshRelayArtifactDownloadResult, + artifact: SshRelaySelectedArtifact, + archivePath: string +): void { + if ( + result.destinationPath !== archivePath || + result.size !== artifact.archive.size || + result.sha256 !== artifact.archive.sha256 + ) { + throw new Error('SSH relay artifact cache download result identity is inconsistent') + } +} + +function frozenEntry( + entry: SshRelayArtifactCacheEntry, + artifact: SshRelaySelectedArtifact +): Readonly { + if (entry.contentId !== artifact.contentId || entry.tupleId !== artifact.tupleId) { + throw new Error('SSH relay artifact cache published entry identity is inconsistent') + } + return Object.freeze({ ...entry }) +} + +export type SshRelayArtifactCachePopulationOperations = Readonly<{ + download: typeof downloadSshRelayArtifact + publish: typeof publishSshRelayArtifactCacheEntry + acquireInUseLease: typeof acquireSshRelayArtifactCacheInUseLease +}> + +const DEFAULT_OPERATIONS: SshRelayArtifactCachePopulationOperations = Object.freeze({ + download: downloadSshRelayArtifact, + publish: publishSshRelayArtifactCacheEntry, + acquireInUseLease: acquireSshRelayArtifactCacheInUseLease +}) + +export type SshRelayArtifactCachePopulation = Readonly<{ + artifact: SshRelaySelectedArtifact + entry: Readonly + lease: SshRelayArtifactCacheInUseLease +}> + +export async function populateSshRelayArtifactCache( + { + cacheRoot, + artifact, + signal + }: { + cacheRoot: string + artifact: SshRelaySelectedArtifact + signal?: AbortSignal + }, + operations: SshRelayArtifactCachePopulationOperations = DEFAULT_OPERATIONS +): Promise { + signal?.throwIfAborted() + if (typeof cacheRoot !== 'string' || !isAbsolute(cacheRoot)) { + throw new Error('SSH relay artifact cache population root must be absolute') + } + + const staging = await createDownloadStaging(cacheRoot, artifact) + let stagingRemoved = false + try { + const download = await operations.download({ + artifact, + destinationPath: staging.archivePath, + signal + }) + assertDownloadResult(download, artifact, staging.archivePath) + signal?.throwIfAborted() + // Why: publication already re-hashes, strictly extracts, verifies, and atomically exposes the + // complete tree; this composition must not create a weaker or duplicate extraction path. + const published = await operations.publish({ + cacheRoot: staging.cacheRoot, + artifact, + archivePath: staging.archivePath, + signal + }) + const entry = frozenEntry(published, artifact) + await rm(staging.directory, { recursive: true }) + stagingRemoved = true + signal?.throwIfAborted() + + const lease = await operations.acquireInUseLease({ + cacheRoot: staging.cacheRoot, + entry, + signal + }) + try { + signal?.throwIfAborted() + return Object.freeze({ artifact, entry, lease }) + } catch (error) { + await lease.release().catch(() => {}) + throw error + } + } finally { + if (!stagingRemoved) { + await rm(staging.directory, { recursive: true, force: true }).catch(() => {}) + } + } +} From bd240049ffb9de196ce7cbd0f772639f4ba310f3 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 02:25:46 -0400 Subject: [PATCH 186/267] fix: settle Windows relay cache locks Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 25 +++- ...github-release-implementation-checklist.md | 91 +++++++++++- .../ssh-relay-artifact-cache-lock-lease.ts | 36 +++-- ...-relay-artifact-cache-lock-release.test.ts | 129 ++++++++++++++++++ .../ssh-relay-artifact-cache-lock-release.ts | 73 ++++++++++ 7 files changed, 337 insertions(+), 20 deletions(-) create mode 100644 src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts create mode 100644 src/main/ssh/ssh-relay-artifact-cache-lock-release.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index a9bf767ca86..42549c3eca4 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -264,6 +264,7 @@ jobs: src/main/ssh/ssh-relay-artifact-selector.test.ts \ src/main/ssh/ssh-relay-artifact-download.test.ts \ src/main/ssh/ssh-relay-artifact-extraction.test.ts \ + src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts \ src/main/ssh/ssh-relay-artifact-cache-lock.test.ts \ src/main/ssh/ssh-relay-artifact-cache-entry.test.ts \ src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts \ @@ -806,6 +807,7 @@ jobs: src/main/ssh/ssh-relay-artifact-selector.test.ts ` src/main/ssh/ssh-relay-artifact-download.test.ts ` src/main/ssh/ssh-relay-artifact-extraction.test.ts ` + src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts ` src/main/ssh/ssh-relay-artifact-cache-lock.test.ts ` src/main/ssh/ssh-relay-artifact-cache-entry.test.ts ` src/main/ssh/ssh-relay-artifact-cache-in-use-lease.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 7fe2346c326..b76eb938a0e 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -122,6 +122,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-artifact-selector.test.ts', 'ssh-relay-artifact-download.test.ts', 'ssh-relay-artifact-extraction.test.ts', + 'ssh-relay-artifact-cache-lock-release.test.ts', 'ssh-relay-artifact-cache-lock.test.ts', 'ssh-relay-artifact-cache-entry.test.ts', 'ssh-relay-artifact-cache-in-use-lease.test.ts', 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 82637b67806..35517a07004 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 @@ -9,9 +9,10 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — disconnected cold-cache -population locally green and wired; commit/all-six native proof next, 2026-07-15, Codex -implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — prove the locally green +Windows active-owner cache-lock release correction on replacement all-six native CI before +accepting cold-cache population, 2026-07-15, Codex implementation owner.** +`E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, 16/16 focused+workflow-oracle tests, 323/323 non-full-size SSH relay tests, 280/280 release-script @@ -38,6 +39,24 @@ implementation now canonicalizes lease identity/locking/recency to the existing `E-M5-ARTIFACT-CACHE-PHYSICAL-ROOT-CORRECTION-LOCAL-001`: 7/7 correction tests and 21/21 focused+ workflow-oracle tests pass while missing/misplaced entries remain rejected. Broader exact-head proof is still required before checkpointing. +Exact-head cold-cache artifact run 29475848463 then repeated the prior Windows x64 active-owner +release race in job 87548519537: the new real Linux/Windows cold→warm integration cases pass, but +lock tombstone rename receives `EPERM` and the live waiter later receives teardown `ENOENT` +(`E-M5-ARTIFACT-CACHE-LOCK-RELEASE-CI-RED-001`). The prior ledger explicitly required correction if +this recurred. The bounded sharing-error correction below is locally green, but replacement all-six +native proof remains required; do not advance to acquisition, Electron/startup, SSH, settings, +fallback, tuple, publication, or default behavior. +`E-M5-ARTIFACT-CACHE-LOCK-RELEASE-LOCAL-RED-001` records the expected missing-module failure for the +fixed bounded retry/displacement/exhaustion contract before implementation. +`E-M5-ARTIFACT-CACHE-LOCK-RELEASE-LOCAL-001` is now locally green: only `EPERM`/`EACCES` rename +failures retry for at most 5 s at 50 ms intervals; every retry rechecks exact directory/nonce +ownership, displacement preserves the successor, absent paths settle, and unexpected or exhausted +failures propagate closed. Focused plus workflow-oracle proof passes 24/24, broader non-full-size +relay proof passes 339/339 with the three declared full-size skips, release scripts pass 280/280, +and typecheck, lint, format, diff, and protected-resolver gates pass. A replacement exact-head +all-six native run plus adjacent PR/E2E checks is still mandatory before this correction or the +cold-cache population package is accepted; do not advance to acquisition, Electron/startup, SSH, +settings, fallback, tuple, publication, or default behavior yet. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 8258032198f..8338be33452 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: prove disconnected cold-cache population**. Require the eventual startup caller to supply Orca's pre-`app.setName()` canonical user-data path; keep filesystem orchestration outside import time and production manifest/resources, proxy, SSH/settings/tuples/release publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa686d2a7c96e687bc02c16fdb08bc987d7` on all six primary native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. The disconnected cold population is locally green under `E-M5-ARTIFACT-CACHE-POPULATION-LOCAL-001`: exclusive cache-local download staging → strict existing immutable publication → staging cleanup → in-use lease, with failures propagating closed. Merging to `main` remains prohibited and SignPath is deferred. No Electron/startup, SSH, setting, tuple enablement, release publication, fallback, or default-path consumer exists. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: prove the locally green Windows cache-lock release correction on replacement all-six native CI before accepting cold-cache population**. Keep the correction independently evidenced and bounded; require the eventual startup caller to supply Orca's pre-`app.setName()` canonical user-data path; keep production manifest/resources, proxy, SSH/settings/tuples/release publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **LOCAL GREEN / CI REQUIRED — 2026-07-15, Codex implementation owner** — `E-M5-ARTIFACT-CACHE-LOCK-RELEASE-LOCAL-001` implements and proves a 5 s/50 ms ownership-preserving retry only for Windows sharing-style `EPERM`/`EACCES`; displaced ownership stops without deletion and exhaustion fails closed. Focused/workflow, 339/339 non-full-size relay, 280/280 release-script, typecheck, lint, format, diff, and protected-resolver gates pass locally. The correction is not accepted until a new exact-head run replaces failing job 87548519537 across all six primary native jobs and adjacent checks. Merging to `main` remains prohibited and SignPath is deferred. No Electron/startup, SSH, setting, tuple enablement, release publication, fallback, or default-path consumer exists. 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) @@ -147,10 +147,11 @@ same change as the work it records. local gates, and all-six native Node 24 proof are green under E-M5-ARTIFACT-CACHE-ROOT-LOCAL-RED-001, E-M5-ARTIFACT-CACHE-ROOT-LOCAL-001, and E-M5-ARTIFACT-CACHE-ROOT-CI-001. No Electron or product caller exists. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit the smallest - disconnected resolver/cache composition over explicit dependencies. The eventual startup caller, - not an SSH module, must supply `getCanonicalUserDataPath()` captured before `app.setName()`. - Keep proxy integration, SSH, mode, tuple, publication, and defaults separately gated. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — prove the bounded + ownership-preserving cache-lock release correction on all six native clients before accepting + cold-cache population or auditing the next acquisition composition. The eventual startup caller, + not an SSH module, must supply `getCanonicalUserDataPath()` captured before `app.setName()`; keep + proxy integration, SSH, mode, tuple, publication, and defaults separately gated. - 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 @@ -14603,6 +14604,84 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 4 files / 21 tests - Boundary: this corrects physical path identity only. It adds no network, Electron/startup caller, SSH, setting, fallback, tuple, release publication, or default behavior. +### E-M5-ARTIFACT-CACHE-LOCK-RELEASE-CI-RED-001 — repeated Windows active-owner release race + +- Date/owner: 2026-07-15, Codex implementation owner. +- Exact source/job: `3018977c113926addcbb97b5715242ed86d820d2`, artifact run + [29475848463](https://github.com/stablyai/orca/actions/runs/29475848463), Windows x64 job + [87548519537](https://github.com/stablyai/orca/actions/runs/29475848463/job/87548519537). +- Runner/runtime: GitHub-hosted Windows Server 2022 build 20348 x64, `windows-2022` image + `20260706.237.1`, Actions runner 2.335.1, Node 24.18.0. +- New-path result: both real cold-cache integration cases pass on the Windows client, including the + actual bounded downloader, Linux tar/Brotli and Windows ZIP strict publication, complete-tree + verification, physical-root cold→warm lookup, and in-use lease. +- Failure: the pre-existing `serializes concurrent writers and transfers ownership only after owner + release` test received `EPERM` renaming the owned lock to its release tombstone after the owner + handle closed. Test teardown then removed the root while the waiter was live, producing the + secondary `ENOENT` creating its next pending directory. Combined contract result: 1 failed, 489 + passed, 13 skipped across 76 files in 26.94 s. +- Repetition/decision: this matches `E-M5-COMPILED-TRUST-WINDOWS-CI-RED-001` (job 87532052082), whose + disposition required a separately evidenced correction if it repeated. It is no longer accepted + as a non-repeating runner event. Add a bounded retry only for Windows sharing-style `EPERM`/ + `EACCES`, re-check exact nonce/directory ownership before every retry, never delete a displaced + successor, retain unexpected-error propagation, and prove retry exhaustion is bounded. No test- + only relaxation or blind recursive deletion is allowed. +- Gate: do not accept `3018977c1` or begin the next acquisition package until a correction commit + passes replacement all-six native contracts and adjacent PR/E2E gates. + +### E-M5-ARTIFACT-CACHE-LOCK-RELEASE-LOCAL-RED-001 — bounded release retry is absent + +- Date/owner: 2026-07-15, Codex implementation owner. +- Command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 + src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts`. +- Result: expected FAIL, one failed suite / zero tests in 237 ms because the purpose-named + `ssh-relay-artifact-cache-lock-release` module does not exist. +- Fixed contract: 5 s/50 ms frozen bounds; retry only `EPERM`/`EACCES`; exact ownership recheck + before every retry; stop without deletion on displacement; fail closed at exact exhaustion; + already-absent path settles; unexpected errors propagate without retry. The test seam controls + time and filesystem operations, so exhaustion proof has no real delay or platform conditional. + +### E-M5-ARTIFACT-CACHE-LOCK-RELEASE-LOCAL-001 — bounded ownership-preserving release retry + +- Date/owner: 2026-07-15, Codex implementation owner. +- Implementation: lock release still closes its owned heartbeat handle before renaming the live + lock to its exact nonce-specific tombstone. Only rename `EPERM`/`EACCES` enters a frozen 50 ms, + 5 s retry policy. Each retry re-reads the exact directory identity and owner nonce immediately + before rename; displacement settles without deleting the successor. `ENOENT` is already + released, unexpected rename/ownership errors propagate, exact exhaustion fails closed, and only + a successfully renamed tombstone is recursively removed. There is no blind deletion of the live + lock path. +- Focused plus native-workflow oracle: `/usr/bin/time -l pnpm exec vitest run --config + config/vitest.config.ts --maxWorkers=1 + src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts + src/main/ssh/ssh-relay-artifact-cache-lock.test.ts + config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 3 files / 24 tests in 1.42 s Vitest, + 2.69 s wall, 132,562,944-byte maximum RSS. Seven purpose cases cover both sharing codes, exact + ownership rechecks, displaced-successor preservation, 100 controlled waits over the exact 5 s + budget, absent-path settlement, and unexpected rename/ownership error propagation. The workflow + oracle proves the purpose suite occurs exactly once in both POSIX and PowerShell native commands. +- Broader relay command: `/usr/bin/time -l pnpm exec vitest run --config + config/vitest.config.ts --maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS, + 30 files / 339 tests; 3 declared full-size files/tests skipped; 21.01 s Vitest, 23.06 s wall, + 335,773,696-byte maximum RSS. An earlier quoted-glob invocation matched no files and exited 1; + the exact unquoted checklist command above was then run and passed. +- Release-script command: `/usr/bin/time -l pnpm exec vitest run --config + config/vitest.config.ts --maxWorkers=1 --silent=true + config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 22.20 s Vitest, + 25.44 s wall, 194,150,400-byte maximum RSS. +- Repository gates: targeted `pnpm exec oxfmt --write` completes on five implementation/workflow + files in 428 ms; the post-format focused/workflow suite passes 24/24. `pnpm typecheck` and + `pnpm lint` pass; lint includes switch exhaustiveness, 41 reliability gates, no new max-lines + bypass, bundled skill-guide verification, 9,837 localization references and locale parity, and + zero localization coverage candidates. `git diff --check` passes and the protected + `ssh-remote-node-resolution.ts`/test pair has zero correction diff. Local Node 26.0.0 emits only + the expected Node-24 engine warning; exact Node 24 proof remains the native CI gate. +- Boundary/gate: the correction changes only cache-lock release settlement and native contract + execution. It adds no Electron/startup, network/proxy, SSH, setting, fallback classification, + tuple enablement, release publication, or default behavior. Require a replacement exact-head + all-six native run plus adjacent PR/E2E checks before accepting cold-cache population or starting + the next acquisition package. + ### E-M5-ARTIFACT-CACHE-POPULATION-LOCAL-001 — disconnected cold-cache population - Date/owner: 2026-07-15, Codex implementation owner. diff --git a/src/main/ssh/ssh-relay-artifact-cache-lock-lease.ts b/src/main/ssh/ssh-relay-artifact-cache-lock-lease.ts index 854e6de3c86..afbe89f8916 100644 --- a/src/main/ssh/ssh-relay-artifact-cache-lock-lease.ts +++ b/src/main/ssh/ssh-relay-artifact-cache-lock-lease.ts @@ -1,8 +1,11 @@ -import { lstat, rename, rm, type FileHandle } from 'node:fs/promises' +import { lstat, type FileHandle } from 'node:fs/promises' + +import { releaseSshRelayArtifactCacheLockPath } from './ssh-relay-artifact-cache-lock-release' import { readSshRelayArtifactCacheLockOwner, sameSshRelayArtifactCacheLockDirectory, + sshRelayArtifactCacheErrorCode, sshRelayArtifactCacheLockOwnerBytes, type SshRelayArtifactCacheLockDirectoryIdentity, type SshRelayArtifactCacheLockOwnerRecord @@ -72,14 +75,23 @@ class SshRelayArtifactCacheLockLease { }) } - private async assertPathOwnership(): Promise { - const metadata = await lstat(this.lockPath, { bigint: true }).catch(() => null) + private async ownsPath(): Promise { + const metadata = await lstat(this.lockPath, { bigint: true }).catch((error) => { + if (sshRelayArtifactCacheErrorCode(error) === 'ENOENT') { + return null + } + throw error + }) const current = await readSshRelayArtifactCacheLockOwner(this.lockPath, this.contentId) - if ( - !metadata?.isDirectory() || - !sameSshRelayArtifactCacheLockDirectory(this.directory, metadata) || - current?.token !== this.token - ) { + return Boolean( + metadata?.isDirectory() && + sameSshRelayArtifactCacheLockDirectory(this.directory, metadata) && + current?.token === this.token + ) + } + + private async assertPathOwnership(): Promise { + if (!(await this.ownsPath())) { throw new Error('SSH relay artifact cache lock ownership was displaced') } } @@ -118,9 +130,11 @@ class SshRelayArtifactCacheLockLease { // Windows cannot rename a directory containing an open file; ownership is final-checked first. await this.ownerHandle.close() - const tombstone = `${this.lockPath}.released-${this.token}` - await rename(this.lockPath, tombstone) - await rm(tombstone, { recursive: true, force: true }) + await releaseSshRelayArtifactCacheLockPath({ + lockPath: this.lockPath, + token: this.token, + checkOwnership: async () => ((await this.ownsPath()) ? 'owned' : 'displaced') + }) } } diff --git a/src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts b/src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts new file mode 100644 index 00000000000..4a3150982fe --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts @@ -0,0 +1,129 @@ +import { join } from 'node:path' + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { + releaseSshRelayArtifactCacheLockPath, + SSH_RELAY_ARTIFACT_CACHE_LOCK_RELEASE_LIMITS, + type SshRelayArtifactCacheLockReleaseOperations +} from './ssh-relay-artifact-cache-lock-release' + +function fileError(code: string): NodeJS.ErrnoException { + return Object.assign(new Error(`filesystem ${code}`), { code }) +} + +const lockPath = join('cache', 'locks', `${'a'.repeat(64)}.lock`) +const token = 'b'.repeat(32) +let now = 0 +const operations = { + rename: vi.fn(), + remove: vi.fn(), + wait: vi.fn(), + now: vi.fn() +} + +beforeEach(() => { + now = 0 + operations.rename.mockReset() + operations.remove.mockReset().mockResolvedValue(undefined) + operations.wait.mockReset().mockImplementation(async () => { + now += SSH_RELAY_ARTIFACT_CACHE_LOCK_RELEASE_LIMITS.retryIntervalMs + }) + operations.now.mockReset().mockImplementation(() => now) +}) + +describe('SSH relay artifact cache lock release', () => { + it('pins a bounded retry policy below cancellation settlement', () => { + expect(SSH_RELAY_ARTIFACT_CACHE_LOCK_RELEASE_LIMITS).toEqual({ + retryIntervalMs: 50, + retryTimeoutMs: 5_000 + }) + expect(Object.isFrozen(SSH_RELAY_ARTIFACT_CACHE_LOCK_RELEASE_LIMITS)).toBe(true) + }) + + it('rechecks ownership before retrying transient Windows sharing failures', async () => { + const checkOwnership = vi.fn(async () => 'owned' as const) + operations.rename + .mockRejectedValueOnce(fileError('EPERM')) + .mockRejectedValueOnce(fileError('EACCES')) + .mockResolvedValueOnce(undefined) + + await expect( + releaseSshRelayArtifactCacheLockPath({ lockPath, token, checkOwnership }, operations) + ).resolves.toBeUndefined() + + const tombstone = `${lockPath}.released-${token}` + expect(operations.rename).toHaveBeenCalledTimes(3) + expect(operations.rename).toHaveBeenNthCalledWith(1, lockPath, tombstone) + expect(checkOwnership).toHaveBeenCalledTimes(2) + expect(operations.wait).toHaveBeenCalledTimes(2) + expect(operations.remove).toHaveBeenCalledWith(tombstone) + }) + + it('stops without deleting when ownership is displaced during a sharing retry', async () => { + const checkOwnership = vi.fn(async () => 'displaced' as const) + operations.rename.mockRejectedValueOnce(fileError('EPERM')) + + await expect( + releaseSshRelayArtifactCacheLockPath({ lockPath, token, checkOwnership }, operations) + ).resolves.toBeUndefined() + + expect(operations.rename).toHaveBeenCalledTimes(1) + expect(operations.wait).toHaveBeenCalledTimes(1) + expect(operations.remove).not.toHaveBeenCalled() + }) + + it('fails closed after the exact retry budget without deleting the owned lock', async () => { + const checkOwnership = vi.fn(async () => 'owned' as const) + operations.rename.mockRejectedValue(fileError('EPERM')) + + await expect( + releaseSshRelayArtifactCacheLockPath({ lockPath, token, checkOwnership }, operations) + ).rejects.toThrow(/release|sharing|timeout|blocked/i) + + expect(now).toBe(SSH_RELAY_ARTIFACT_CACHE_LOCK_RELEASE_LIMITS.retryTimeoutMs) + expect(operations.wait).toHaveBeenCalledTimes( + SSH_RELAY_ARTIFACT_CACHE_LOCK_RELEASE_LIMITS.retryTimeoutMs / + SSH_RELAY_ARTIFACT_CACHE_LOCK_RELEASE_LIMITS.retryIntervalMs + ) + expect(operations.remove).not.toHaveBeenCalled() + }) + + it('treats an already absent owned path as released', async () => { + const checkOwnership = vi.fn(async () => 'owned' as const) + operations.rename.mockRejectedValueOnce(fileError('ENOENT')) + + await expect( + releaseSshRelayArtifactCacheLockPath({ lockPath, token, checkOwnership }, operations) + ).resolves.toBeUndefined() + expect(checkOwnership).not.toHaveBeenCalled() + expect(operations.wait).not.toHaveBeenCalled() + expect(operations.remove).not.toHaveBeenCalled() + }) + + it('propagates unexpected filesystem failures without retry or deletion', async () => { + const checkOwnership = vi.fn(async () => 'owned' as const) + operations.rename.mockRejectedValueOnce(fileError('EIO')) + + await expect( + releaseSshRelayArtifactCacheLockPath({ lockPath, token, checkOwnership }, operations) + ).rejects.toThrow(/filesystem EIO/i) + expect(checkOwnership).not.toHaveBeenCalled() + expect(operations.wait).not.toHaveBeenCalled() + expect(operations.remove).not.toHaveBeenCalled() + }) + + it('propagates ownership-inspection errors instead of guessing displacement', async () => { + const checkOwnership = vi.fn(async () => { + throw fileError('EIO') + }) + operations.rename.mockRejectedValueOnce(fileError('EPERM')) + + await expect( + releaseSshRelayArtifactCacheLockPath({ lockPath, token, checkOwnership }, operations) + ).rejects.toThrow(/filesystem EIO/i) + expect(operations.rename).toHaveBeenCalledTimes(1) + expect(operations.wait).toHaveBeenCalledTimes(1) + expect(operations.remove).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/ssh/ssh-relay-artifact-cache-lock-release.ts b/src/main/ssh/ssh-relay-artifact-cache-lock-release.ts new file mode 100644 index 00000000000..9a199c387d4 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-cache-lock-release.ts @@ -0,0 +1,73 @@ +import { rename, rm } from 'node:fs/promises' + +import { sshRelayArtifactCacheErrorCode } from './ssh-relay-artifact-cache-lock-record' + +const RETRY_INTERVAL_MS = 50 +const RETRY_TIMEOUT_MS = 5_000 +const TOKEN = /^[0-9a-f]{32}$/ +const WINDOWS_SHARING_ERRORS = new Set(['EACCES', 'EPERM']) + +export const SSH_RELAY_ARTIFACT_CACHE_LOCK_RELEASE_LIMITS = Object.freeze({ + retryIntervalMs: RETRY_INTERVAL_MS, + retryTimeoutMs: RETRY_TIMEOUT_MS +}) + +export type SshRelayArtifactCacheLockReleaseOperations = Readonly<{ + rename: (sourcePath: string, destinationPath: string) => Promise + remove: (path: string) => Promise + wait: () => Promise + now: () => number +}> + +const DEFAULT_OPERATIONS: SshRelayArtifactCacheLockReleaseOperations = Object.freeze({ + rename, + remove: async (path) => rm(path, { recursive: true, force: true }), + wait: async () => new Promise((resolveWait) => setTimeout(resolveWait, RETRY_INTERVAL_MS)), + now: Date.now +}) + +export async function releaseSshRelayArtifactCacheLockPath( + { + lockPath, + token, + checkOwnership + }: { + lockPath: string + token: string + checkOwnership: () => Promise<'owned' | 'displaced'> + }, + operations: SshRelayArtifactCacheLockReleaseOperations = DEFAULT_OPERATIONS +): Promise { + if (!TOKEN.test(token)) { + throw new Error('SSH relay artifact cache lock release token must be exact lowercase hex') + } + const tombstone = `${lockPath}.released-${token}` + const deadline = operations.now() + RETRY_TIMEOUT_MS + let retrying = false + while (true) { + if (retrying && (await checkOwnership()) === 'displaced') { + return + } + try { + await operations.rename(lockPath, tombstone) + break + } catch (error) { + const code = sshRelayArtifactCacheErrorCode(error) + if (code === 'ENOENT') { + return + } + if (!WINDOWS_SHARING_ERRORS.has(code ?? '')) { + throw error + } + if (operations.now() >= deadline) { + throw new Error('SSH relay artifact cache lock release remained blocked by file sharing', { + cause: error + }) + } + await operations.wait() + // Why: a sharing retry may proceed only while this exact nonce still owns this directory. + retrying = true + } + } + await operations.remove(tombstone) +} From 6715b5153c7fdd29160b7e7f0c161614f3b9e5cf Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 02:45:10 -0400 Subject: [PATCH 187/267] docs: record relay cache correction CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 21 ++++-- ...github-release-implementation-checklist.md | 68 +++++++++++++++++-- 2 files changed, 75 insertions(+), 14 deletions(-) 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 35517a07004..da603a87bf3 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 @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — prove the locally green -Windows active-owner cache-lock release correction on replacement all-six native CI before -accepting cold-cache population, 2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — audit the next smallest +disconnected acquisition composition after exact-head acceptance of cold-cache population, +2026-07-15, Codex implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -53,10 +53,17 @@ failures retry for at most 5 s at 50 ms intervals; every retry rechecks exact di ownership, displacement preserves the successor, absent paths settle, and unexpected or exhausted failures propagate closed. Focused plus workflow-oracle proof passes 24/24, broader non-full-size relay proof passes 339/339 with the three declared full-size skips, release scripts pass 280/280, -and typecheck, lint, format, diff, and protected-resolver gates pass. A replacement exact-head -all-six native run plus adjacent PR/E2E checks is still mandatory before this correction or the -cold-cache population package is accepted; do not advance to acquisition, Electron/startup, SSH, -settings, fallback, tuple, publication, or default behavior yet. +and typecheck, lint, format, diff, and protected-resolver gates pass. This local evidence was not +sufficient until the replacement exact-head all-six native run and adjacent checks recorded below. +`E-M5-ARTIFACT-CACHE-LOCK-RELEASE-CI-001` now closes correction commit `bd240049f` and accepts the +cold-cache population package: all six primary native Node 24 jobs and both Linux supplements pass; +Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. Windows arm64 executes the full +runtime smoke and retains only the declared hosted build-26200 rejection against required 26100. +Every primary client passes its contract command and full-size extraction/cache lifecycle; the +repeated Windows x64 lock-release race is closed. The next package is an audit of the smallest +disconnected acquisition composition only. Electron/startup, live proxy/network, SSH, settings, +fallback, tuple enablement, release publication, and default behavior remain absent, and legacy +remains the sole production path. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 8338be33452..f8ff6434356 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: prove the locally green Windows cache-lock release correction on replacement all-six native CI before accepting cold-cache population**. Keep the correction independently evidenced and bounded; require the eventual startup caller to supply Orca's pre-`app.setName()` canonical user-data path; keep production manifest/resources, proxy, SSH/settings/tuples/release publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **LOCAL GREEN / CI REQUIRED — 2026-07-15, Codex implementation owner** — `E-M5-ARTIFACT-CACHE-LOCK-RELEASE-LOCAL-001` implements and proves a 5 s/50 ms ownership-preserving retry only for Windows sharing-style `EPERM`/`EACCES`; displaced ownership stops without deletion and exhaustion fails closed. Focused/workflow, 339/339 non-full-size relay, 280/280 release-script, typecheck, lint, format, diff, and protected-resolver gates pass locally. The correction is not accepted until a new exact-head run replaces failing job 87548519537 across all six primary native jobs and adjacent checks. Merging to `main` remains prohibited and SignPath is deferred. No Electron/startup, SSH, setting, tuple enablement, release publication, fallback, or default-path consumer exists. Legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the next smallest disconnected acquisition composition after exact-head acceptance of cold-cache population**. Require the eventual startup caller to supply Orca's pre-`app.setName()` canonical user-data path; keep production manifest/resources, proxy, SSH/settings/tuples/release publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **CI CLOSED / NEXT AUDIT — 2026-07-15, Codex implementation owner** — `E-M5-ARTIFACT-CACHE-LOCK-RELEASE-CI-001` closes correction commit `bd240049ffb9de196ce7cbd0f772639f4ba310f3` and accepts the cold-cache population package: all six primary native Node 24 jobs, both Linux oldest-userland supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. Windows arm64 retains only the declared hosted build-26200 rejection against required 26100. The next package may audit a disconnected acquisition boundary only; do not add Electron/startup, SSH, setting, fallback, tuple enablement, release publication, or default-path behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -147,11 +147,11 @@ same change as the work it records. local gates, and all-six native Node 24 proof are green under E-M5-ARTIFACT-CACHE-ROOT-LOCAL-RED-001, E-M5-ARTIFACT-CACHE-ROOT-LOCAL-001, and E-M5-ARTIFACT-CACHE-ROOT-CI-001. No Electron or product caller exists. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — prove the bounded - ownership-preserving cache-lock release correction on all six native clients before accepting - cold-cache population or auditing the next acquisition composition. The eventual startup caller, - not an SSH module, must supply `getCanonicalUserDataPath()` captured before `app.setName()`; keep - proxy integration, SSH, mode, tuple, publication, and defaults separately gated. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit the next + smallest disconnected acquisition composition now that the cache-lock correction and cold-cache + population are exact-head accepted. The eventual startup caller, not an SSH module, must supply + `getCanonicalUserDataPath()` captured before `app.setName()`; keep proxy integration, SSH, mode, + tuple, publication, fallback, and defaults separately gated. - 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 @@ -14682,6 +14682,60 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 4 files / 21 tests all-six native run plus adjacent PR/E2E checks before accepting cold-cache population or starting the next acquisition package. +### E-M5-ARTIFACT-CACHE-LOCK-RELEASE-CI-001 — replacement native correction and cold-cache acceptance + +- Date/owner: 2026-07-15, Codex implementation owner. +- Exact source/run: correction commit `bd240049ffb9de196ce7cbd0f772639f4ba310f3`, artifact run + [29476740292](https://github.com/stablyai/orca/actions/runs/29476740292). The six primary build + jobs all pass with Node 24.18.0 and the exact source commit: Linux x64 job + [87551250555](https://github.com/stablyai/orca/actions/runs/29476740292/job/87551250555) + (`ubuntu-24.04`, `ubuntu24` image `20260714.240.1`, x64); Linux arm64 job + [87551250534](https://github.com/stablyai/orca/actions/runs/29476740292/job/87551250534) + (`ubuntu-24.04-arm`, `ubuntu24-arm64` image `20260706.52.2`); macOS x64 job + [87551250530](https://github.com/stablyai/orca/actions/runs/29476740292/job/87551250530) + (`macos-15-intel`, `macos15` image `20260629.0276.1`, Darwin 24.6.0); macOS arm64 job + [87551250627](https://github.com/stablyai/orca/actions/runs/29476740292/job/87551250627) + (`macos-15`, `macos15` image `20260706.0213.1`, Darwin 24.6.0); Windows x64 job + [87551250538](https://github.com/stablyai/orca/actions/runs/29476740292/job/87551250538) + (`windows-2022`, `win22` image `20260714.244.1`, Windows Server 2022 Datacenter build 20348); and + Windows arm64 job + [87551250513](https://github.com/stablyai/orca/actions/runs/29476740292/job/87551250513) + (`windows-11-arm`, `win11-arm64` image `20260714.109.1`, Windows 10 Enterprise build 26200). +- Contract result: every POSIX client passes 76 files / 506 tests; every Windows client passes 77 + files / 497 tests with 13 declared platform/full-size skips. Windows x64 completes in 26.55 s and + directly replaces failing job 87548519537: the concurrent lock handoff, seven bounded-release + purpose cases, real Linux tar/Brotli and Windows ZIP cold→warm population, exact physical-root + lease, strict tree proof, and all other runtime contracts pass. Windows arm64 completes the same + contract in 24.80 s; Linux x64/arm64 in 10.34/10.71 s and macOS x64/arm64 in 26.29/22.81 s. +- Full-size result: every primary job passes native extraction and immutable cache lifecycle. Across + all six clients, extraction is 1,050.16–3,613.00 ms with 32,280,576–48,705,536 bytes incremental + RSS; cold publication is 1,316.89–4,810.28 ms with 40,620,032–52,109,312 bytes incremental RSS; + warm verified lookup is 138.28–656.65 ms with 0–6,438,912 bytes incremental RSS; active retention + is 9.46–44.46 ms with 0–1,847,296 bytes incremental RSS; and eviction is 18.36–90.42 ms with + 65,536–2,166,784 bytes incremental RSS while reclaiming each exact 118,418,686–161,259,304-byte + entry. These remain within the recorded desktop budgets and show no latency/memory regression. +- Supplemental/baseline result: oldest-userland Linux x64 job + [87552366350](https://github.com/stablyai/orca/actions/runs/29476740292/job/87552366350) and arm64 + job [87552366347](https://github.com/stablyai/orca/actions/runs/29476740292/job/87552366347) + pass. Windows x64 baseline job + [87552698722](https://github.com/stablyai/orca/actions/runs/29476740292/job/87552698722) passes. + Windows arm64 baseline job + [87552698719](https://github.com/stablyai/orca/actions/runs/29476740292/job/87552698719) executes + the full runtime smoke, then fails closed only because the hosted image reports kernel/OS build + 26200 while the immutable tuple contract requires exact build 26100; platform and architecture + match. This is the existing declared runner-floor gap, not a cache-lock or cold-population failure. +- Adjacent exact-head result: PR Checks + [29476740298](https://github.com/stablyai/orca/actions/runs/29476740298), Golden E2E + [29476740309](https://github.com/stablyai/orca/actions/runs/29476740309), and computer-use + [29476740291](https://github.com/stablyai/orca/actions/runs/29476740291) pass. Computer-use includes + successful Ubuntu 22.04 and Windows native smoke jobs; the selector-only mac/Linux/Windows rows + are expected skips. +- Acceptance/boundary: this evidence supersedes `E-M5-ARTIFACT-CACHE-LOCK-RELEASE-CI-RED-001` and + accepts the disconnected cold-cache population package. It does not prove Electron/startup, + proxy/live network, SSH transfer/install/launch, Beta settings, fallback classification, tuple + enablement, release publication, or default behavior. Legacy remains the only production path; + the next package may only audit the next disconnected acquisition composition. + ### E-M5-ARTIFACT-CACHE-POPULATION-LOCAL-001 — disconnected cold-cache population - Date/owner: 2026-07-15, Codex implementation owner. From c170ff92c1c5c82c31048cf9a9245c13efdaddd1 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 02:58:45 -0400 Subject: [PATCH 188/267] feat: acquire verified relay artifacts Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 4 + .../ssh-relay-runtime-workflow.test.mjs | 2 + ...elease-implementation-checklist-summary.md | 24 +- ...github-release-implementation-checklist.md | 108 +++++++- ...y-artifact-acquisition-integration.test.ts | 74 +++++ .../ssh-relay-artifact-acquisition.test.ts | 254 ++++++++++++++++++ .../ssh/ssh-relay-artifact-acquisition.ts | 151 +++++++++++ .../ssh-relay-artifact-cache-entry-fixture.ts | 14 +- 8 files changed, 620 insertions(+), 11 deletions(-) create mode 100644 src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts create mode 100644 src/main/ssh/ssh-relay-artifact-acquisition.test.ts create mode 100644 src/main/ssh/ssh-relay-artifact-acquisition.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 42549c3eca4..c2cbd60d792 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -273,6 +273,8 @@ jobs: src/main/ssh/ssh-relay-artifact-cache-resolution.test.ts \ src/main/ssh/ssh-relay-artifact-cache-population.test.ts \ src/main/ssh/ssh-relay-artifact-cache-population-integration.test.ts \ + src/main/ssh/ssh-relay-artifact-acquisition.test.ts \ + src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts \ src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts \ src/main/ssh/ssh-relay-official-manifest.test.ts \ src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts \ @@ -816,6 +818,8 @@ jobs: src/main/ssh/ssh-relay-artifact-cache-resolution.test.ts ` src/main/ssh/ssh-relay-artifact-cache-population.test.ts ` src/main/ssh/ssh-relay-artifact-cache-population-integration.test.ts ` + src/main/ssh/ssh-relay-artifact-acquisition.test.ts ` + src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts ` src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts ` src/main/ssh/ssh-relay-official-manifest.test.ts ` src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index b76eb938a0e..816923aee75 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -131,6 +131,8 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-artifact-cache-resolution.test.ts', 'ssh-relay-artifact-cache-population.test.ts', 'ssh-relay-artifact-cache-population-integration.test.ts', + 'ssh-relay-artifact-acquisition.test.ts', + 'ssh-relay-artifact-acquisition-integration.test.ts', 'ssh-relay-compiled-manifest-trust.test.ts', 'ssh-relay-official-manifest.test.ts', 'ssh-relay-manifest-accepted-keys.test.ts', 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 da603a87bf3..b0e26f06779 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 @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — audit the next smallest -disconnected acquisition composition after exact-head acceptance of cold-cache population, -2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — checkpoint and prove the +locally green disconnected warm/cold acquisition composition on exact-head native CI, 2026-07-15, +Codex implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -64,6 +64,24 @@ repeated Windows x64 lock-release race is closed. The next package is an audit o disconnected acquisition composition only. Electron/startup, live proxy/network, SSH, settings, fallback, tuple enablement, release publication, and default behavior remain absent, and legacy remains the sole production path. +`E-M5-ARTIFACT-CACHE-ACQUISITION-AUDIT-001` fixes the next narrow boundary: explicit verified +manifest/host/cache-root inputs; unavailable/compatibility short circuits; a source-qualified leased +ready result from a warm hit without download; and exactly one accepted cold-population call on a +verified miss. Final identity and cancellation are checked before exposure, with lease release on +failure. A real Linux/Windows cold→warm client-offline integration must fetch exactly once. Manifest +loading, Electron/startup, proxy policy, SSH, settings, fallback classification, tuple enablement, +publication, and defaults remain absent. +`E-M5-ARTIFACT-ACQUISITION-LOCAL-RED-001` records the expected two-suite missing-module failure for +nine purpose cases plus real Linux/Windows cold→warm client-offline integration before +implementation. +`E-M5-ARTIFACT-ACQUISITION-LOCAL-001` is green: 11/11 purpose/integration and 18/18 focused+ +workflow-oracle tests pass; 350/350 non-full-size relay and 280/280 release-script tests plus +typecheck, lint, format, diff, and protected-resolver gates pass. Real Linux tar/Brotli and Windows +ZIP paths perform exactly one verified cold fetch, release the lease, then acquire warm while the +next client fetch is configured offline. Ready results are source-qualified and leased; identity, +cancellation, and all lower-layer errors fail closed without classification. The package remains +disconnected from manifest loading, Electron/startup, proxy policy, SSH, settings, fallback, tuple +enablement, publication, and defaults. Exact-head all-six native proof is next. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 f8ff6434356..b08e4f468e3 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the next smallest disconnected acquisition composition after exact-head acceptance of cold-cache population**. Require the eventual startup caller to supply Orca's pre-`app.setName()` canonical user-data path; keep production manifest/resources, proxy, SSH/settings/tuples/release publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **CI CLOSED / NEXT AUDIT — 2026-07-15, Codex implementation owner** — `E-M5-ARTIFACT-CACHE-LOCK-RELEASE-CI-001` closes correction commit `bd240049ffb9de196ce7cbd0f772639f4ba310f3` and accepts the cold-cache population package: all six primary native Node 24 jobs, both Linux oldest-userland supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. Windows arm64 retains only the declared hosted build-26200 rejection against required 26100. The next package may audit a disconnected acquisition boundary only; do not add Electron/startup, SSH, setting, fallback, tuple enablement, release publication, or default-path behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: checkpoint and prove the locally green disconnected warm/cold acquisition composition on exact-head native CI**. Require the eventual startup caller to supply Orca's pre-`app.setName()` canonical user-data path; keep production manifest/resources, proxy, SSH/settings/tuples/release publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **LOCAL GREEN / CI REQUIRED — 2026-07-15, Codex implementation owner** — `E-M5-ARTIFACT-ACQUISITION-LOCAL-RED-001` precedes `E-M5-ARTIFACT-ACQUISITION-LOCAL-001`: 11/11 purpose/integration, 18/18 focused+workflow-oracle, 350/350 non-full-size relay, 280/280 release-script, typecheck, lint, format, diff, and protected-resolver gates pass. Real Linux tar/Brotli and Windows ZIP cases prove one cold verified fetch followed by warm acquisition while the client fetch is offline. No Electron/startup state, fallback classification, SSH/settings, tuple enablement, release publication, or default behavior is connected. Exact-head all-six native proof is required before the next package. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -147,11 +147,11 @@ same change as the work it records. local gates, and all-six native Node 24 proof are green under E-M5-ARTIFACT-CACHE-ROOT-LOCAL-RED-001, E-M5-ARTIFACT-CACHE-ROOT-LOCAL-001, and E-M5-ARTIFACT-CACHE-ROOT-CI-001. No Electron or product caller exists. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit the next - smallest disconnected acquisition composition now that the cache-lock correction and cold-cache - population are exact-head accepted. The eventual startup caller, not an SSH module, must supply - `getCanonicalUserDataPath()` captured before `app.setName()`; keep proxy integration, SSH, mode, - tuple, publication, fallback, and defaults separately gated. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — checkpoint and prove + the locally green disconnected warm/cold acquisition composition on all six native clients. The + eventual startup caller, not an SSH module, must supply `getCanonicalUserDataPath()` captured + before `app.setName()`; keep proxy integration, SSH, mode, tuple, publication, fallback, and + defaults separately gated. - 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 @@ -14736,6 +14736,100 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 4 files / 21 tests enablement, release publication, or default behavior. Legacy remains the only production path; the next package may only audit the next disconnected acquisition composition. +### E-M5-ARTIFACT-CACHE-ACQUISITION-AUDIT-001 — warm/cold leased acquisition boundary + +- Date/owner: 2026-07-15, Codex implementation owner. +- Audit: complete reads of the accepted warm resolution and cold population implementations/tests, + official-manifest and cache-root boundaries, selected-artifact identity, real Linux/Windows cache + fixtures, and the native workflow oracle. A production-caller search confirms both accepted + capabilities remain disconnected outside tests. +- Finding: the smallest next composition consumes an explicit already-verified official manifest, + probed host evidence, absolute cache root, and optional cancellation signal. It returns + unavailable/compatibility-legacy unchanged, maps a verified warm hit to a source-qualified leased + ready result without download, and invokes the accepted cold population exactly once only for a + verified miss. A successful cold result is mapped to the same leased ready shape with `download` + source. No cache miss escapes this boundary. +- Safety contract: final artifact/entry tuple and content identities must match; every returned entry + snapshot/result is frozen; cancellation or identity failure after a warm/cold lease is acquired + releases that lease before rejecting. Resolution, download, integrity, publication, lease, and + cleanup failures propagate unchanged without availability/fallback classification. Concurrent + cold misses retain the accepted unique staging and content-lock publication safety; download + deduplication is deliberately not invented in this composition. +- Required RED: unavailable and compatibility short circuits, warm zero-download result, exact + miss→population order/call identity, warm/cold post-lease cancellation release, inconsistent + population identity failure/release, and fail-closed resolution/population errors. A real + Linux/Windows integration must prove cold download followed by warm client-offline acquisition + with exactly one client fetch. +- Deliberately absent: official-manifest loading, canonical user-data/Electron/startup adaptation, + proxy/certificate policy, eviction scheduling, SSH transfer/install/launch, `ORCA_RELAY_PATH`, + per-target Beta settings, fallback state machine/reason classification, tuple enablement, release + publication, production resources, and default behavior. The primary HTML plan is unchanged. + +### E-M5-ARTIFACT-ACQUISITION-LOCAL-RED-001 — warm/cold composition is absent + +- Date/owner: 2026-07-15, Codex implementation owner. +- Command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 + src/main/ssh/ssh-relay-artifact-acquisition.test.ts + src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts`. +- Result: expected FAIL, two failed suites / zero tests in 616 ms because the purpose-named + `ssh-relay-artifact-acquisition` module does not exist. +- Fixed contract: nine purpose cases define unavailable/legacy zero-population short circuits, + frozen cache/download ready results, exact resolve→populate miss ordering, pre-abort, warm/cold + post-lease cancellation release, inconsistent-entry rejection/release, and unclassified error + propagation. Two real Linux tar/Brotli and Windows ZIP cases require a cold verified fetch followed + by a warm acquisition while the client fetch is configured offline, with exactly one fetch total. +- Consumer disconnection: only the two purpose/integration suites import the absent module. No + Electron/startup, product/SSH, setting, fallback, tuple, publication, or default caller exists. + +### E-M5-ARTIFACT-ACQUISITION-LOCAL-001 — disconnected warm/cold client acquisition + +- Date/owner: 2026-07-15, Codex implementation owner. +- Implementation: `ssh-relay-artifact-acquisition.ts` composes only accepted cache resolution and + cold population behind explicit verified-manifest, host-evidence, cache-root, and cancellation + inputs. Unavailable/compatibility results return frozen without population; a warm hit returns a + frozen `ready/cache` leased entry; only a verified miss invokes population and returns the same + shape as `ready/download`. Artifact release/content/archive and entry tuple/content identities are + checked before exposure. Cancellation or identity failure after lease acquisition releases the + lease; every lower-layer error propagates without fallback classification. +- Purpose/integration command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 + src/main/ssh/ssh-relay-artifact-acquisition.test.ts + src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts` — PASS, 2 files / 11 tests in + 1.66 s. Nine purpose cases cover both short circuits, warm/download result shape and operation + order, pre-abort, warm/cold post-lease cancellation, identity failure/release, and unclassified + resolver/population errors. Two real Linux tar/Brotli and Windows ZIP cases pass exact signed + fixture bytes through the default selector, downloader, strict publication/tree proof, lease, and + warm lookup: cold fetch count is one, the lease is released, and a second acquisition succeeds + from cache while the next client fetch is configured to fail; total fetch count remains one. +- Focused plus native workflow oracle: `/usr/bin/time -l pnpm exec vitest run --config + config/vitest.config.ts --maxWorkers=1 + src/main/ssh/ssh-relay-artifact-acquisition.test.ts + src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts + config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 3 files / 18 tests in 2.90 s Vitest, + 5.50 s wall, 132,792,320-byte maximum RSS. Both purpose suites occur exactly once in POSIX and + PowerShell native commands. +- Broader relay command: `/usr/bin/time -l pnpm exec vitest run --config + config/vitest.config.ts --maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS, + 32 files / 350 tests; 3 declared full-size files/tests skipped; accepted rerun 28.77 s Vitest, + 30.56 s wall, 243,253,248-byte maximum RSS. An earlier completed invocation returned only partial + captured output from the command runner and is not counted; the exact command was rerun to an + observed exit code 0 and complete summary. +- Release-script command: `/usr/bin/time -l pnpm exec vitest run --config + config/vitest.config.ts --maxWorkers=1 --silent=true + config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 22.48 s Vitest, + 27.42 s wall, 195,969,024-byte maximum RSS. +- Repository gates: targeted `pnpm exec oxfmt --write` completes on six implementation/workflow + files in 472 ms. `pnpm typecheck` and `pnpm lint` pass; lint includes switch exhaustiveness, 41 + reliability gates, no new max-lines bypass, bundled skill-guide verification, 9,837 localization + references and locale parity, and zero localization coverage candidates. `git diff --check` + passes and the protected `ssh-remote-node-resolution.ts`/test pair has zero package diff. Local + Node 26.0.0 emits only the expected Node-24 engine warning; exact Node 24 proof remains pending. +- Boundary/gate: the fixture exposes its already-verified manifest and exact host evidence only to + support real acquisition tests. There is still no official-manifest loader, Electron/startup or + product caller, proxy policy/live credential path, eviction scheduler, SSH transfer/install, + `ORCA_RELAY_PATH`, Beta setting, fallback classification, tuple enablement, release publication, + production resource, or default-path change. Legacy remains unchanged. Require exact-head all-six + native and adjacent PR/E2E proof before advancing. + ### E-M5-ARTIFACT-CACHE-POPULATION-LOCAL-001 — disconnected cold-cache population - Date/owner: 2026-07-15, Codex implementation owner. diff --git a/src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts b/src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts new file mode 100644 index 00000000000..b486f09c7e8 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts @@ -0,0 +1,74 @@ +import { mkdtemp, readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { acquireSshRelayArtifact } from './ssh-relay-artifact-acquisition' +import { createSshRelayArtifactCacheEntryFixture } from './ssh-relay-artifact-cache-entry-fixture' + +const { netFetchMock } = vi.hoisted(() => ({ netFetchMock: vi.fn() })) + +vi.mock('electron', () => ({ net: { fetch: netFetchMock } })) + +const temporaryDirectories: string[] = [] + +afterEach(async () => { + netFetchMock.mockReset() + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })) + ) +}) + +describe('SSH relay artifact warm/cold acquisition integration', () => { + it.each(['linux', 'win32'] as const)( + 'downloads the %s fixture once and then acquires it while the client is offline', + async (os) => { + const root = await mkdtemp(join(tmpdir(), 'orca-relay-artifact-acquisition-integration-')) + temporaryDirectories.push(root) + const fixture = await createSshRelayArtifactCacheEntryFixture({ root, os }) + const archive = await readFile(fixture.archivePath) + netFetchMock.mockResolvedValueOnce( + new Response(archive, { + status: 200, + headers: { 'content-length': String(archive.length) } + }) + ) + const input = { + officialManifest: fixture.officialManifest, + host: fixture.host, + cacheRoot: join(root, 'cache') + } + + const cold = await acquireSshRelayArtifact(input) + if (cold.kind !== 'ready') { + throw new Error(`Expected ready cold acquisition, got ${cold.kind}`) + } + expect(cold).toMatchObject({ + source: 'download', + artifact: { contentId: fixture.artifact.contentId }, + entry: { contentId: fixture.artifact.contentId } + }) + await cold.lease.release() + + netFetchMock.mockRejectedValueOnce(new Error('client is offline')) + const warm = await acquireSshRelayArtifact(input) + if (warm.kind !== 'ready') { + throw new Error(`Expected ready warm acquisition, got ${warm.kind}`) + } + try { + expect(warm).toMatchObject({ + source: 'cache', + artifact: { contentId: fixture.artifact.contentId }, + entry: { contentId: fixture.artifact.contentId } + }) + await expect(warm.lease.assertOwned()).resolves.toBeUndefined() + } finally { + await warm.lease.release() + } + expect(netFetchMock).toHaveBeenCalledTimes(1) + } + ) +}) diff --git a/src/main/ssh/ssh-relay-artifact-acquisition.test.ts b/src/main/ssh/ssh-relay-artifact-acquisition.test.ts new file mode 100644 index 00000000000..ca6615777c7 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-acquisition.test.ts @@ -0,0 +1,254 @@ +import { join } from 'node:path' +import { tmpdir } from 'node:os' + +import nacl from 'tweetnacl' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { + acquireSshRelayArtifact, + type SshRelayArtifactAcquisitionOperations +} from './ssh-relay-artifact-acquisition' +import { createSshRelayArtifactTestManifest } from './ssh-relay-artifact-test-manifest' +import type { SshRelayArtifactCacheEntry } from './ssh-relay-artifact-cache-entry-verification' +import type { SshRelayArtifactCacheInUseLease } from './ssh-relay-artifact-cache-in-use-lease' +import { + selectSshRelayArtifact, + type SshRelaySelectedArtifact +} from './ssh-relay-artifact-selector' +import type { SshRelayOfficialManifest } from './ssh-relay-official-manifest' +import { + signSshRelayArtifactManifest, + sshRelayManifestKeyId, + verifySshRelayArtifactManifest +} from './ssh-relay-manifest-signature' +import type { SshRelayDigest } from './ssh-relay-runtime-identity' + +const keyPair = nacl.sign.keyPair.fromSeed(Uint8Array.from({ length: 32 }, (_, index) => index)) +const cacheRoot = join(tmpdir(), 'orca-relay-artifact-acquisition') +const host = { + os: 'linux' as const, + architecture: 'x64' as const, + processTranslated: false, + kernelVersion: '6.8.0', + libc: { family: 'glibc' as const, version: '2.39' }, + libstdcxxVersion: '6.0.33', + glibcxxVersion: '3.4.33' +} + +function officialManifest(): SshRelayOfficialManifest { + const manifest = createSshRelayArtifactTestManifest() + manifest.signatures = [signSshRelayArtifactManifest(manifest, keyPair.secretKey)] + return Object.freeze({ + manifest: verifySshRelayArtifactManifest(manifest, [ + { keyId: sshRelayManifestKeyId(keyPair.publicKey), publicKey: keyPair.publicKey } + ]), + acceptedKeysSha256: `sha256:${'a'.repeat(64)}` as SshRelayDigest + }) +} + +function artifact(official: SshRelayOfficialManifest): SshRelaySelectedArtifact { + const selected = selectSshRelayArtifact(official.manifest, host) + if (selected.kind !== 'selected') { + throw new Error(`Expected selected acquisition fixture, got ${selected.reason}`) + } + return selected +} + +function entryFor(selected: ReturnType): SshRelayArtifactCacheEntry { + const entryPath = join(cacheRoot, 'entries', selected.contentId.slice('sha256:'.length)) + return { + contentId: selected.contentId, + tupleId: selected.tupleId, + entryPath, + archivePath: join(entryPath, selected.archive.name), + runtimeRoot: join(entryPath, 'runtime'), + proofPath: join(entryPath, 'proof.json'), + files: selected.archive.fileCount, + expandedBytes: selected.archive.expandedSize + } +} + +function lease(token = 'b'.repeat(32)): SshRelayArtifactCacheInUseLease { + return { + leasePath: join(cacheRoot, 'in-use', token), + token, + assertOwned: vi.fn(async () => {}), + release: vi.fn(async () => {}) + } +} + +const operations = { + resolve: vi.fn(), + populate: vi.fn() +} + +beforeEach(() => { + operations.resolve.mockReset() + operations.populate.mockReset() +}) + +describe('SSH relay artifact warm/cold acquisition', () => { + it('returns unavailable without attempting cold population', async () => { + operations.resolve.mockResolvedValue({ + kind: 'unavailable', + reason: 'official-manifest-unavailable' + }) + + const result = await acquireSshRelayArtifact( + { officialManifest: null, host, cacheRoot }, + operations + ) + + expect(result).toEqual({ kind: 'unavailable', reason: 'official-manifest-unavailable' }) + expect(Object.isFrozen(result)).toBe(true) + expect(operations.populate).not.toHaveBeenCalled() + }) + + it('returns compatibility legacy without attempting cold population', async () => { + operations.resolve.mockResolvedValue({ kind: 'legacy', reason: 'kernel-too-old' }) + + await expect( + acquireSshRelayArtifact({ officialManifest: officialManifest(), host, cacheRoot }, operations) + ).resolves.toEqual({ kind: 'legacy', reason: 'kernel-too-old' }) + expect(operations.populate).not.toHaveBeenCalled() + }) + + it('maps a warm hit to a frozen cache-sourced leased result without download', async () => { + const official = officialManifest() + const selected = artifact(official) + const entry = entryFor(selected) + const acquired = lease() + operations.resolve.mockResolvedValue({ + kind: 'cache-hit', + artifact: selected, + entry, + lease: acquired + }) + + const result = await acquireSshRelayArtifact( + { officialManifest: official, host, cacheRoot }, + operations + ) + + expect(result).toEqual({ + kind: 'ready', + source: 'cache', + artifact: selected, + entry, + lease: acquired + }) + expect(Object.isFrozen(result)).toBe(true) + expect(result.kind === 'ready' && Object.isFrozen(result.entry)).toBe(true) + expect(operations.populate).not.toHaveBeenCalled() + }) + + it('populates exactly once after a verified miss and returns a download-sourced lease', async () => { + const official = officialManifest() + const selected = artifact(official) + const entry = entryFor(selected) + const acquired = lease('c'.repeat(32)) + operations.resolve.mockResolvedValue({ kind: 'cache-miss', artifact: selected }) + operations.populate.mockResolvedValue({ artifact: selected, entry, lease: acquired }) + + const result = await acquireSshRelayArtifact( + { officialManifest: official, host, cacheRoot }, + operations + ) + + expect(result).toMatchObject({ kind: 'ready', source: 'download', artifact: selected, entry }) + expect(operations.resolve).toHaveBeenCalledTimes(1) + expect(operations.populate).toHaveBeenCalledTimes(1) + expect(operations.populate).toHaveBeenCalledWith({ + cacheRoot, + artifact: selected, + signal: undefined + }) + expect(operations.resolve.mock.invocationCallOrder[0]).toBeLessThan( + operations.populate.mock.invocationCallOrder[0] + ) + }) + + it('settles a pre-aborted request before resolution', async () => { + const controller = new AbortController() + controller.abort(new Error('cancel artifact acquisition')) + + await expect( + acquireSshRelayArtifact( + { + officialManifest: officialManifest(), + host, + cacheRoot, + signal: controller.signal + }, + operations + ) + ).rejects.toThrow(/cancel artifact acquisition/i) + expect(operations.resolve).not.toHaveBeenCalled() + }) + + it.each(['cache', 'download'] as const)( + 'releases a %s lease when cancellation wins before exposure', + async (source) => { + const official = officialManifest() + const selected = artifact(official) + const entry = entryFor(selected) + const acquired = lease(source === 'cache' ? 'd'.repeat(32) : 'e'.repeat(32)) + const controller = new AbortController() + if (source === 'cache') { + operations.resolve.mockImplementation(async () => { + controller.abort(new Error('cancel after warm lease')) + return { kind: 'cache-hit', artifact: selected, entry, lease: acquired } + }) + } else { + operations.resolve.mockResolvedValue({ kind: 'cache-miss', artifact: selected }) + operations.populate.mockImplementation(async () => { + controller.abort(new Error('cancel after cold lease')) + return { artifact: selected, entry, lease: acquired } + }) + } + + await expect( + acquireSshRelayArtifact( + { officialManifest: official, host, cacheRoot, signal: controller.signal }, + operations + ) + ).rejects.toThrow(source === 'cache' ? /warm lease/i : /cold lease/i) + expect(acquired.release).toHaveBeenCalledTimes(1) + } + ) + + it('rejects inconsistent cold identity and releases the acquired lease', async () => { + const official = officialManifest() + const selected = artifact(official) + const entry = entryFor(selected) + const acquired = lease('f'.repeat(32)) + operations.resolve.mockResolvedValue({ kind: 'cache-miss', artifact: selected }) + operations.populate.mockResolvedValue({ + artifact: selected, + entry: { ...entry, tupleId: 'win32-x64' }, + lease: acquired + }) + + await expect( + acquireSshRelayArtifact({ officialManifest: official, host, cacheRoot }, operations) + ).rejects.toThrow(/identity|tuple|content/i) + expect(acquired.release).toHaveBeenCalledTimes(1) + }) + + it('propagates warm resolution and cold population failures without classification', async () => { + operations.resolve.mockRejectedValueOnce(new Error('cache integrity failure')) + await expect( + acquireSshRelayArtifact({ officialManifest: officialManifest(), host, cacheRoot }, operations) + ).rejects.toThrow(/cache integrity failure/i) + + const official = officialManifest() + operations.resolve.mockResolvedValueOnce({ + kind: 'cache-miss', + artifact: artifact(official) + }) + operations.populate.mockRejectedValueOnce(new Error('certificate verification failed')) + await expect( + acquireSshRelayArtifact({ officialManifest: official, host, cacheRoot }, operations) + ).rejects.toThrow(/certificate verification failed/i) + }) +}) diff --git a/src/main/ssh/ssh-relay-artifact-acquisition.ts b/src/main/ssh/ssh-relay-artifact-acquisition.ts new file mode 100644 index 00000000000..4208c53a437 --- /dev/null +++ b/src/main/ssh/ssh-relay-artifact-acquisition.ts @@ -0,0 +1,151 @@ +import type { SshRelayArtifactCacheEntry } from './ssh-relay-artifact-cache-entry-verification' +import type { SshRelayArtifactCacheInUseLease } from './ssh-relay-artifact-cache-in-use-lease' +import { + populateSshRelayArtifactCache, + type SshRelayArtifactCachePopulation +} from './ssh-relay-artifact-cache-population' +import { resolveSshRelayArtifactCache } from './ssh-relay-artifact-cache-resolution' +import type { + SshRelayArtifactLegacyReason, + SshRelayHostEvidence, + SshRelaySelectedArtifact +} from './ssh-relay-artifact-selector' +import type { SshRelayOfficialManifest } from './ssh-relay-official-manifest' + +export type SshRelayArtifactAcquisitionOperations = Readonly<{ + resolve: typeof resolveSshRelayArtifactCache + populate: typeof populateSshRelayArtifactCache +}> + +const DEFAULT_OPERATIONS: SshRelayArtifactAcquisitionOperations = Object.freeze({ + resolve: resolveSshRelayArtifactCache, + populate: populateSshRelayArtifactCache +}) + +export type SshRelayArtifactAcquisition = + | Readonly<{ kind: 'unavailable'; reason: 'official-manifest-unavailable' }> + | Readonly<{ kind: 'legacy'; reason: SshRelayArtifactLegacyReason }> + | Readonly<{ + kind: 'ready' + source: 'cache' | 'download' + artifact: SshRelaySelectedArtifact + entry: Readonly + lease: SshRelayArtifactCacheInUseLease + }> + +function sameArtifactIdentity( + expected: SshRelaySelectedArtifact, + actual: SshRelaySelectedArtifact +): boolean { + return ( + actual.tupleId === expected.tupleId && + actual.contentId === expected.contentId && + actual.releaseTag === expected.releaseTag && + actual.archive.name === expected.archive.name && + actual.archive.sha256 === expected.archive.sha256 + ) +} + +function sameEntryIdentity( + artifact: SshRelaySelectedArtifact, + entry: SshRelayArtifactCacheEntry +): boolean { + return entry.tupleId === artifact.tupleId && entry.contentId === artifact.contentId +} + +async function readyResult({ + source, + expectedArtifact, + actualArtifact, + entry, + lease, + signal +}: { + source: 'cache' | 'download' + expectedArtifact: SshRelaySelectedArtifact + actualArtifact: SshRelaySelectedArtifact + entry: SshRelayArtifactCacheEntry + lease: SshRelayArtifactCacheInUseLease + signal?: AbortSignal +}): Promise { + try { + signal?.throwIfAborted() + if ( + !sameArtifactIdentity(expectedArtifact, actualArtifact) || + !sameEntryIdentity(expectedArtifact, entry) + ) { + throw new Error('SSH relay artifact acquisition identity is inconsistent') + } + return Object.freeze({ + kind: 'ready', + source, + artifact: expectedArtifact, + entry: Object.freeze({ ...entry }), + lease + }) + } catch (error) { + await lease.release().catch(() => {}) + throw error + } +} + +function unavailableResult(): SshRelayArtifactAcquisition { + return Object.freeze({ kind: 'unavailable', reason: 'official-manifest-unavailable' }) +} + +function legacyResult(reason: SshRelayArtifactLegacyReason): SshRelayArtifactAcquisition { + return Object.freeze({ kind: 'legacy', reason }) +} + +export async function acquireSshRelayArtifact( + { + officialManifest, + host, + cacheRoot, + signal + }: { + officialManifest: SshRelayOfficialManifest | null + host: SshRelayHostEvidence + cacheRoot: string + signal?: AbortSignal + }, + operations: SshRelayArtifactAcquisitionOperations = DEFAULT_OPERATIONS +): Promise { + signal?.throwIfAborted() + const resolution = await operations.resolve({ officialManifest, host, cacheRoot, signal }) + + if (resolution.kind === 'unavailable') { + signal?.throwIfAborted() + return unavailableResult() + } + if (resolution.kind === 'legacy') { + signal?.throwIfAborted() + return legacyResult(resolution.reason) + } + if (resolution.kind === 'cache-hit') { + return readyResult({ + source: 'cache', + expectedArtifact: resolution.artifact, + actualArtifact: resolution.artifact, + entry: resolution.entry, + lease: resolution.lease, + signal + }) + } + + signal?.throwIfAborted() + // Why: availability and integrity classification belongs to the later fallback state machine. + const populated: SshRelayArtifactCachePopulation = await operations.populate({ + cacheRoot, + artifact: resolution.artifact, + signal + }) + return readyResult({ + source: 'download', + expectedArtifact: resolution.artifact, + actualArtifact: populated.artifact, + entry: populated.entry, + lease: populated.lease, + signal + }) +} diff --git a/src/main/ssh/ssh-relay-artifact-cache-entry-fixture.ts b/src/main/ssh/ssh-relay-artifact-cache-entry-fixture.ts index de2bd48b420..8a49083ab45 100644 --- a/src/main/ssh/ssh-relay-artifact-cache-entry-fixture.ts +++ b/src/main/ssh/ssh-relay-artifact-cache-entry-fixture.ts @@ -16,6 +16,7 @@ import { type SshRelayHostEvidence, type SshRelaySelectedArtifact } from './ssh-relay-artifact-selector' +import type { SshRelayOfficialManifest } from './ssh-relay-official-manifest' import { signSshRelayArtifactManifest, sshRelayManifestKeyId, @@ -103,6 +104,8 @@ export type SshRelayArtifactCacheEntryFixture = { archivePath: string artifact: SshRelaySelectedArtifact fileBytes: ReadonlyMap + host: SshRelayHostEvidence + officialManifest: SshRelayOfficialManifest } export async function createSshRelayArtifactCacheEntryFixture({ @@ -172,5 +175,14 @@ export async function createSshRelayArtifactCacheEntryFixture({ } const archivePath = join(root, tuple.archive.name) await writeFile(archivePath, archive, { mode: 0o600 }) - return { archivePath, artifact, fileBytes } + return { + archivePath, + artifact, + fileBytes, + host, + officialManifest: Object.freeze({ + manifest: verified, + acceptedKeysSha256: sha256(keyPair.publicKey) + }) + } } From 248dce617894ec129cbf423ef2559e6715f809b5 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 03:18:59 -0400 Subject: [PATCH 189/267] docs: record relay acquisition CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 16 +++-- ...github-release-implementation-checklist.md | 67 +++++++++++++++++-- 2 files changed, 72 insertions(+), 11 deletions(-) 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 b0e26f06779..ba08b9e33dc 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 @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — checkpoint and prove the -locally green disconnected warm/cold acquisition composition on exact-head native CI, 2026-07-15, -Codex implementation owner.** +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — audit the next smallest +remaining resolver/cache boundary after exact-head acceptance of disconnected warm/cold +acquisition, 2026-07-15, Codex implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -81,7 +81,15 @@ ZIP paths perform exactly one verified cold fetch, release the lease, then acqui next client fetch is configured offline. Ready results are source-qualified and leased; identity, cancellation, and all lower-layer errors fail closed without classification. The package remains disconnected from manifest loading, Electron/startup, proxy policy, SSH, settings, fallback, tuple -enablement, publication, and defaults. Exact-head all-six native proof is next. +enablement, publication, and defaults. Exact-head all-six native proof is recorded immediately below. +`E-M5-ARTIFACT-ACQUISITION-CI-001` closes acquisition commit `c170ff92c`: all six primary native +Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use +pass. Windows arm64 retains only the declared hosted build-26200 rejection against required 26100 +after full runtime smoke. Every native contract command proves real Linux/Windows cold→warm client- +offline acquisition, and every full-size extraction/cache lifecycle remains inside its latency and +memory budgets. Live GitHub/CDN/proxy/certificate behavior, manifest/startup adaptation, SSH, +settings, fallback, tuple enablement, publication, and defaults remain open; legacy remains the sole +production path. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 b08e4f468e3..3357a3f485d 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: checkpoint and prove the locally green disconnected warm/cold acquisition composition on exact-head native CI**. Require the eventual startup caller to supply Orca's pre-`app.setName()` canonical user-data path; keep production manifest/resources, proxy, SSH/settings/tuples/release publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **LOCAL GREEN / CI REQUIRED — 2026-07-15, Codex implementation owner** — `E-M5-ARTIFACT-ACQUISITION-LOCAL-RED-001` precedes `E-M5-ARTIFACT-ACQUISITION-LOCAL-001`: 11/11 purpose/integration, 18/18 focused+workflow-oracle, 350/350 non-full-size relay, 280/280 release-script, typecheck, lint, format, diff, and protected-resolver gates pass. Real Linux tar/Brotli and Windows ZIP cases prove one cold verified fetch followed by warm acquisition while the client fetch is offline. No Electron/startup state, fallback classification, SSH/settings, tuple enablement, release publication, or default behavior is connected. Exact-head all-six native proof is required before the next package. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the next smallest remaining resolver/cache boundary after exact-head acceptance of disconnected warm/cold acquisition**. Require the eventual startup caller to supply Orca's pre-`app.setName()` canonical user-data path; keep production manifest/resources, proxy, SSH/settings/tuples/release publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **CI CLOSED / NEXT AUDIT — 2026-07-15, Codex implementation owner** — `E-M5-ARTIFACT-ACQUISITION-CI-001` closes acquisition commit `c170ff92c1c5c82c31048cf9a9245c13efdaddd1`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. Windows arm64 retains only the declared hosted build-26200 rejection against required 26100 after full runtime smoke. Real Linux/Windows cold→warm client-offline acquisition passes in every native contract command. No Electron/startup state, fallback classification, SSH/settings, tuple enablement, release publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -147,11 +147,11 @@ same change as the work it records. local gates, and all-six native Node 24 proof are green under E-M5-ARTIFACT-CACHE-ROOT-LOCAL-RED-001, E-M5-ARTIFACT-CACHE-ROOT-LOCAL-001, and E-M5-ARTIFACT-CACHE-ROOT-CI-001. No Electron or product caller exists. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — checkpoint and prove - the locally green disconnected warm/cold acquisition composition on all six native clients. The - eventual startup caller, not an SSH module, must supply `getCanonicalUserDataPath()` captured - before `app.setName()`; keep proxy integration, SSH, mode, tuple, publication, fallback, and - defaults separately gated. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit the next + smallest remaining resolver/cache boundary after exact-head acceptance of disconnected warm/cold + acquisition. The eventual startup caller, not an SSH module, must supply + `getCanonicalUserDataPath()` captured before `app.setName()`; keep proxy integration, SSH, mode, + tuple, publication, fallback, and defaults separately gated. - 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 @@ -14830,6 +14830,59 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 4 files / 21 tests production resource, or default-path change. Legacy remains unchanged. Require exact-head all-six native and adjacent PR/E2E proof before advancing. +### E-M5-ARTIFACT-ACQUISITION-CI-001 — all-six native warm/cold client-offline acquisition + +- Date/owner: 2026-07-15, Codex implementation owner. +- Exact source/run: acquisition commit `c170ff92c1c5c82c31048cf9a9245c13efdaddd1`, artifact run + [29478400576](https://github.com/stablyai/orca/actions/runs/29478400576). All six primary jobs pass + under Node 24.18.0 with the exact source: Linux x64 job + [87556411878](https://github.com/stablyai/orca/actions/runs/29478400576/job/87556411878) + (`ubuntu-24.04`, `ubuntu24` image `20260705.232.1`); Linux arm64 job + [87556411960](https://github.com/stablyai/orca/actions/runs/29478400576/job/87556411960) + (`ubuntu-24.04-arm`, `ubuntu24-arm64` image `20260706.52.2`); macOS x64 job + [87556411874](https://github.com/stablyai/orca/actions/runs/29478400576/job/87556411874) + (`macos-15-intel`, `macos15` image `20260629.0276.1`); macOS arm64 job + [87556411891](https://github.com/stablyai/orca/actions/runs/29478400576/job/87556411891) + (`macos-15`, `macos15` image `20260706.0213.1`); Windows x64 job + [87556411869](https://github.com/stablyai/orca/actions/runs/29478400576/job/87556411869) + (`windows-2022`, `win22` image `20260706.237.1`, Windows Server 2022 build 20348); and Windows + arm64 job + [87556411842](https://github.com/stablyai/orca/actions/runs/29478400576/job/87556411842) + (`windows-11-arm`, `win11-arm64` image `20260714.109.1`, Windows build 26200). +- Contract result: every POSIX client passes 78 files / 517 tests; every Windows client passes 79 + files / 508 tests with 13 declared platform/full-size skips. Linux x64/arm64 complete in + 17.55/11.41 s, macOS x64/arm64 in 35.07/17.01 s, and Windows x64/arm64 in 22.71/26.48 s. Both + acquisition suites run in each command, including real Linux tar/Brotli and Windows ZIP cold + verified download → lease release → warm client-offline acquisition with exactly one fetch. +- Full-size result: all primary jobs pass extraction and immutable cache lifecycle. Across the six + clients, extraction is 1,198.22–2,686.71 ms with 30,941,184–48,615,424 bytes incremental RSS; + cold publication is 1,336.48–3,751.17 ms with 40,288,256–61,591,552 bytes incremental RSS; warm + verified lookup is 128.21–653.39 ms with 0–6,074,368 bytes incremental RSS; active retention is + 8.49–45.47 ms with 0–1,110,016 bytes incremental RSS; and eviction is 16.73–92.13 ms with + 0–1,986,560 bytes incremental RSS while reclaiming each exact 118,418,686–161,259,373-byte entry. + These remain inside the recorded budgets with no acquisition latency/memory regression. +- Supplemental/baseline result: oldest-userland Linux x64/arm64 jobs + [87557134697](https://github.com/stablyai/orca/actions/runs/29478400576/job/87557134697) and + [87557134698](https://github.com/stablyai/orca/actions/runs/29478400576/job/87557134698) + pass. Windows x64 baseline job + [87558102764](https://github.com/stablyai/orca/actions/runs/29478400576/job/87558102764) passes. + Windows arm64 baseline job + [87558102782](https://github.com/stablyai/orca/actions/runs/29478400576/job/87558102782) verifies + and executes the exact runtime, then fails closed only because observed OS/kernel build 26200 does + not equal required 26100. The retained floor gap is unrelated to acquisition and no tuple is + enabled. +- Adjacent exact-head result: PR Checks + [29478401854](https://github.com/stablyai/orca/actions/runs/29478401854), Golden E2E + [29478400513](https://github.com/stablyai/orca/actions/runs/29478400513), and computer-use + [29478400560](https://github.com/stablyai/orca/actions/runs/29478400560) pass, including unpacked + app, packaged CLI, Ubuntu 22.04, Windows, Linux E2E, and macOS E2E gates. +- Acceptance/boundary: this accepts disconnected source-qualified warm/cold acquisition and proves + its in-memory Electron-response integration portably; it is not live GitHub/CDN/proxy/certificate + or live SSH evidence. Manifest loading, Electron/startup/product adaptation, eviction scheduling, + remote detection/transfer/install, `ORCA_RELAY_PATH`, Beta setting, fallback classification, + tuple enablement, release publication, production resources, and defaults remain absent. Legacy + remains the sole production path. + ### E-M5-ARTIFACT-CACHE-POPULATION-LOCAL-001 — disconnected cold-cache population - Date/owner: 2026-07-15, Codex implementation owner. From d8b17a354bd4c5566381d514c42c9f68e657de8a Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 03:33:55 -0400 Subject: [PATCH 190/267] feat: detect remote Linux libc Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 26 ++- ...github-release-implementation-checklist.md | 159 +++++++++++--- src/main/ssh/ssh-relay-libc-detection.test.ts | 132 ++++++++++++ src/main/ssh/ssh-relay-libc-detection.ts | 197 ++++++++++++++++++ 6 files changed, 483 insertions(+), 34 deletions(-) create mode 100644 src/main/ssh/ssh-relay-libc-detection.test.ts create mode 100644 src/main/ssh/ssh-relay-libc-detection.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index c2cbd60d792..fb37ecbe158 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -262,6 +262,7 @@ jobs: src/main/ssh/ssh-relay-manifest-signature.test.ts \ src/main/ssh/ssh-relay-release-asset.test.ts \ src/main/ssh/ssh-relay-artifact-selector.test.ts \ + src/main/ssh/ssh-relay-libc-detection.test.ts \ src/main/ssh/ssh-relay-artifact-download.test.ts \ src/main/ssh/ssh-relay-artifact-extraction.test.ts \ src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts \ @@ -807,6 +808,7 @@ jobs: src/main/ssh/ssh-relay-manifest-signature.test.ts ` src/main/ssh/ssh-relay-release-asset.test.ts ` src/main/ssh/ssh-relay-artifact-selector.test.ts ` + src/main/ssh/ssh-relay-libc-detection.test.ts ` src/main/ssh/ssh-relay-artifact-download.test.ts ` src/main/ssh/ssh-relay-artifact-extraction.test.ts ` src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 816923aee75..2d23dc5eeb5 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -120,6 +120,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-manifest-signature.test.ts', 'ssh-relay-release-asset.test.ts', 'ssh-relay-artifact-selector.test.ts', + 'ssh-relay-libc-detection.test.ts', 'ssh-relay-artifact-download.test.ts', 'ssh-relay-artifact-extraction.test.ts', 'ssh-relay-artifact-cache-lock-release.test.ts', 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 ba08b9e33dc..58eeb69d757 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 @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — audit the next smallest -remaining resolver/cache boundary after exact-head acceptance of disconnected warm/cold -acquisition, 2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — checkpoint and prove the +locally green disconnected marked Linux libc probe on exact-head native CI, 2026-07-15, Codex +implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -90,6 +90,26 @@ offline acquisition, and every full-size extraction/cache lifecycle remains insi memory budgets. Live GitHub/CDN/proxy/certificate behavior, manifest/startup adaptation, SSH, settings, fallback, tuple enablement, publication, and defaults remain open; legacy remains the sole production path. +`E-M5-LIBC-DETECTION-AUDIT-001` fixes the next smallest package: one disconnected POSIX-shell, +no-Node Linux libc probe. Only complete Orca-marked `getconf GNU_LIBC_VERSION`, `ldd --version`, or +known musl-loader segments count; unmarked startup noise, incomplete/duplicate/malformed segments, +and ambiguous/conflicting candidates return unknown. Ordinary unavailable probes remain +compatibility evidence, while cancellation propagates. Kernel/libstdc++/GLIBCXX and macOS/Windows +host-evidence composition, Electron/startup, live SSH, transfer/install, settings, fallback, tuple +enablement, publication, and defaults stay absent. +`E-M5-LIBC-DETECTION-LOCAL-RED-001` records the expected one-suite/zero-test missing-module failure +before implementation; its initial 12-case contract fixes marked glibc/musl sources, noise and +malformed/duplicate/conflict rejection, unavailable-command classification, cancellation +propagation, one 15-second exec, and exact no-Node command construction. Three bounded/concatenated- +noise cases are implementation-review hardening recorded only by the later green evidence. +`E-M5-LIBC-DETECTION-LOCAL-001` passes 15/15 purpose, 46/46 focused+workflow, 365/365 non-full-size +relay, 280/280 release-script, typecheck, lint, format, reliability, max-lines, localization, diff, +and protected-resolver gates. One 15-second cancellable POSIX command prefers marked getconf, +accepts exact marked glibc/musl ldd or known-loader evidence, bounds parsed segments/lines, returns +unknown for missing/malformed/oversized/ambiguous/conflicting evidence, and propagates cancellation. +The purpose suite now runs in both native workflow families. This remains unit/contract evidence, +not live SSH or GNU/BusyBox/Alpine proof; full host composition and every production/default path +remain absent. Exact-head all-six native proof is next. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 3357a3f485d..e2bed894de9 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the next smallest remaining resolver/cache boundary after exact-head acceptance of disconnected warm/cold acquisition**. Require the eventual startup caller to supply Orca's pre-`app.setName()` canonical user-data path; keep production manifest/resources, proxy, SSH/settings/tuples/release publication/default behavior absent. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **CI CLOSED / NEXT AUDIT — 2026-07-15, Codex implementation owner** — `E-M5-ARTIFACT-ACQUISITION-CI-001` closes acquisition commit `c170ff92c1c5c82c31048cf9a9245c13efdaddd1`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. Windows arm64 retains only the declared hosted build-26200 rejection against required 26100 after full runtime smoke. Real Linux/Windows cold→warm client-offline acquisition passes in every native contract command. No Electron/startup state, fallback classification, SSH/settings, tuple enablement, release publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: checkpoint and prove the locally green disconnected marked Linux libc probe on exact-head native CI**. Keep this capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **LOCAL GREEN / CI REQUIRED — 2026-07-15, Codex implementation owner** — `E-M5-LIBC-DETECTION-LOCAL-001` passes 15/15 purpose, 46/46 focused+workflow, 365/365 non-full-size relay, 280/280 release-script, typecheck, lint, format, max-lines, reliability, localization, diff, and protected-resolver gates. Marked getconf/glibc, ldd glibc/musl, known-loader musl, noise/conflict/bounds, missing-command classification, and cancellation are covered. This is not live SSH/remote-distro evidence and no host-evidence composer, product caller, setting, transfer/install, fallback classifier, tuple enablement, release publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -1052,10 +1052,11 @@ Suggested concrete modules: ### Platform and libc selection - [ ] Reuse current OS/architecture detection without changing supported relay-platform parsing. -- [ ] Add marked, noise-resistant, no-Node libc probes. -- [ ] Prefer `getconf GNU_LIBC_VERSION` when present; parse only output following Orca’s marker. -- [ ] Detect musl through marked `ldd --version` output and known loader paths without treating - arbitrary startup text as evidence. +- [x] Add marked, noise-resistant, no-Node libc probes. (E-M5-LIBC-DETECTION-LOCAL-001) +- [x] Prefer `getconf GNU_LIBC_VERSION` when present; parse only output following Orca’s marker. + (E-M5-LIBC-DETECTION-LOCAL-001) +- [x] Detect musl through marked `ldd --version` output and known loader paths without treating + arbitrary startup text as evidence. (E-M5-LIBC-DETECTION-LOCAL-001) - [x] Validate required libc/libstdc++/kernel/OS versions against manifest constraints. E-M5-OFFLINE-SELECTION-LOCAL-001 validates Linux kernel/glibc/libstdc++/GLIBCXX, macOS, and Windows build/OpenSSH/PowerShell/.NET exact minima from a signature-verified manifest. @@ -14616,7 +14617,7 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 4 files / 21 tests actual bounded downloader, Linux tar/Brotli and Windows ZIP strict publication, complete-tree verification, physical-root cold→warm lookup, and in-use lease. - Failure: the pre-existing `serializes concurrent writers and transfers ownership only after owner - release` test received `EPERM` renaming the owned lock to its release tombstone after the owner +release` test received `EPERM` renaming the owned lock to its release tombstone after the owner handle closed. Test teardown then removed the root while the waiter was live, producing the secondary `ENOENT` creating its next pending directory. Combined contract result: 1 failed, 489 passed, 13 skipped across 76 files in 26.94 s. @@ -14633,7 +14634,7 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 4 files / 21 tests - Date/owner: 2026-07-15, Codex implementation owner. - Command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 - src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts`. +src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts`. - Result: expected FAIL, one failed suite / zero tests in 237 ms because the purpose-named `ssh-relay-artifact-cache-lock-release` module does not exist. - Fixed contract: 5 s/50 ms frozen bounds; retry only `EPERM`/`EACCES`; exact ownership recheck @@ -14652,22 +14653,22 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 4 files / 21 tests a successfully renamed tombstone is recursively removed. There is no blind deletion of the live lock path. - Focused plus native-workflow oracle: `/usr/bin/time -l pnpm exec vitest run --config - config/vitest.config.ts --maxWorkers=1 - src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts - src/main/ssh/ssh-relay-artifact-cache-lock.test.ts - config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 3 files / 24 tests in 1.42 s Vitest, +config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts +src/main/ssh/ssh-relay-artifact-cache-lock.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 3 files / 24 tests in 1.42 s Vitest, 2.69 s wall, 132,562,944-byte maximum RSS. Seven purpose cases cover both sharing codes, exact ownership rechecks, displaced-successor preservation, 100 controlled waits over the exact 5 s budget, absent-path settlement, and unexpected rename/ownership error propagation. The workflow oracle proves the purpose suite occurs exactly once in both POSIX and PowerShell native commands. - Broader relay command: `/usr/bin/time -l pnpm exec vitest run --config - config/vitest.config.ts --maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS, +config/vitest.config.ts --maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS, 30 files / 339 tests; 3 declared full-size files/tests skipped; 21.01 s Vitest, 23.06 s wall, 335,773,696-byte maximum RSS. An earlier quoted-glob invocation matched no files and exited 1; the exact unquoted checklist command above was then run and passed. - Release-script command: `/usr/bin/time -l pnpm exec vitest run --config - config/vitest.config.ts --maxWorkers=1 --silent=true - config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 22.20 s Vitest, +config/vitest.config.ts --maxWorkers=1 --silent=true +config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 22.20 s Vitest, 25.44 s wall, 194,150,400-byte maximum RSS. - Repository gates: targeted `pnpm exec oxfmt --write` completes on five implementation/workflow files in 428 ms; the post-format focused/workflow suite passes 24/24. `pnpm typecheck` and @@ -14769,8 +14770,8 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 4 files / 21 tests - Date/owner: 2026-07-15, Codex implementation owner. - Command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 - src/main/ssh/ssh-relay-artifact-acquisition.test.ts - src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts`. +src/main/ssh/ssh-relay-artifact-acquisition.test.ts +src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts`. - Result: expected FAIL, two failed suites / zero tests in 616 ms because the purpose-named `ssh-relay-artifact-acquisition` module does not exist. - Fixed contract: nine purpose cases define unavailable/legacy zero-population short circuits, @@ -14792,8 +14793,8 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 4 files / 21 tests checked before exposure. Cancellation or identity failure after lease acquisition releases the lease; every lower-layer error propagates without fallback classification. - Purpose/integration command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 - src/main/ssh/ssh-relay-artifact-acquisition.test.ts - src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts` — PASS, 2 files / 11 tests in +src/main/ssh/ssh-relay-artifact-acquisition.test.ts +src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts` — PASS, 2 files / 11 tests in 1.66 s. Nine purpose cases cover both short circuits, warm/download result shape and operation order, pre-abort, warm/cold post-lease cancellation, identity failure/release, and unclassified resolver/population errors. Two real Linux tar/Brotli and Windows ZIP cases pass exact signed @@ -14801,21 +14802,21 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 4 files / 21 tests warm lookup: cold fetch count is one, the lease is released, and a second acquisition succeeds from cache while the next client fetch is configured to fail; total fetch count remains one. - Focused plus native workflow oracle: `/usr/bin/time -l pnpm exec vitest run --config - config/vitest.config.ts --maxWorkers=1 - src/main/ssh/ssh-relay-artifact-acquisition.test.ts - src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts - config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 3 files / 18 tests in 2.90 s Vitest, +config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-artifact-acquisition.test.ts +src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 3 files / 18 tests in 2.90 s Vitest, 5.50 s wall, 132,792,320-byte maximum RSS. Both purpose suites occur exactly once in POSIX and PowerShell native commands. - Broader relay command: `/usr/bin/time -l pnpm exec vitest run --config - config/vitest.config.ts --maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS, +config/vitest.config.ts --maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS, 32 files / 350 tests; 3 declared full-size files/tests skipped; accepted rerun 28.77 s Vitest, 30.56 s wall, 243,253,248-byte maximum RSS. An earlier completed invocation returned only partial captured output from the command runner and is not counted; the exact command was rerun to an observed exit code 0 and complete summary. - Release-script command: `/usr/bin/time -l pnpm exec vitest run --config - config/vitest.config.ts --maxWorkers=1 --silent=true - config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 22.48 s Vitest, +config/vitest.config.ts --maxWorkers=1 --silent=true +config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 22.48 s Vitest, 27.42 s wall, 195,969,024-byte maximum RSS. - Repository gates: targeted `pnpm exec oxfmt --write` completes on six implementation/workflow files in 472 ms. `pnpm typecheck` and `pnpm lint` pass; lint includes switch exhaustiveness, 41 @@ -14926,6 +14927,102 @@ config/vitest.config.ts --maxWorkers=1 --silent=true config/scripts/ssh-relay-ru - Remaining proof for this slice: commit the coherent package and require all six primary native Node 24 jobs plus adjacent PR/E2E gates before adding another desktop orchestration boundary. +### E-M5-LIBC-DETECTION-AUDIT-001 — disconnected marked Linux libc evidence boundary + +- Date/owner: 2026-07-15, Codex implementation owner. +- Audit: complete reads of the existing remote OS/architecture detection and tests, the signed- + manifest artifact selector and tests, the bounded process-output field scanner, the accepted + acquisition boundary, and every Milestone 5 platform/libc checklist requirement. The existing + platform parser must remain unchanged and the protected Node/npm resolver pair remains untouched. +- Finding: the smallest coherent next package is the suggested purpose-named + `ssh-relay-libc-detection` module, not full host-evidence composition. It consumes an explicit SSH + connection and optional cancellation signal, performs one POSIX-shell/no-Node probe, and returns + only `{ family: glibc|musl, version? }` or `{ family: unknown }`. It prefers marked + `getconf GNU_LIBC_VERSION`; marked `ldd --version` and executable known musl-loader paths provide + conservative fallback evidence. Only complete Orca-marked segments count. +- Safety contract: arbitrary startup text and unterminated/duplicated marker segments are ignored; + malformed, ambiguous, or conflicting marked candidates return unknown. A supported numeric libc + version grammar is exact. Ordinary missing commands/nonzero probe execution return unknown so a + later selector can classify compatibility; an `AbortError` propagates so cancellation can never + masquerade as legacy eligibility. The probe invokes no remote Node, npm, Python, Perl, checksum, + archive, download, or package operation. +- Required RED: marked getconf preference; marked glibc and musl ldd cases; known-loader musl case; + unmarked startup-noise rejection; incomplete/duplicate/conflicting/malformed result rejection; + unavailable command classification; cancellation propagation; exact signal/command construction; + and native-workflow inclusion on POSIX and Windows clients. Record the executable missing-module + failure before adding implementation. +- Deliberately absent: kernel/libstdc++/GLIBCXX/macOS/Windows/process-translation composition, + Electron/startup/product callers, live SSH evidence, SFTP/system-SSH transfer, cache acquisition + wiring, `ORCA_RELAY_PATH`, per-target Beta settings, fallback classification, tuple enablement, + release publication, and default behavior. The primary HTML plan is unchanged. + +### E-M5-LIBC-DETECTION-LOCAL-RED-001 — marked Linux libc probe is absent + +- Date/owner: 2026-07-15, Codex implementation owner. +- Command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-libc-detection.test.ts`. +- Result: expected FAIL, one failed suite / zero tests in 243 ms because the purpose-named + `ssh-relay-libc-detection` module does not exist. +- Fixed contract: the initial 12 cases cover marked getconf/glibc, marked ldd glibc/musl, + known-loader musl, + unmarked noise, unterminated/malformed/duplicate/conflicting evidence, missing-command + classification, `AbortError` propagation, one 15-second cancellable exec, preferred source order, + exact known-loader globs, and absence of Node/npm/Python/Perl/archive/checksum commands. Three + bounded/concatenated-noise cases were added during implementation review and belong to the green + evidence below, not this recorded RED run. +- Consumer disconnection: only the purpose suite imports the absent module. No platform parser, + host-evidence composer, product/SSH caller, setting, fallback, tuple, publication, or default path + changes. + +### E-M5-LIBC-DETECTION-LOCAL-001 — disconnected marked/no-Node Linux libc probe + +- Date/owner: 2026-07-15, Codex implementation owner. +- Implementation: `ssh-relay-libc-detection.ts` performs one 15-second cancellable POSIX-shell + command and returns only exact glibc/musl family+version evidence or unknown. It emits complete + source-qualified markers around preferred `getconf GNU_LIBC_VERSION`, fallback `ldd --version`, + and the first executable known musl-loader glob. The client accepts only complete, unique marked + segments, bounds each segment to eight lines and each line to the shared 4,096-character scan + ceiling, requires strict two-to-four-component numeric versions, deduplicates identical evidence, + and returns unknown for absent, malformed, oversized, ambiguous, or conflicting candidates. + Ordinary execution failure returns unknown; `AbortError` propagates. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-libc-detection.test.ts` — PASS, 1 file / 15 tests in 193 ms + Vitest, 1.02 s wall, 131,973,120-byte maximum RSS. Cases cover marked getconf/glibc, common ldd + glibc, ldd musl, known-loader musl, unmarked and marker-concatenated startup noise, unterminated/ + malformed/duplicate/conflicting evidence, oversized line/segment rejection, ordinary failure, + cancellation propagation, one exact signal/15-second exec, source order, known-loader globs, and + absence of Node/npm/Python/Perl/archive/checksum commands. +- Focused plus native-workflow oracle: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-libc-detection.test.ts +src/main/ssh/ssh-relay-artifact-selector.test.ts +src/main/ssh/ssh-remote-platform-detection.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 4 files / 46 tests in 1.44 s Vitest, + 2.67 s wall, 136,740,864-byte maximum RSS. The existing OS/architecture parser remains green and + unchanged; the workflow oracle requires this purpose suite exactly once in both POSIX and + PowerShell native commands. +- Broader relay command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS, + 33 files / 365 tests; 3 declared full-size files/tests skipped; 19.61 s Vitest, 21.61 s wall, + 261,455,872-byte maximum RSS. +- Release-script command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 --silent=true +config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 23.82 s Vitest, + 25.76 s wall, 190,906,368-byte maximum RSS. +- Repository gates: `pnpm run typecheck`, targeted `oxlint`, targeted `oxfmt --check`, `pnpm lint`, + and `pnpm run check:max-lines-ratchet` pass. Lint includes switch exhaustiveness, 41 reliability + gates, no new max-lines bypass, bundled skill-guide verification, 9,837 localization references + and locale parity, and zero localization coverage candidates. Existing unrelated lint warnings + remain warnings. `git diff --check` passes and the protected Node/npm resolver pair has zero diff. + Local Node 26.0.0 emits only the expected Node-24 engine warning; exact Node 24 proof remains the + native CI gate. +- Boundary/residual gaps: the new module is imported only by its purpose tests. It does not compose + kernel/libstdc++/GLIBCXX, macOS/Windows, or translation evidence and has no Electron/startup, + product SSH, cache acquisition, `ORCA_RELAY_PATH`, setting, transfer/install, fallback, tuple, + publication, or default caller. This local package does not prove real SSH, GNU/BusyBox/Alpine, + localization, `MaxSessions=1`, or remote output/channel-size behavior. Those live cells remain + open; legacy remains the sole production path. Require exact-head all-six native proof before the + next package. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -14984,11 +15081,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Checkpoint the disconnected cold-cache population package and require exact-head Node 24 proof on -Linux, macOS, and Windows x64/arm64. After that proof, audit the next smallest non-signing desktop -orchestration boundary; do not provision production secrets/environments, add an Electron/startup -consumer, embed unreviewed keys or manifest bytes, add SSH transfer/install, mode wiring, fallback, -tuple enablement, release publication, or default behavior. +Checkpoint the disconnected marked Linux libc probe and require exact-head Node 24 proof on Linux, +macOS, and Windows x64/arm64 before auditing the next host-evidence slice. Do not compose full host +evidence, provision production secrets/environments, add an Electron/startup consumer, embed +unreviewed keys or manifest bytes, add SSH transfer/install, mode wiring, fallback, tuple enablement, +release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. diff --git a/src/main/ssh/ssh-relay-libc-detection.test.ts b/src/main/ssh/ssh-relay-libc-detection.test.ts new file mode 100644 index 00000000000..d2837e5d460 --- /dev/null +++ b/src/main/ssh/ssh-relay-libc-detection.test.ts @@ -0,0 +1,132 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { SshConnection } from './ssh-connection' + +const execCommandMock = vi.hoisted(() => vi.fn()) + +vi.mock('./ssh-relay-deploy-helpers', () => ({ + execCommand: execCommandMock +})) + +const { detectSshRelayLinuxLibc } = await import('./ssh-relay-libc-detection') + +const conn = {} as SshConnection +const marker = '__ORCA_SSH_RELAY_LIBC__' + +function segment(source: 'getconf' | 'ldd' | 'loader', ...lines: string[]): string { + return [`${marker} BEGIN ${source}`, ...lines, `${marker} END ${source}`].join('\n') +} + +describe('detectSshRelayLinuxLibc', () => { + beforeEach(() => { + execCommandMock.mockReset() + }) + + it('prefers exact marked getconf evidence and ignores unmarked startup text', async () => { + execCommandMock.mockResolvedValueOnce( + ['musl libc (x86_64)', 'Version 9.9.9', segment('getconf', 'glibc 2.28'), 'glibc 99.0'].join( + '\n' + ) + ) + + await expect(detectSshRelayLinuxLibc(conn)).resolves.toEqual({ + family: 'glibc', + version: '2.28' + }) + }) + + it('accepts exact glibc evidence from a complete marked ldd fallback', async () => { + execCommandMock.mockResolvedValueOnce( + segment('ldd', 'ldd (GNU libc) 2.39', 'Copyright (C) Free Software Foundation') + ) + + await expect(detectSshRelayLinuxLibc(conn)).resolves.toEqual({ + family: 'glibc', + version: '2.39' + }) + }) + + it('accepts exact musl evidence from a complete marked ldd fallback', async () => { + execCommandMock.mockResolvedValueOnce( + segment('ldd', 'musl libc (x86_64)', 'Version 1.2.5', 'Dynamic Program Loader') + ) + + await expect(detectSshRelayLinuxLibc(conn)).resolves.toEqual({ + family: 'musl', + version: '1.2.5' + }) + }) + + it('accepts musl evidence from the first complete known-loader segment', async () => { + execCommandMock.mockResolvedValueOnce( + segment('loader', 'musl libc (aarch64)', 'Version 1.2.4', 'Dynamic Program Loader') + ) + + await expect(detectSshRelayLinuxLibc(conn)).resolves.toEqual({ + family: 'musl', + version: '1.2.4' + }) + }) + + it('returns unknown for conflicting recognized marked candidates', async () => { + execCommandMock.mockResolvedValueOnce( + [segment('getconf', 'glibc 2.28'), segment('ldd', 'musl libc', 'Version 1.2.5')].join('\n') + ) + + await expect(detectSshRelayLinuxLibc(conn)).resolves.toEqual({ family: 'unknown' }) + }) + + it.each([ + [`${marker} BEGIN getconf\nglibc 2.28`, 'unterminated segment'], + [segment('getconf', 'glibc 2'), 'malformed version'], + [ + [segment('getconf', 'glibc 2.28'), segment('getconf', 'glibc 2.28')].join('\n'), + 'duplicate source segment' + ], + [ + `startup noise${marker} BEGIN getconf\nglibc 2.28\n${marker} END getconf`, + 'marker concatenated to startup noise' + ], + [segment('getconf', 'x'.repeat(4097)), 'oversized marked line'], + [segment('ldd', ...Array.from({ length: 9 }, () => 'noise')), 'oversized marked segment'], + ['glibc 2.28\nldd (GNU libc) 2.28', 'unmarked evidence'] + ])('returns unknown for %s', async (output) => { + execCommandMock.mockResolvedValueOnce(output) + + await expect(detectSshRelayLinuxLibc(conn)).resolves.toEqual({ family: 'unknown' }) + }) + + it('classifies an unavailable probe as unknown without inventing fallback policy', async () => { + execCommandMock.mockRejectedValueOnce(new Error('remote command unavailable')) + + await expect(detectSshRelayLinuxLibc(conn)).resolves.toEqual({ family: 'unknown' }) + }) + + it('propagates cancellation and passes the signal into the single bounded probe', async () => { + const signal = new AbortController().signal + const abortError = Object.assign(new Error('cancelled'), { name: 'AbortError' }) + execCommandMock.mockRejectedValueOnce(abortError) + + await expect(detectSshRelayLinuxLibc(conn, { signal })).rejects.toBe(abortError) + expect(execCommandMock).toHaveBeenCalledTimes(1) + expect(execCommandMock).toHaveBeenCalledWith(conn, expect.any(String), { + signal, + timeoutMs: 15_000 + }) + }) + + it('constructs a POSIX-shell no-Node probe with marked preferred and fallback sources', async () => { + execCommandMock.mockResolvedValueOnce(segment('getconf', 'glibc 2.28')) + + await detectSshRelayLinuxLibc(conn) + + const command = execCommandMock.mock.calls[0]?.[1] ?? '' + expect(command).toContain('getconf GNU_LIBC_VERSION') + expect(command).toContain('ldd --version') + expect(command).toContain('/lib/ld-musl-*.so.1') + expect(command).toContain('/usr/lib/*/ld-musl-*.so.1') + expect(command.indexOf('getconf GNU_LIBC_VERSION')).toBeLessThan( + command.indexOf('ldd --version') + ) + expect(command).not.toMatch(/\b(?:node|npm|python|perl|tar|sha256sum|shasum)\b/u) + }) +}) diff --git a/src/main/ssh/ssh-relay-libc-detection.ts b/src/main/ssh/ssh-relay-libc-detection.ts new file mode 100644 index 00000000000..9f31491e14e --- /dev/null +++ b/src/main/ssh/ssh-relay-libc-detection.ts @@ -0,0 +1,197 @@ +import { + getProcessOutputFields, + iterateProcessOutputLines, + PROCESS_OUTPUT_FIELD_SCAN_MAX_CHARS +} from '../../shared/process-output-field-scanner' +import type { SshConnection } from './ssh-connection' +import { execCommand } from './ssh-relay-deploy-helpers' +import type { SshRelayLinuxHostEvidence } from './ssh-relay-artifact-selector' + +type SshRelayLinuxLibcEvidence = SshRelayLinuxHostEvidence['libc'] +type ProbeSource = 'getconf' | 'ldd' | 'loader' +type LibcCandidate = Exclude + +const PROBE_MARKER = '__ORCA_SSH_RELAY_LIBC__' +const PROBE_TIMEOUT_MS = 15_000 +const MAX_SEGMENT_LINES = 8 + +const PROBE_COMMAND = [ + `printf '\n%s\n' '${PROBE_MARKER} BEGIN getconf'`, + 'if command -v getconf >/dev/null 2>&1; then getconf GNU_LIBC_VERSION 2>&1 || :; fi', + `printf '%s\n' '${PROBE_MARKER} END getconf'`, + `printf '%s\n' '${PROBE_MARKER} BEGIN ldd'`, + 'if command -v ldd >/dev/null 2>&1; then ldd --version 2>&1 || :; fi', + `printf '%s\n' '${PROBE_MARKER} END ldd'`, + 'for orca_loader in /lib/ld-musl-*.so.1 /usr/lib/ld-musl-*.so.1 /lib/*/ld-musl-*.so.1 /usr/lib/*/ld-musl-*.so.1; do', + ' if test -x "$orca_loader"; then', + ` printf '%s\n' '${PROBE_MARKER} BEGIN loader'`, + ' "$orca_loader" --version 2>&1 || :', + ` printf '%s\n' '${PROBE_MARKER} END loader'`, + ' break', + ' fi', + 'done' +].join('\n') + +type ParsedSegments = { + invalid: boolean + segments: Partial> +} + +function parseMarker(line: string): { boundary: 'BEGIN' | 'END'; source: ProbeSource } | null { + const fields = getProcessOutputFields(line, 4) + if (fields.length !== 3 || fields[0] !== PROBE_MARKER) { + return null + } + const boundary = fields[1] + const source = fields[2] + if ( + (boundary !== 'BEGIN' && boundary !== 'END') || + (source !== 'getconf' && source !== 'ldd' && source !== 'loader') + ) { + return null + } + return { boundary, source } +} + +function parseMarkedSegments(output: string): ParsedSegments { + const segments: ParsedSegments['segments'] = {} + let active: { source: ProbeSource; lines: string[] } | null = null + let invalid = false + + for (const line of iterateProcessOutputLines(output)) { + const marker = parseMarker(line) + if (!marker) { + if (active) { + if ( + active.lines.length >= MAX_SEGMENT_LINES || + line.length > PROCESS_OUTPUT_FIELD_SCAN_MAX_CHARS + ) { + invalid = true + } else { + active.lines.push(line) + } + } + continue + } + + if (marker.boundary === 'BEGIN') { + if (active) { + invalid = true + } + active = { source: marker.source, lines: [] } + continue + } + + if (!active || active.source !== marker.source || segments[marker.source] !== undefined) { + invalid = true + active = null + continue + } + segments[marker.source] = active.lines + active = null + } + + return { segments, invalid: invalid || active !== null } +} + +function isNumericVersion(value: string | undefined): value is string { + return value !== undefined && /^\d+\.\d+(?:\.\d+){0,2}$/u.test(value) +} + +function parseGetconf(lines: string[] | undefined): LibcCandidate | null { + if (!lines || lines.length !== 1) { + return null + } + const fields = getProcessOutputFields(lines[0], 3) + return fields.length === 2 && fields[0] === 'glibc' && isNumericVersion(fields[1]) + ? { family: 'glibc', version: fields[1] } + : null +} + +function parseMusl(lines: string[]): LibcCandidate | null { + const hasMuslHeader = lines.some((line) => { + const fields = getProcessOutputFields(line, 3) + return fields[0]?.toLowerCase() === 'musl' && fields[1]?.toLowerCase() === 'libc' + }) + if (!hasMuslHeader) { + return null + } + for (const line of lines) { + const fields = getProcessOutputFields(line, 3) + if (fields.length === 2 && fields[0] === 'Version' && isNumericVersion(fields[1])) { + return { family: 'musl', version: fields[1] } + } + } + return null +} + +function parseGlibcLdd(lines: string[]): LibcCandidate | null { + for (const line of lines) { + const fields = getProcessOutputFields(line, 32) + const normalized = line.toLowerCase() + const version = fields.at(-1) + if ( + fields[0] === 'ldd' && + (normalized.includes('glibc') || normalized.includes('gnu libc')) && + isNumericVersion(version) + ) { + return { family: 'glibc', version } + } + } + return null +} + +function addCandidate( + candidates: Map, + candidate: LibcCandidate | null +): void { + if (candidate) { + candidates.set(`${candidate.family}:${candidate.version ?? ''}`, candidate) + } +} + +function parseLibcEvidence(output: string): SshRelayLinuxLibcEvidence { + const parsed = parseMarkedSegments(output) + if (parsed.invalid) { + return Object.freeze({ family: 'unknown' }) + } + + const candidates = new Map() + addCandidate(candidates, parseGetconf(parsed.segments.getconf)) + if (parsed.segments.ldd) { + addCandidate(candidates, parseMusl(parsed.segments.ldd)) + addCandidate(candidates, parseGlibcLdd(parsed.segments.ldd)) + } + if (parsed.segments.loader) { + addCandidate(candidates, parseMusl(parsed.segments.loader)) + } + + if (candidates.size !== 1) { + return Object.freeze({ family: 'unknown' }) + } + const candidate = candidates.values().next().value + return candidate ? Object.freeze({ ...candidate }) : Object.freeze({ family: 'unknown' }) +} + +function isAbortError(error: unknown): boolean { + return error instanceof Error && error.name === 'AbortError' +} + +export async function detectSshRelayLinuxLibc( + conn: SshConnection, + { signal }: { signal?: AbortSignal } = {} +): Promise { + try { + const output = await execCommand(conn, PROBE_COMMAND, { + signal, + timeoutMs: PROBE_TIMEOUT_MS + }) + return parseLibcEvidence(output) + } catch (error) { + // Why: cancellation must not be downgraded into compatibility evidence and trigger later legacy. + if (isAbortError(error)) { + throw error + } + return Object.freeze({ family: 'unknown' }) + } +} From 54d523a81ad89ffcc3983d6c391f1b4f611d3862 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 03:54:59 -0400 Subject: [PATCH 191/267] docs: record relay libc detection CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 14 +++- ...github-release-implementation-checklist.md | 67 +++++++++++++++++-- 2 files changed, 71 insertions(+), 10 deletions(-) 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 58eeb69d757..70231968a75 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 @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — checkpoint and prove the -locally green disconnected marked Linux libc probe on exact-head native CI, 2026-07-15, Codex -implementation owner.** +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — audit the next smallest +remaining disconnected host-evidence boundary after exact-head acceptance of Linux libc detection, +2026-07-15, Codex implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -110,6 +110,14 @@ unknown for missing/malformed/oversized/ambiguous/conflicting evidence, and prop The purpose suite now runs in both native workflow families. This remains unit/contract evidence, not live SSH or GNU/BusyBox/Alpine proof; full host composition and every production/default path remain absent. Exact-head all-six native proof is next. +`E-M5-LIBC-DETECTION-CI-001` closes implementation commit `d8b17a354`: all six primary native Node +24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. +Every POSIX client passes 79 files / 532 tests and every Windows client passes 80 files / 523 tests +with 13 declared skips, including the new 15-case libc suite. Full-size extraction/cache metrics stay +inside budget. Windows arm64 completes exact-byte PTY/watcher smoke, then retains only the declared +hosted build-26200 rejection against required 26100; this is the aggregate workflow's sole failure. +Real SSH/distro behavior, remaining host evidence, composition, and every production/default path +remain open; legacy remains the sole production path. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 e2bed894de9..e07e32c9af9 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: checkpoint and prove the locally green disconnected marked Linux libc probe on exact-head native CI**. Keep this capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **LOCAL GREEN / CI REQUIRED — 2026-07-15, Codex implementation owner** — `E-M5-LIBC-DETECTION-LOCAL-001` passes 15/15 purpose, 46/46 focused+workflow, 365/365 non-full-size relay, 280/280 release-script, typecheck, lint, format, max-lines, reliability, localization, diff, and protected-resolver gates. Marked getconf/glibc, ldd glibc/musl, known-loader musl, noise/conflict/bounds, missing-command classification, and cancellation are covered. This is not live SSH/remote-distro evidence and no host-evidence composer, product caller, setting, transfer/install, fallback classifier, tuple enablement, release publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the next smallest remaining disconnected host-evidence boundary after exact-head acceptance of Linux libc detection**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **CI CLOSED / NEXT AUDIT — 2026-07-15, Codex implementation owner** — `E-M5-LIBC-DETECTION-CI-001` closes implementation commit `d8b17a354`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. Windows arm64 executes the full runtime smoke and retains only the declared hosted build-26200 rejection against required 26100. Every native client runs the new 15-case marked/no-Node libc contract. This remains contract evidence, not live SSH/distro evidence; no host-evidence composer, product caller, setting, transfer/install, fallback classifier, tuple enablement, release publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -15023,6 +15023,60 @@ config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 2 open; legacy remains the sole production path. Require exact-head all-six native proof before the next package. +### E-M5-LIBC-DETECTION-CI-001 — all-six native marked Linux libc contract + +- Date/owner: 2026-07-15, Codex implementation owner. +- Exact source/run: implementation commit `d8b17a354bd4c5566381d514c42c9f68e657de8a`, artifact run + [29480364900](https://github.com/stablyai/orca/actions/runs/29480364900). All six primary jobs pass + under Node 24.18.0 with that exact source: Linux x64 job + [87562476656](https://github.com/stablyai/orca/actions/runs/29480364900/job/87562476656) + (`ubuntu-24.04`, `ubuntu24` image `20260705.232.1`, x64); Linux arm64 job + [87562476613](https://github.com/stablyai/orca/actions/runs/29480364900/job/87562476613) + (`ubuntu-24.04-arm`, `ubuntu24-arm64` image `20260714.61.1`, native arm64); macOS x64 job + [87562476686](https://github.com/stablyai/orca/actions/runs/29480364900/job/87562476686) + (`macos-15-intel`, `macos15` image `20260629.0276.1`, x64); macOS arm64 job + [87562476665](https://github.com/stablyai/orca/actions/runs/29480364900/job/87562476665) + (`macos-15`, `macos15` image `20260706.0213.1`, native arm64); Windows x64 job + [87562476713](https://github.com/stablyai/orca/actions/runs/29480364900/job/87562476713) + (`windows-2022`, `win22` image `20260714.244.1`, x64); and Windows arm64 job + [87562476682](https://github.com/stablyai/orca/actions/runs/29480364900/job/87562476682) + (`windows-11-arm`, `win11-arm64` image `20260714.109.1`, native arm64). +- Contract result: every POSIX client passes 79 files / 532 tests. Linux x64/arm64 complete in + 13.83/11.37 s and macOS x64/arm64 in 40.50/35.84 s. Every Windows client passes 80 files / 523 + tests with the 13 declared platform/full-size skips, in 24.78/28.55 s for x64/arm64. The new + 15-case libc suite is required exactly once by both native workflow families and therefore runs on + every client architecture under Node 24; all existing selector, cache, release, runtime, and + workflow contracts remain green. +- Full-size result: extraction spans 1,239.81–4,316.50 ms with 33,431,552–48,717,824 bytes + incremental RSS. Cold immutable-cache publication spans 1,730.07–6,529.74 ms with + 32,653,312–50,659,328 bytes; warm verified lookup 147.39–883.09 ms with 0–6,361,088 bytes; active + retention 11.88–57.61 ms with 0–1,310,720 bytes; and eviction 28.58–119.18 ms with 0–860,160 + bytes while reclaiming each exact 118,418,686–161,259,376-byte entry. All remain within the + recorded budgets; this libc-only package adds no full-size latency or memory path. +- Supplemental/baseline result: oldest-userland Linux x64 job + [87563395186](https://github.com/stablyai/orca/actions/runs/29480364900/job/87563395186) + (`ubuntu-24.04`, image `20260705.232.1`) and Linux arm64 job + [87563395287](https://github.com/stablyai/orca/actions/runs/29480364900/job/87563395287) + (`ubuntu-24.04-arm`, image `20260714.61.1`) pass. Windows x64 baseline job + [87564525786](https://github.com/stablyai/orca/actions/runs/29480364900/job/87564525786) + (`windows-2022`, image `20260706.237.1`) passes. Windows arm64 baseline job + [87564525789](https://github.com/stablyai/orca/actions/runs/29480364900/job/87564525789) + (`windows-11-arm`, image `20260714.109.1`) verifies the exact bytes and completes PTY/watcher + smoke in 8,077.57 ms, then fails closed only because the observed OS/kernel build is 26200 while + the immutable contract requires 26100; platform and architecture checks pass. That declared + hosted-runner floor gap is the sole reason the aggregate workflow conclusion is failure. +- Adjacent exact-head result: PR Checks + [29480364896](https://github.com/stablyai/orca/actions/runs/29480364896), Golden E2E + [29480365058](https://github.com/stablyai/orca/actions/runs/29480365058), and computer-use + [29480365013](https://github.com/stablyai/orca/actions/runs/29480365013) pass, including unpacked + app, packaged CLI, Ubuntu 22.04, Windows native, Linux E2E, and macOS E2E gates. +- Acceptance/boundary: this accepts only the disconnected marked/no-Node Linux libc capability. It + does not prove a real SSH server, GNU/BusyBox/Alpine remote behavior, localization, + `MaxSessions=1`, remote output/channel-size limits, kernel/libstdc++/GLIBCXX evidence, macOS/ + Windows/process-translation evidence, host composition, Electron/startup, transfer/install, + settings, fallback, tuple enablement, publication, or defaults. Legacy remains the sole production + path; no tuple is enabled. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -15081,11 +15135,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Checkpoint the disconnected marked Linux libc probe and require exact-head Node 24 proof on Linux, -macOS, and Windows x64/arm64 before auditing the next host-evidence slice. Do not compose full host -evidence, provision production secrets/environments, add an Electron/startup consumer, embed -unreviewed keys or manifest bytes, add SSH transfer/install, mode wiring, fallback, tuple enablement, -release publication, or default behavior. +Audit the next smallest disconnected host-evidence slice after exact-head acceptance of Linux libc +detection. Do not compose full host evidence, provision production secrets/environments, add an +Electron/startup consumer, embed unreviewed keys or manifest bytes, add SSH transfer/install, mode +wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. From d1eb0eb63e56255d151f2f263e47fa420b970875 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 04:12:20 -0400 Subject: [PATCH 192/267] feat: detect remote Linux kernel Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 31 ++++- ...github-release-implementation-checklist.md | 110 +++++++++++++++++- .../ssh/ssh-relay-artifact-selector.test.ts | 51 ++++++++ src/main/ssh/ssh-relay-artifact-selector.ts | 26 ++++- .../ssh-relay-linux-kernel-detection.test.ts | 87 ++++++++++++++ .../ssh/ssh-relay-linux-kernel-detection.ts | 86 ++++++++++++++ 8 files changed, 384 insertions(+), 10 deletions(-) create mode 100644 src/main/ssh/ssh-relay-linux-kernel-detection.test.ts create mode 100644 src/main/ssh/ssh-relay-linux-kernel-detection.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index fb37ecbe158..d3d4ec16ec2 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -263,6 +263,7 @@ jobs: src/main/ssh/ssh-relay-release-asset.test.ts \ src/main/ssh/ssh-relay-artifact-selector.test.ts \ src/main/ssh/ssh-relay-libc-detection.test.ts \ + src/main/ssh/ssh-relay-linux-kernel-detection.test.ts \ src/main/ssh/ssh-relay-artifact-download.test.ts \ src/main/ssh/ssh-relay-artifact-extraction.test.ts \ src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts \ @@ -809,6 +810,7 @@ jobs: src/main/ssh/ssh-relay-release-asset.test.ts ` src/main/ssh/ssh-relay-artifact-selector.test.ts ` src/main/ssh/ssh-relay-libc-detection.test.ts ` + src/main/ssh/ssh-relay-linux-kernel-detection.test.ts ` src/main/ssh/ssh-relay-artifact-download.test.ts ` src/main/ssh/ssh-relay-artifact-extraction.test.ts ` src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 2d23dc5eeb5..3ada4e78f36 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -121,6 +121,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-release-asset.test.ts', 'ssh-relay-artifact-selector.test.ts', 'ssh-relay-libc-detection.test.ts', + 'ssh-relay-linux-kernel-detection.test.ts', 'ssh-relay-artifact-download.test.ts', 'ssh-relay-artifact-extraction.test.ts', 'ssh-relay-artifact-cache-lock-release.test.ts', 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 70231968a75..6b9e1adb377 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 @@ -9,9 +9,10 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — audit the next smallest -remaining disconnected host-evidence boundary after exact-head acceptance of Linux libc detection, -2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — require exact-head +all-six native CI for the disconnected marked Linux kernel probe and kernel-only distro-suffix +parser accepted locally by `E-M5-LINUX-KERNEL-DETECTION-LOCAL-001`, 2026-07-15, Codex implementation +owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -118,6 +119,30 @@ inside budget. Windows arm64 completes exact-byte PTY/watcher smoke, then retain hosted build-26200 rejection against required 26100; this is the aggregate workflow's sole failure. Real SSH/distro behavior, remaining host evidence, composition, and every production/default path remain open; legacy remains the sole production path. +`E-M5-LINUX-KERNEL-DETECTION-AUDIT-001` fixes the next narrow boundary: one disconnected, +15-second cancellable POSIX-shell/no-Node marked `uname -r` probe plus a kernel-only numeric-prefix +parser. Common supported Rocky/RHEL releases such as `4.18.0-553.5.1.el8_10.x86_64` currently fail +the generic parser because its suffix grammar excludes `_`; the kernel parser will admit only the +bounded distro suffix alphabet `[0-9A-Za-z._+~-]` without relaxing libc, macOS, or Windows version +parsing. Missing, noisy, incomplete, duplicate, malformed, oversized, or invalid evidence returns +unknown; ordinary probe failure remains availability evidence and cancellation propagates. The +reviewed 4.18 floor is unchanged. Host-evidence composition, product callers, live SSH/distro +proof, transfer/install, settings, fallback, tuple enablement, publication, and defaults stay +absent; legacy remains the sole production path. +`E-M5-LINUX-KERNEL-DETECTION-LOCAL-RED-001` records the expected two-file failure before +implementation: the purpose suite collects zero tests because its detection module is absent, and +the existing selector passes 24 cases but fails three Rocky/RHEL assertions. Supported and +below-floor `el8_10` releases both return `unknown-kernel`, proving the underscore suffix gap. Exact +Node 24 and all-six native execution remain required after local green. +`E-M5-LINUX-KERNEL-DETECTION-LOCAL-001` is green: 19/19 purpose tests, 60/60 focused+workflow tests, +394/394 non-full-size relay tests with three declared skips, 280/280 release-script tests, typecheck, +lint, format, reliability, max-lines, localization, diff, and protected-resolver gates pass. One +15-second cancellable marked POSIX command returns only a unique strict `uname -r`; supported +Rocky/RHEL, Ubuntu, and Alpine suffixes select against the unchanged 4.18 floor, while malformed, +oversized, noisy, unavailable, or invalid evidence returns unknown and cancellation propagates. +Libc, macOS, and PowerShell grammars remain strict. This is contract evidence, not live SSH/distro +proof; full host composition and every production/default path remain absent. Exact-head all-six +native proof is next. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 e07e32c9af9..51d5d2e36b2 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the next smallest remaining disconnected host-evidence boundary after exact-head acceptance of Linux libc detection**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **CI CLOSED / NEXT AUDIT — 2026-07-15, Codex implementation owner** — `E-M5-LIBC-DETECTION-CI-001` closes implementation commit `d8b17a354`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. Windows arm64 executes the full runtime smoke and retains only the declared hosted build-26200 rejection against required 26100. Every native client runs the new 15-case marked/no-Node libc contract. This remains contract evidence, not live SSH/distro evidence; no host-evidence composer, product caller, setting, transfer/install, fallback classifier, tuple enablement, release publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: require exact-head all-six native CI for the disconnected marked Linux kernel probe and kernel-only distro-suffix parser accepted locally by E-M5-LINUX-KERNEL-DETECTION-LOCAL-001**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **LOCAL CLOSED / CI NEXT — 2026-07-15, Codex implementation owner** — `E-M5-LINUX-KERNEL-DETECTION-LOCAL-001` passes the 19-case purpose contract, selector and native-workflow gates, 394 non-full-size relay cases, 280 release-script cases, typecheck, lint, format, reliability, localization, max-lines, diff, and protected-resolver gates. The reviewed Linux 4.18 floor is unchanged. No host-evidence composer, product caller, setting, transfer/install, fallback classifier, tuple enablement, release publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -15077,6 +15077,102 @@ config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 2 settings, fallback, tuple enablement, publication, or defaults. Legacy remains the sole production path; no tuple is enabled. +### E-M5-LINUX-KERNEL-DETECTION-AUDIT-001 — marked kernel evidence and distro suffix boundary + +- Date/owner: 2026-07-15, Codex implementation owner. +- Audit: complete rereads of current OS/architecture detection, the accepted marked libc probe, the + signed-manifest selector/version parser, and their purpose tests. Common supported Rocky/RHEL 8 + kernels report releases such as `4.18.0-553.5.1.el8_10.x86_64`; the selector's generic numeric + parser rejects `_` in that suffix and would return `unknown-kernel` despite a valid supported + numeric prefix. This is an implementation-detail gap, not a change to the reviewed 4.18 floor or + the primary plan. +- Finding: add one purpose-named `ssh-relay-linux-kernel-detection` module that consumes an explicit + SSH connection and optional cancellation signal, executes one 15-second POSIX-shell/no-Node + marked `uname -r`, and returns an exact bounded release string or undefined. Add a kernel-specific + parser to the selector that compares only the authenticated probe's strict numeric prefix while + admitting the bounded real-world suffix alphabet `[0-9A-Za-z._+~-]`; keep the generic libc/macOS/ + PowerShell version grammar unchanged. +- Safety contract: only one complete marker-qualified line counts; arbitrary or marker-concatenated + startup noise, duplicate/unterminated markers, whitespace/control characters, oversized releases, + missing commands, and unparseable numeric prefixes yield unknown. Ordinary execution failure is + compatibility evidence; `AbortError` propagates. The parser must accept exact-minimum Rocky/RHEL, + Ubuntu, and Alpine suffixes but reject whitespace, slash, colon, and arbitrary punctuation. +- Required RED: missing purpose module plus a selector assertion showing a supported Rocky-style + `4.18.0-...el8_10...` release currently returns `unknown-kernel`; then marked valid/noisy/ + malformed/bounded/unavailable/cancelled probe cases, kernel-suffix accept/reject boundaries, + existing selector regressions, and native-workflow inclusion. +- Deliberately absent: libc composition, libstdc++/GLIBCXX, macOS/Windows/translation evidence, + Electron/startup/product callers, live SSH or distro cells, cache acquisition, transfer/install, + settings, fallback, tuple enablement, release publication, and defaults. Legacy remains the sole + production path and the primary HTML plan is unchanged. + +### E-M5-LINUX-KERNEL-DETECTION-LOCAL-RED-001 — kernel probe and Rocky suffix boundaries fail first + +- Date/owner: 2026-07-15, Codex implementation owner. +- Source: uncommitted RED tests atop exact accepted evidence head `54d523a81`. +- Command: `pnpm vitest run src/main/ssh/ssh-relay-linux-kernel-detection.test.ts src/main/ssh/ssh-relay-artifact-selector.test.ts`. +- Expected result: FAIL, 2 failed files; the purpose suite has zero collected tests because + `ssh-relay-linux-kernel-detection` does not exist, and the selector has 24 passed / 3 failed. + Supported `4.18.0-553.5.1.el8_10.x86_64` returns legacy/`unknown-kernel`; the equivalent + below-floor `4.17.99-553.5.1.el8_10.x86_64` also returns `unknown-kernel` instead of + `kernel-too-old`. +- Environment: local macOS arm64 worktree; pnpm 10.24.0 under Node 26.0.0 emits the repository's + expected Node-24 engine warning. Exact Node 24 and all-six native execution remain required after + local green. +- Boundary: this is failure-before-implementation evidence only. It authorizes only the audited + disconnected probe, selector parser, purpose tests, and native workflow inclusion; no production + caller or behavior is connected. + +### E-M5-LINUX-KERNEL-DETECTION-LOCAL-001 — disconnected marked kernel probe and strict selector + +- Date/owner: 2026-07-15, Codex implementation owner. +- Implementation: `ssh-relay-linux-kernel-detection.ts` performs one 15-second cancellable POSIX + command, brackets the optional `uname -r` result with exact Orca markers, and returns only one + complete, unique, strict release or undefined. Startup noise outside the markers is ignored; + empty/multi-line/duplicate/unterminated/marker-concatenated evidence, releases above 256 + characters, invalid numeric prefixes, whitespace/control characters, slash, colon, and arbitrary + punctuation are rejected. Ordinary execution failure returns undefined and `AbortError` + propagates. The selector's kernel-only parser admits the audited distro suffix alphabet after a + numeric kernel prefix and compares the prefix to the unchanged reviewed 4.18 floor. The generic + libc/macOS/PowerShell grammar remains strict, with direct underscore-regression assertions. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-linux-kernel-detection.test.ts` — PASS, 1 file / 19 tests in + 209 ms Vitest, 1.08 s wall, 132,005,888-byte maximum RSS. Rocky/RHEL, Ubuntu, and Alpine releases; + outside noise; empty/multiple/duplicate/unterminated/concatenated/oversized/malformed evidence; + invalid suffixes; missing command; cancellation; one signal-qualified 15-second exec; marked + `uname -r`; and no Node/npm/Python/Perl/archive/checksum dependency all pass. +- Focused plus native-workflow oracle: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-linux-kernel-detection.test.ts +src/main/ssh/ssh-relay-artifact-selector.test.ts +src/main/ssh/ssh-remote-platform-detection.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 4 files / 60 tests in 3.32 s Vitest, + 5.26 s wall, 132,300,800-byte maximum RSS while run beside independent typecheck/format gates. + Both POSIX and PowerShell native commands now require the purpose suite exactly once. Supported + Rocky/RHEL, Ubuntu, and Alpine suffixes select; a Rocky release below 4.18 reports + `kernel-too-old`; invalid suffixes report `unknown-kernel`; libc, macOS, and PowerShell versions + do not inherit the kernel grammar. +- Broader relay command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS, + 34 files / 394 tests; 3 declared full-size files/tests skipped; 15.00 s Vitest, 16.38 s wall, + 246,890,496-byte maximum RSS. One preceding diagnostic invocation quoted the shell glob, selected + zero files, and exited 1; the exact checklist command above was then run unquoted and passed. +- Release-script command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 --silent=true +config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 18.76 s Vitest, + 20.26 s wall, 190,480,384-byte maximum RSS. +- Repository gates: `pnpm run typecheck`, targeted `oxlint`, targeted `oxfmt --check`, `pnpm lint`, + and `pnpm run check:max-lines-ratchet` pass. Full lint includes switch exhaustiveness, 41 + reliability gates, no new max-lines bypass, bundled skill-guide verification, 9,837 localization + references and locale parity, and zero localization coverage candidates; existing unrelated lint + warnings remain warnings. `git diff --check` passes and the protected Node/npm resolver pair has + zero diff. Local Node 26.0.0 emits only the expected Node-24 engine warning; exact Node 24 proof + remains the native CI gate. +- Boundary/residual gaps: the new detector is imported only by its purpose tests. This is contract + evidence, not live SSH/GNU/BusyBox/Rocky/Ubuntu/Alpine execution. It does not compose libc, + libstdc++/GLIBCXX, macOS/Windows/translation evidence and adds no Electron/startup/product caller, + transfer/install, mode, setting, fallback, tuple, publication, or default behavior. Legacy remains + the sole production path. Require exact-head all-six native proof before advancing. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -15135,10 +15231,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Audit the next smallest disconnected host-evidence slice after exact-head acceptance of Linux libc -detection. Do not compose full host evidence, provision production secrets/environments, add an -Electron/startup consumer, embed unreviewed keys or manifest bytes, add SSH transfer/install, mode -wiring, fallback, tuple enablement, release publication, or default behavior. +Commit and push the locally accepted disconnected Linux kernel package, then require exact-head +Node 24 proof on all six native clients plus adjacent PR, Golden E2E, and computer-use checks before +auditing another host-evidence slice. Do not compose full host evidence, provision production +secrets/environments, add an Electron/startup consumer, embed unreviewed keys or manifest bytes, add +SSH transfer/install, mode wiring, fallback, tuple enablement, release publication, or default +behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. diff --git a/src/main/ssh/ssh-relay-artifact-selector.test.ts b/src/main/ssh/ssh-relay-artifact-selector.test.ts index f702a612a60..37d127ba1b5 100644 --- a/src/main/ssh/ssh-relay-artifact-selector.test.ts +++ b/src/main/ssh/ssh-relay-artifact-selector.test.ts @@ -102,6 +102,57 @@ describe('SSH relay artifact selector', () => { ).toBe('selected') }) + it.each(['4.18.0-553.5.1.el8_10.x86_64', '5.15.0-107-generic', '6.6.15-0-lts'])( + 'accepts a supported Linux distro kernel release %s', + (kernelVersion) => { + expect( + selectSshRelayArtifact(verifiedManifest(), { + ...compatibleLinuxHost, + kernelVersion + }).kind + ).toBe('selected') + } + ) + + it.each([ + ['4.18.0-553.5.1.el8_10.x86_64', 'selected'], + ['4.17.99-553.5.1.el8_10.x86_64', 'kernel-too-old'], + ['4.18.0 bad', 'unknown-kernel'], + ['4.18.0/bad', 'unknown-kernel'], + ['4.18.0:bad', 'unknown-kernel'], + ['4.18.0@bad', 'unknown-kernel'] + ] as const)('classifies the kernel suffix boundary for %s', (kernelVersion, expected) => { + const result = selectSshRelayArtifact(verifiedManifest(), { + ...compatibleLinuxHost, + kernelVersion + }) + + expect(result.kind === 'selected' ? result.kind : result.reason).toBe(expected) + }) + + it('keeps non-kernel version grammars strict', () => { + expect( + selectSshRelayArtifact(verifiedManifest(), { + ...compatibleLinuxHost, + libc: { family: 'glibc', version: '2.28_bad' } + }) + ).toEqual({ kind: 'legacy', reason: 'unknown-libc' }) + expect( + selectSshRelayArtifact(verifiedManifest(createSshRelayDarwinArtifactTestManifest()), { + os: 'darwin', + architecture: 'arm64', + processTranslated: false, + version: '13.5_bad' + }) + ).toEqual({ kind: 'legacy', reason: 'unknown-os-version' }) + expect( + selectSshRelayArtifact(verifiedManifest(windowsManifest()), { + ...compatibleWindowsHost, + powerShellVersion: '5.1_bad' + }) + ).toEqual({ kind: 'legacy', reason: 'unknown-powershell' }) + }) + it('selects the detected libc family when both Linux variants exist', () => { const manifest = createSshRelayArtifactTestManifest() const musl = structuredClone(manifest.tuples[0]) diff --git a/src/main/ssh/ssh-relay-artifact-selector.ts b/src/main/ssh/ssh-relay-artifact-selector.ts index c8088f21891..5c0b4d3395d 100644 --- a/src/main/ssh/ssh-relay-artifact-selector.ts +++ b/src/main/ssh/ssh-relay-artifact-selector.ts @@ -3,6 +3,8 @@ import { sshRelayRuntimeDownloadUrl } from './ssh-relay-release-asset' type VerifiedSshRelayRuntimeTuple = VerifiedSshRelayArtifactManifest['tuples'][number] +const LINUX_KERNEL_RELEASE_MAX_CHARS = 256 + type SshRelayHostBase = { architecture: 'x64' | 'arm64' processTranslated: boolean @@ -100,6 +102,24 @@ function parseNumericVersion(value: string | undefined): number[] | null { return components.every(Number.isSafeInteger) ? components : null } +function parseLinuxKernelVersion(value: string | undefined): number[] | null { + if (!value || value.length > LINUX_KERNEL_RELEASE_MAX_CHARS) { + return null + } + // Why: real distro kernels include `_`, `+`, and `~`; other version fields keep the stricter + // generic grammar so this compatibility exception cannot weaken unrelated host evidence. + const match = /^(\d+(?:\.\d+){1,3})(?:-[0-9A-Za-z._+~-]+)?$/.exec(value) + if (!match) { + return null + } + const components = match[1].split('.').map(Number) + return components.every(Number.isSafeInteger) ? components : null +} + +export function isSshRelayLinuxKernelRelease(value: string | undefined): value is string { + return parseLinuxKernelVersion(value) !== null +} + function parseOpenSshVersion(value: string | undefined): number[] | null { if (!value) { return null @@ -144,7 +164,11 @@ function selectLinux( if (tuple.compatibility.kind !== 'linux') { return { kind: 'legacy', reason: 'tuple-inconsistent' } } - const kernel = meetsVersion(host.kernelVersion, tuple.compatibility.minimumKernelVersion) + const kernel = meetsVersion( + host.kernelVersion, + tuple.compatibility.minimumKernelVersion, + parseLinuxKernelVersion + ) if (kernel === null) { return { kind: 'legacy', reason: 'unknown-kernel' } } diff --git a/src/main/ssh/ssh-relay-linux-kernel-detection.test.ts b/src/main/ssh/ssh-relay-linux-kernel-detection.test.ts new file mode 100644 index 00000000000..f46cf80596c --- /dev/null +++ b/src/main/ssh/ssh-relay-linux-kernel-detection.test.ts @@ -0,0 +1,87 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { SshConnection } from './ssh-connection' + +const execCommandMock = vi.hoisted(() => vi.fn()) + +vi.mock('./ssh-relay-deploy-helpers', () => ({ + execCommand: execCommandMock +})) + +const { detectSshRelayLinuxKernelRelease } = await import('./ssh-relay-linux-kernel-detection') + +const conn = {} as SshConnection +const marker = '__ORCA_SSH_RELAY_KERNEL__' + +function segment(...lines: string[]): string { + return [`${marker} BEGIN`, ...lines, `${marker} END`].join('\n') +} + +describe('detectSshRelayLinuxKernelRelease', () => { + beforeEach(() => { + execCommandMock.mockReset() + }) + + it.each(['4.18.0-553.5.1.el8_10.x86_64', '5.15.0-107-generic', '6.6.15-0-lts'])( + 'returns the exact marked distro kernel release %s', + async (release) => { + execCommandMock.mockResolvedValueOnce( + ['Last login: ignored', segment(release), 'post-command noise'].join('\n') + ) + + await expect(detectSshRelayLinuxKernelRelease(conn)).resolves.toBe(release) + } + ) + + it.each([ + ['', 'empty segment'], + ['6.8.0\nsecond-line', 'multiple marked lines'], + [`${marker} BEGIN\n6.8.0`, 'unterminated segment'], + [segment('x'.repeat(257)), 'oversized release'], + [segment('release-6.8.0'), 'malformed numeric prefix'], + [segment('6.8.0 generic'), 'suffix whitespace'], + [segment('6.8.0/generic'), 'suffix slash'], + [segment('6.8.0:generic'), 'suffix colon'], + [segment('6.8.0@generic'), 'unsupported suffix punctuation'], + [segment('6.8.0\tgeneric'), 'control whitespace'], + ['6.8.0', 'unmarked evidence'], + [[segment('6.8.0'), segment('6.8.0')].join('\n'), 'duplicate complete segments'], + [`startup${marker} BEGIN\n6.8.0\n${marker} END`, 'marker concatenated to startup noise'] + ])('returns unknown for %s', async (output) => { + execCommandMock.mockResolvedValueOnce(output) + + await expect(detectSshRelayLinuxKernelRelease(conn)).resolves.toBeUndefined() + }) + + it('classifies an unavailable probe as unknown without inventing fallback policy', async () => { + execCommandMock.mockRejectedValueOnce(new Error('remote command unavailable')) + + await expect(detectSshRelayLinuxKernelRelease(conn)).resolves.toBeUndefined() + }) + + it('propagates cancellation and passes the signal into the single bounded probe', async () => { + const signal = new AbortController().signal + const abortError = Object.assign(new Error('cancelled'), { name: 'AbortError' }) + execCommandMock.mockRejectedValueOnce(abortError) + + await expect(detectSshRelayLinuxKernelRelease(conn, { signal })).rejects.toBe(abortError) + expect(execCommandMock).toHaveBeenCalledTimes(1) + expect(execCommandMock).toHaveBeenCalledWith(conn, expect.any(String), { + signal, + timeoutMs: 15_000 + }) + }) + + it('constructs one marked POSIX-shell uname probe without runtime dependencies', async () => { + execCommandMock.mockResolvedValueOnce(segment('6.8.0')) + + await detectSshRelayLinuxKernelRelease(conn) + + const command = execCommandMock.mock.calls[0]?.[1] ?? '' + expect(command).toContain('command -v uname') + expect(command).toContain('uname -r') + expect(command.match(/uname -r/gu)).toHaveLength(1) + expect(command).toContain(`${marker} BEGIN`) + expect(command).toContain(`${marker} END`) + expect(command).not.toMatch(/\b(?:node|npm|python|perl|tar|sha256sum|shasum)\b/u) + }) +}) diff --git a/src/main/ssh/ssh-relay-linux-kernel-detection.ts b/src/main/ssh/ssh-relay-linux-kernel-detection.ts new file mode 100644 index 00000000000..a3a8e153262 --- /dev/null +++ b/src/main/ssh/ssh-relay-linux-kernel-detection.ts @@ -0,0 +1,86 @@ +import { + getProcessOutputFields, + iterateProcessOutputLines +} from '../../shared/process-output-field-scanner' +import type { SshConnection } from './ssh-connection' +import { isSshRelayLinuxKernelRelease } from './ssh-relay-artifact-selector' +import { execCommand } from './ssh-relay-deploy-helpers' + +const PROBE_MARKER = '__ORCA_SSH_RELAY_KERNEL__' +const PROBE_TIMEOUT_MS = 15_000 + +const PROBE_COMMAND = [ + `printf '\n%s\n' '${PROBE_MARKER} BEGIN'`, + 'if command -v uname >/dev/null 2>&1; then uname -r 2>&1 || :; fi', + `printf '%s\n' '${PROBE_MARKER} END'` +].join('\n') + +function parseMarker(line: string): 'BEGIN' | 'END' | null { + const fields = getProcessOutputFields(line, 3) + if (fields.length !== 2 || fields[0] !== PROBE_MARKER) { + return null + } + return fields[1] === 'BEGIN' || fields[1] === 'END' ? fields[1] : null +} + +function parseKernelRelease(output: string): string | undefined { + let activeLines: string[] | null = null + let release: string | undefined + let invalid = false + + for (const line of iterateProcessOutputLines(output)) { + const marker = parseMarker(line) + if (marker === 'BEGIN') { + if (activeLines || release !== undefined) { + invalid = true + } + activeLines = [] + continue + } + if (marker === 'END') { + if (!activeLines || activeLines.length !== 1 || release !== undefined) { + invalid = true + activeLines = null + continue + } + release = activeLines[0] + activeLines = null + continue + } + if (activeLines) { + if (activeLines.length === 1) { + invalid = true + } else { + activeLines.push(line) + } + } + } + + if (invalid || activeLines || !isSshRelayLinuxKernelRelease(release)) { + return undefined + } + return release +} + +function isAbortError(error: unknown): boolean { + return error instanceof Error && error.name === 'AbortError' +} + +export async function detectSshRelayLinuxKernelRelease( + conn: SshConnection, + { signal }: { signal?: AbortSignal } = {} +): Promise { + try { + const output = await execCommand(conn, PROBE_COMMAND, { + signal, + timeoutMs: PROBE_TIMEOUT_MS + }) + return parseKernelRelease(output) + } catch (error) { + // Why: cancellation must settle the caller's work instead of becoming compatibility evidence. + if (isAbortError(error)) { + throw error + } + return undefined + } +} From e6383cbcdfbaffe5d645e52b9a6c7e4dbae8c938 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 04:31:21 -0400 Subject: [PATCH 193/267] docs: record relay kernel detection CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 16 +++-- ...github-release-implementation-checklist.md | 68 ++++++++++++++++--- 2 files changed, 72 insertions(+), 12 deletions(-) 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 6b9e1adb377..e94125a6556 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 @@ -9,10 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — require exact-head -all-six native CI for the disconnected marked Linux kernel probe and kernel-only distro-suffix -parser accepted locally by `E-M5-LINUX-KERNEL-DETECTION-LOCAL-001`, 2026-07-15, Codex implementation -owner.** +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — audit the next smallest +remaining disconnected host-evidence boundary after exact-head acceptance of Linux kernel +detection, 2026-07-15, Codex implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -143,6 +142,15 @@ oversized, noisy, unavailable, or invalid evidence returns unknown and cancellat Libc, macOS, and PowerShell grammars remain strict. This is contract evidence, not live SSH/distro proof; full host composition and every production/default path remain absent. Exact-head all-six native proof is next. +`E-M5-LINUX-KERNEL-DETECTION-CI-001` closes implementation commit `d1eb0eb63`: all six primary +native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and +computer-use pass. Every POSIX client passes 80 files / 561 tests and every Windows client passes 81 +files / 552 tests with 13 declared skips, including the new 19-case kernel suite and strict selector +cases. Full-size extraction/cache metrics stay inside budget. Windows arm64 completes exact-byte +Node/PTY/watcher smoke, then retains only the declared hosted build-26200 rejection against required +26100; this is the aggregate workflow's sole failure. Real SSH/distro behavior, remaining host +evidence, composition, and every production/default path remain open; legacy remains the sole +production path. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 51d5d2e36b2..ecc0061b391 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: require exact-head all-six native CI for the disconnected marked Linux kernel probe and kernel-only distro-suffix parser accepted locally by E-M5-LINUX-KERNEL-DETECTION-LOCAL-001**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **LOCAL CLOSED / CI NEXT — 2026-07-15, Codex implementation owner** — `E-M5-LINUX-KERNEL-DETECTION-LOCAL-001` passes the 19-case purpose contract, selector and native-workflow gates, 394 non-full-size relay cases, 280 release-script cases, typecheck, lint, format, reliability, localization, max-lines, diff, and protected-resolver gates. The reviewed Linux 4.18 floor is unchanged. No host-evidence composer, product caller, setting, transfer/install, fallback classifier, tuple enablement, release publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the next smallest remaining disconnected host-evidence boundary after exact-head acceptance of Linux kernel detection**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **CI CLOSED / NEXT AUDIT — 2026-07-15, Codex implementation owner** — `E-M5-LINUX-KERNEL-DETECTION-CI-001` closes implementation commit `d1eb0eb63`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. Windows arm64 executes the full runtime smoke and retains only the declared hosted build-26200 rejection against required 26100. Every native client runs the new 19-case kernel contract and strict selector cases. This remains contract evidence, not live SSH/distro evidence; no host-evidence composer, product caller, setting, transfer/install, fallback classifier, tuple enablement, release publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -15173,6 +15173,60 @@ config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 1 transfer/install, mode, setting, fallback, tuple, publication, or default behavior. Legacy remains the sole production path. Require exact-head all-six native proof before advancing. +### E-M5-LINUX-KERNEL-DETECTION-CI-001 — all-six native marked kernel contract + +- Date/owner: 2026-07-15, Codex implementation owner. +- Exact source/run: implementation commit `d1eb0eb63e56255d151f2f263e47fa420b970875`, artifact run + [29482651735](https://github.com/stablyai/orca/actions/runs/29482651735). All six primary jobs pass + under Node 24.18.0 with that exact source: Linux x64 job + [87569723013](https://github.com/stablyai/orca/actions/runs/29482651735/job/87569723013) + (`ubuntu-24.04`, `ubuntu24` image `20260714.240`, x64); Linux arm64 job + [87569723055](https://github.com/stablyai/orca/actions/runs/29482651735/job/87569723055) + (`ubuntu-24.04-arm`, `ubuntu24-arm64` image `20260714.61`, native arm64); macOS x64 job + [87569723110](https://github.com/stablyai/orca/actions/runs/29482651735/job/87569723110) + (`macos-15-intel`, `macos15` image `20260629.0276`, x64); macOS arm64 job + [87569723016](https://github.com/stablyai/orca/actions/runs/29482651735/job/87569723016) + (`macos-15`, `macos15` image `20260706.0213`, native arm64); Windows x64 job + [87569722968](https://github.com/stablyai/orca/actions/runs/29482651735/job/87569722968) + (`windows-2022`, `win22` image `20260714.244`, x64); and Windows arm64 job + [87569722974](https://github.com/stablyai/orca/actions/runs/29482651735/job/87569722974) + (`windows-11-arm`, `win11-arm64` image `20260706.102`, native arm64). +- Contract result: every POSIX client passes 80 files / 561 tests. Linux x64/arm64 complete in + 13.35/11.69 s and macOS x64/arm64 in 59.66/18.45 s. Every Windows client passes 81 files / 552 + tests with the 13 declared platform/full-size skips, in 27.01/25.25 s for x64/arm64. The new + 19-case kernel suite and strict selector cases are required by both native workflow families and + therefore run on every client architecture under Node 24; all existing selector, cache, release, + runtime, and workflow contracts remain green. +- Full-size result: extraction spans 1,304.99–4,880.08 ms with 34,021,376–48,541,696 bytes + incremental RSS. Cold immutable-cache publication spans 1,774.51–5,929.28 ms with + 40,210,432–60,735,488 bytes; warm verified lookup 196.12–1,197.59 ms with 0–6,352,896 bytes; + active retention 11.17–50.44 ms with 0–991,232 bytes; and eviction 25.97–99.14 ms with + 0–1,052,672 bytes while reclaiming each exact 118,418,686–161,259,305-byte entry. All remain + within recorded budgets; this kernel-only package adds no full-size latency or memory path. +- Supplemental/baseline result: oldest-userland Linux x64 job + [87570980242](https://github.com/stablyai/orca/actions/runs/29482651735/job/87570980242) + (`ubuntu-24.04`, image `20260714.240`) and Linux arm64 job + [87570980244](https://github.com/stablyai/orca/actions/runs/29482651735/job/87570980244) + (`ubuntu-24.04-arm`, image `20260714.61`) pass. Windows x64 baseline job + [87571636418](https://github.com/stablyai/orca/actions/runs/29482651735/job/87571636418) + (`windows-2022`, image `20260714.244`) passes on observed build 20348. Windows arm64 baseline job + [87571636619](https://github.com/stablyai/orca/actions/runs/29482651735/job/87571636619) + (`windows-11-arm`, image `20260714.109`) verifies the exact bytes and completes Node 24, PTY, and + watcher smoke in 8,038.87 ms, then fails closed only because the observed OS/kernel build is + 26200 while the immutable contract requires 26100; platform and architecture checks pass. That + declared hosted-runner floor gap is the sole reason the aggregate workflow conclusion is failure. +- Adjacent exact-head result: PR Checks + [29482650752](https://github.com/stablyai/orca/actions/runs/29482650752), Golden E2E + [29482651866](https://github.com/stablyai/orca/actions/runs/29482651866), and computer-use + [29482650759](https://github.com/stablyai/orca/actions/runs/29482650759) pass, including unpacked + app, packaged CLI, Ubuntu 22.04, Windows native, Linux E2E, and macOS E2E gates. +- Acceptance/boundary: this accepts only the disconnected marked/no-Node Linux kernel capability + and kernel-specific selector grammar. It does not prove a real SSH server, GNU/BusyBox/Rocky/ + Ubuntu/Alpine remote behavior, localization, `MaxSessions=1`, remote output/channel-size limits, + libc composition, libstdc++/GLIBCXX evidence, macOS/Windows/process-translation evidence, host + composition, Electron/startup, transfer/install, settings, fallback, tuple enablement, + publication, or defaults. Legacy remains the sole production path; no tuple is enabled. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -15231,12 +15285,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the locally accepted disconnected Linux kernel package, then require exact-head -Node 24 proof on all six native clients plus adjacent PR, Golden E2E, and computer-use checks before -auditing another host-evidence slice. Do not compose full host evidence, provision production -secrets/environments, add an Electron/startup consumer, embed unreviewed keys or manifest bytes, add -SSH transfer/install, mode wiring, fallback, tuple enablement, release publication, or default -behavior. +Audit the next smallest disconnected host-evidence slice after exact-head acceptance of Linux +kernel detection. Do not compose full host evidence, provision production secrets/environments, add +an Electron/startup consumer, embed unreviewed keys or manifest bytes, add SSH transfer/install, +mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. From bcda04389590e30bec2ac7f8e15516f937a2019f Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 04:41:43 -0400 Subject: [PATCH 194/267] feat: detect remote macOS version Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 25 ++++- ...github-release-implementation-checklist.md | 106 +++++++++++++++++- ...ssh-relay-darwin-version-detection.test.ts | 88 +++++++++++++++ .../ssh/ssh-relay-darwin-version-detection.ts | 93 +++++++++++++++ 6 files changed, 306 insertions(+), 9 deletions(-) create mode 100644 src/main/ssh/ssh-relay-darwin-version-detection.test.ts create mode 100644 src/main/ssh/ssh-relay-darwin-version-detection.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index d3d4ec16ec2..e2258a3a36b 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -264,6 +264,7 @@ jobs: src/main/ssh/ssh-relay-artifact-selector.test.ts \ src/main/ssh/ssh-relay-libc-detection.test.ts \ src/main/ssh/ssh-relay-linux-kernel-detection.test.ts \ + src/main/ssh/ssh-relay-darwin-version-detection.test.ts \ src/main/ssh/ssh-relay-artifact-download.test.ts \ src/main/ssh/ssh-relay-artifact-extraction.test.ts \ src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts \ @@ -811,6 +812,7 @@ jobs: src/main/ssh/ssh-relay-artifact-selector.test.ts ` src/main/ssh/ssh-relay-libc-detection.test.ts ` src/main/ssh/ssh-relay-linux-kernel-detection.test.ts ` + src/main/ssh/ssh-relay-darwin-version-detection.test.ts ` src/main/ssh/ssh-relay-artifact-download.test.ts ` src/main/ssh/ssh-relay-artifact-extraction.test.ts ` src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 3ada4e78f36..4b8aa436ba8 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -122,6 +122,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-artifact-selector.test.ts', 'ssh-relay-libc-detection.test.ts', 'ssh-relay-linux-kernel-detection.test.ts', + 'ssh-relay-darwin-version-detection.test.ts', 'ssh-relay-artifact-download.test.ts', 'ssh-relay-artifact-extraction.test.ts', 'ssh-relay-artifact-cache-lock-release.test.ts', 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 e94125a6556..0eb79abaed2 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 @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — audit the next smallest -remaining disconnected host-evidence boundary after exact-head acceptance of Linux kernel -detection, 2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — exact-head native CI for +the locally green disconnected marked macOS product-version probe, 2026-07-15, Codex implementation +owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -151,6 +151,25 @@ Node/PTY/watcher smoke, then retains only the declared hosted build-26200 reject 26100; this is the aggregate workflow's sole failure. Real SSH/distro behavior, remaining host evidence, composition, and every production/default path remain open; legacy remains the sole production path. +`E-M5-DARWIN-VERSION-DETECTION-AUDIT-001` fixes the next smallest package: one disconnected, +15-second cancellable POSIX-shell/no-Node marked `sw_vers -productVersion` probe. Only one complete +strict two-to-four-component numeric version counts; missing, noisy, incomplete, duplicate, +malformed, suffixed, or oversized evidence returns unknown and cancellation propagates. The +reviewed macOS 13.5 floor and selector grammar stay unchanged. Rosetta/process translation, Linux +libstdc++/GLIBCXX, Windows evidence, full host composition, product callers, live SSH, transfer/ +install, settings, fallback, tuple enablement, publication, and defaults remain absent. +`E-M5-DARWIN-VERSION-DETECTION-LOCAL-RED-001` records the expected missing-module failure before +implementation: the purpose suite has one failed file and zero collected tests because the audited +detection module does not exist. The native-workflow oracle now also requires that purpose suite +exactly once in both POSIX and PowerShell runner commands and remains red until they are wired. +Protected resolver files have zero diff; no production caller or default behavior is connected. +`E-M5-DARWIN-VERSION-DETECTION-LOCAL-001` is green: 21/21 purpose tests, 62/62 focused+workflow +tests, 415/415 non-full-size relay tests with three declared skips, 280/280 release-script tests, +typecheck, lint, format, max-lines, diff, and protected-resolver gates pass. One 15-second +cancellable marked POSIX command returns only a unique bounded numeric `sw_vers -productVersion`; +malformed, noisy, duplicate, unavailable, or oversized evidence returns unknown and cancellation +propagates. Both native runner families require the suite. Exact Node 24/all-six native proof is +next; real SSH/macOS, Rosetta, composition, and all production/default behavior remain absent. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 ecc0061b391..cd65688bf4c 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the next smallest remaining disconnected host-evidence boundary after exact-head acceptance of Linux kernel detection**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **CI CLOSED / NEXT AUDIT — 2026-07-15, Codex implementation owner** — `E-M5-LINUX-KERNEL-DETECTION-CI-001` closes implementation commit `d1eb0eb63`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. Windows arm64 executes the full runtime smoke and retains only the declared hosted build-26200 rejection against required 26100. Every native client runs the new 19-case kernel contract and strict selector cases. This remains contract evidence, not live SSH/distro evidence; no host-evidence composer, product caller, setting, transfer/install, fallback classifier, tuple enablement, release publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: implement the disconnected marked macOS product-version probe fixed by E-M5-DARWIN-VERSION-DETECTION-AUDIT-001**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **LOCAL GREEN / EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M5-DARWIN-VERSION-DETECTION-LOCAL-001` passes the 21-case purpose contract, 62 focused/workflow tests, 415 relay tests with three declared skips, 280 release-script tests, typecheck, lint, format, max-lines, diff, and protected-resolver gates. Both native workflow families require the suite, but exact Node 24/all-six native execution remains open until the implementation commit is pushed. Rosetta/process-translation detection remains a separate uncomposed gate. No host-evidence composer, product caller, setting, transfer/install, fallback classifier, tuple enablement, release publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -15227,6 +15227,98 @@ config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 1 composition, Electron/startup, transfer/install, settings, fallback, tuple enablement, publication, or defaults. Legacy remains the sole production path; no tuple is enabled. +### E-M5-DARWIN-VERSION-DETECTION-AUDIT-001 — disconnected marked macOS version boundary + +- Date/owner: 2026-07-15, Codex implementation owner. +- Audit: complete rereads of current OS/architecture detection, the accepted marked libc and Linux + kernel probes, the signed-manifest selector/version grammar, baseline build evidence, and their + purpose tests. Desktop SSH code has no `sw_vers` caller; only target-native release baseline CI + reads macOS product version. The selector accepts explicit `SshRelayDarwinHostEvidence.version` + against the reviewed 13.5 floor, so the smallest missing acquisition boundary is one disconnected + product-version probe. Linux libstdc++/GLIBCXX evidence requires a separate external-tool and + ambiguity audit; Windows bootstrap evidence and Rosetta translation are also larger independent + slices. +- Finding: add one purpose-named `ssh-relay-darwin-version-detection` module that consumes an + explicit SSH connection and optional cancellation signal, executes one 15-second POSIX-shell/ + no-Node marked `sw_vers -productVersion`, and returns an exact bounded two-to-four-component + numeric version or undefined. Do not change the selector grammar or reviewed macOS 13.5 floor. +- Safety contract: only one complete marker-qualified line counts; arbitrary or marker-concatenated + startup noise, empty/multi-line/duplicate/unterminated segments, whitespace/control characters, + suffixes, oversized values, missing commands, and malformed versions yield unknown. Ordinary + execution failure is availability evidence; `AbortError` propagates. The probe uses no Node, npm, + Python, Perl, archive, checksum, or localization-sensitive parsing. +- Required RED: missing purpose module, then exact 13.5/10.15.7/15.x versions, startup noise, + concatenated/duplicate/unterminated/multi-line/oversized/malformed rejection, unavailable command, + cancellation, exact signal/15-second exec, no-Node command construction, and both native-workflow + families requiring the suite. +- Deliberately absent: Rosetta/process-translation detection, Linux libstdc++/GLIBCXX, Windows + OpenSSH/PowerShell/.NET/build evidence, host-evidence composition, Electron/startup/product + callers, live SSH/macOS cells, cache acquisition, transfer/install, settings, fallback, tuple + enablement, release publication, and defaults. Legacy remains the sole production path and the + primary HTML plan is unchanged. + +### E-M5-DARWIN-VERSION-DETECTION-LOCAL-RED-001 — macOS version probe fails first + +- Date/owner: 2026-07-15, Codex implementation owner. +- Source: uncommitted RED test atop exact accepted evidence head `e6383cbcd`; protected resolver + files have zero diff and `git diff --check` passes. +- Command: `pnpm vitest run src/main/ssh/ssh-relay-darwin-version-detection.test.ts`. +- Expected result: FAIL, 1 failed suite / zero collected tests because + `ssh-relay-darwin-version-detection` does not exist. Duration: 288 ms Vitest. The repository's + expected Node-24 engine warning is present under local Node 26.0.0 / pnpm 10.24.0. +- Native-workflow RED command: `pnpm vitest run +config/scripts/ssh-relay-runtime-workflow.test.mjs` — expected FAIL, 1 file / 6 passed / 1 failed + in 268 ms. The workflow oracle requires `ssh-relay-darwin-version-detection.test.ts` exactly once + in each POSIX and PowerShell native command family; the source split has length 1 rather than the + required 3 until both commands are wired. +- Boundary: this is failure-before-implementation evidence only. It authorizes only the audited + disconnected probe, purpose tests, and native-workflow inclusion; no production caller or + behavior is connected. + +### E-M5-DARWIN-VERSION-DETECTION-LOCAL-001 — disconnected marked macOS version probe + +- Date/owner: 2026-07-15, Codex implementation owner. +- Implementation: `ssh-relay-darwin-version-detection.ts` executes one 15-second cancellable + POSIX-shell command, guards `sw_vers -productVersion`, and brackets its result with exact Orca + markers. It returns only one complete unique two-to-four-component numeric version of at most 64 + characters. Startup noise outside markers is ignored; empty, multi-line, duplicate, + unterminated, marker-concatenated, suffixed, whitespace/control, slash, colon, oversized, or + malformed evidence returns undefined. Ordinary execution failure returns undefined and + `AbortError` propagates. The command has no Node, npm, Python, Perl, tar, or checksum dependency. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-darwin-version-detection.test.ts` — PASS, 1 file / 21 tests + in 265 ms Vitest, 1.35 s wall, 132,022,272-byte maximum RSS, zero swaps. Valid 13.5, 10.15.7, + 15.4.1, and 15.4.1.2 values; noisy/missing/malformed/bounded evidence; unavailable command; + cancellation; exact signal/15-second exec; guarded marked command; and forbidden-runtime absence + all pass. +- Focused plus native-workflow oracle: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-darwin-version-detection.test.ts +src/main/ssh/ssh-relay-artifact-selector.test.ts +src/main/ssh/ssh-remote-platform-detection.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 4 files / 62 tests in 1.67 s Vitest, + 2.86 s wall, 137,003,008-byte maximum RSS, zero swaps. Both POSIX and PowerShell native commands + require the purpose suite exactly once; existing platform and reviewed selector behavior passes. +- Broader relay command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS, 35 files passed / 3 declared + skipped, 415 tests passed / 3 declared skipped in 20.76 s Vitest, 23.12 s wall, 259,325,952-byte + maximum RSS, zero swaps. The expected stale-lock diagnostic and Node 26 `module.register()` + deprecation warning are unchanged. +- Release-script command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 --silent=true +config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 19.48 s Vitest, + 21.47 s wall, 189,300,736-byte maximum RSS, zero swaps. +- Static gates: `pnpm typecheck`; targeted `pnpm exec oxlint`; targeted `pnpm exec oxfmt --check`; + `pnpm lint`; `pnpm run check:max-lines-ratchet`; and `git diff --check` pass after formatting the + two reported files. Full lint includes switch exhaustiveness, 41 reliability gates, 355 + grandfathered max-lines entries with no new bypass, bundled-skill verification, 9,837 + localization references and locale parity, and zero localization coverage candidates. Its 26 + warnings are in untouched files. Protected resolver files have zero diff. +- Residual gap: local execution uses Node 26.0.0 / pnpm 10.24.0; exact Node 24 on all six native + client runners, both Linux supplements, and Windows baselines remains required at the exact + implementation head. No real SSH/macOS host, Rosetta, full host composition, product caller, + transfer/install, setting, fallback, tuple, publication, or default behavior is proven or added. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -15285,10 +15377,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Audit the next smallest disconnected host-evidence slice after exact-head acceptance of Linux -kernel detection. Do not compose full host evidence, provision production secrets/environments, add -an Electron/startup consumer, embed unreviewed keys or manifest bytes, add SSH transfer/install, -mode wiring, fallback, tuple enablement, release publication, or default behavior. +Commit the locally proven disconnected marked macOS product-version package, push it to the draft +PR, and require exact-head all-six native artifact CI before advancing. Do not compose full host +evidence, add +Rosetta detection, provision production secrets/environments, add an Electron/startup consumer, +embed unreviewed keys or manifest bytes, add SSH transfer/install, mode wiring, fallback, tuple +enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. diff --git a/src/main/ssh/ssh-relay-darwin-version-detection.test.ts b/src/main/ssh/ssh-relay-darwin-version-detection.test.ts new file mode 100644 index 00000000000..9032810c104 --- /dev/null +++ b/src/main/ssh/ssh-relay-darwin-version-detection.test.ts @@ -0,0 +1,88 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { SshConnection } from './ssh-connection' + +const execCommandMock = vi.hoisted(() => vi.fn()) + +vi.mock('./ssh-relay-deploy-helpers', () => ({ + execCommand: execCommandMock +})) + +const { detectSshRelayDarwinVersion } = await import('./ssh-relay-darwin-version-detection') + +const conn = {} as SshConnection +const marker = '__ORCA_SSH_RELAY_DARWIN_VERSION__' + +function segment(...lines: string[]): string { + return [`${marker} BEGIN`, ...lines, `${marker} END`].join('\n') +} + +describe('detectSshRelayDarwinVersion', () => { + beforeEach(() => { + execCommandMock.mockReset() + }) + + it.each(['13.5', '10.15.7', '15.4.1', '15.4.1.2'])( + 'returns the exact marked macOS product version %s', + async (version) => { + execCommandMock.mockResolvedValueOnce( + ['Last login: ignored', segment(version), 'post-command noise'].join('\n') + ) + + await expect(detectSshRelayDarwinVersion(conn)).resolves.toBe(version) + } + ) + + it.each([ + ['', 'empty segment'], + ['15.4\nsecond-line', 'multiple marked lines'], + [`${marker} BEGIN\n15.4`, 'unterminated segment'], + [segment('1'.repeat(65)), 'oversized version'], + [segment('15'), 'missing minor component'], + [segment('15.4.1.2.3'), 'too many components'], + [segment('15.4-beta'), 'version suffix'], + [segment('15.4 beta'), 'version whitespace'], + [segment('15.4\u0000'), 'version control character'], + [segment('15.4/beta'), 'version slash'], + [segment('15.4:beta'), 'version colon'], + ['15.4', 'unmarked evidence'], + [[segment('15.4'), segment('15.4')].join('\n'), 'duplicate complete segments'], + [`startup${marker} BEGIN\n15.4\n${marker} END`, 'marker concatenated to startup noise'] + ])('returns unknown for %s', async (output) => { + execCommandMock.mockResolvedValueOnce(output) + + await expect(detectSshRelayDarwinVersion(conn)).resolves.toBeUndefined() + }) + + it('classifies an unavailable probe as unknown without inventing fallback policy', async () => { + execCommandMock.mockRejectedValueOnce(new Error('remote command unavailable')) + + await expect(detectSshRelayDarwinVersion(conn)).resolves.toBeUndefined() + }) + + it('propagates cancellation and passes the signal into the single bounded probe', async () => { + const signal = new AbortController().signal + const abortError = Object.assign(new Error('cancelled'), { name: 'AbortError' }) + execCommandMock.mockRejectedValueOnce(abortError) + + await expect(detectSshRelayDarwinVersion(conn, { signal })).rejects.toBe(abortError) + expect(execCommandMock).toHaveBeenCalledTimes(1) + expect(execCommandMock).toHaveBeenCalledWith(conn, expect.any(String), { + signal, + timeoutMs: 15_000 + }) + }) + + it('constructs one marked POSIX-shell sw_vers probe without runtime dependencies', async () => { + execCommandMock.mockResolvedValueOnce(segment('15.4')) + + await detectSshRelayDarwinVersion(conn) + + const command = execCommandMock.mock.calls[0]?.[1] ?? '' + expect(command).toContain('command -v sw_vers') + expect(command).toContain('sw_vers -productVersion') + expect(command.match(/sw_vers -productVersion/gu)).toHaveLength(1) + expect(command).toContain(`${marker} BEGIN`) + expect(command).toContain(`${marker} END`) + expect(command).not.toMatch(/\b(?:node|npm|python|perl|tar|sha256sum|shasum)\b/u) + }) +}) diff --git a/src/main/ssh/ssh-relay-darwin-version-detection.ts b/src/main/ssh/ssh-relay-darwin-version-detection.ts new file mode 100644 index 00000000000..92fdc52585f --- /dev/null +++ b/src/main/ssh/ssh-relay-darwin-version-detection.ts @@ -0,0 +1,93 @@ +import { + getProcessOutputFields, + iterateProcessOutputLines +} from '../../shared/process-output-field-scanner' +import type { SshConnection } from './ssh-connection' +import { execCommand } from './ssh-relay-deploy-helpers' + +const PROBE_MARKER = '__ORCA_SSH_RELAY_DARWIN_VERSION__' +const PROBE_TIMEOUT_MS = 15_000 +const VERSION_MAX_CHARS = 64 + +const PROBE_COMMAND = [ + `printf '\n%s\n' '${PROBE_MARKER} BEGIN'`, + 'if command -v sw_vers >/dev/null 2>&1; then sw_vers -productVersion 2>&1 || :; fi', + `printf '%s\n' '${PROBE_MARKER} END'` +].join('\n') + +function parseMarker(line: string): 'BEGIN' | 'END' | null { + const fields = getProcessOutputFields(line, 3) + if (fields.length !== 2 || fields[0] !== PROBE_MARKER) { + return null + } + return fields[1] === 'BEGIN' || fields[1] === 'END' ? fields[1] : null +} + +function isDarwinProductVersion(value: string | undefined): value is string { + if (!value || value.length > VERSION_MAX_CHARS || !/^\d+(?:\.\d+){1,3}$/.test(value)) { + return false + } + return value.split('.').map(Number).every(Number.isSafeInteger) +} + +function parseDarwinProductVersion(output: string): string | undefined { + let activeLines: string[] | null = null + let version: string | undefined + let invalid = false + + for (const line of iterateProcessOutputLines(output)) { + const marker = parseMarker(line) + if (marker === 'BEGIN') { + if (activeLines || version !== undefined) { + invalid = true + } + activeLines = [] + continue + } + if (marker === 'END') { + if (!activeLines || activeLines.length !== 1 || version !== undefined) { + invalid = true + activeLines = null + continue + } + version = activeLines[0] + activeLines = null + continue + } + if (activeLines) { + if (activeLines.length === 1) { + invalid = true + } else { + activeLines.push(line) + } + } + } + + if (invalid || activeLines || !isDarwinProductVersion(version)) { + return undefined + } + return version +} + +function isAbortError(error: unknown): boolean { + return error instanceof Error && error.name === 'AbortError' +} + +export async function detectSshRelayDarwinVersion( + conn: SshConnection, + { signal }: { signal?: AbortSignal } = {} +): Promise { + try { + const output = await execCommand(conn, PROBE_COMMAND, { + signal, + timeoutMs: PROBE_TIMEOUT_MS + }) + return parseDarwinProductVersion(output) + } catch (error) { + // Why: cancellation must settle the caller's work instead of becoming compatibility evidence. + if (isAbortError(error)) { + throw error + } + return undefined + } +} From 5e7f8e5f54c7d458f9f8db1a46c06fe0a62943ea Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 04:58:51 -0400 Subject: [PATCH 195/267] docs: record relay macOS version CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 13 +++-- ...github-release-implementation-checklist.md | 53 +++++++++++++++++-- 2 files changed, 58 insertions(+), 8 deletions(-) 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 0eb79abaed2..04f4d9f9a0b 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 @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — exact-head native CI for -the locally green disconnected marked macOS product-version probe, 2026-07-15, Codex implementation -owner.** +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — audit the next smallest +remaining disconnected host-evidence boundary after exact-head acceptance of macOS product-version +detection, 2026-07-15, Codex implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -170,6 +170,13 @@ cancellable marked POSIX command returns only a unique bounded numeric `sw_vers malformed, noisy, duplicate, unavailable, or oversized evidence returns unknown and cancellation propagates. Both native runner families require the suite. Exact Node 24/all-six native proof is next; real SSH/macOS, Rosetta, composition, and all production/default behavior remain absent. +`E-M5-DARWIN-VERSION-DETECTION-CI-001` closes implementation commit `bcda04389`: all six primary +native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and +computer-use pass. Every native client runs the 21-case purpose suite; POSIX jobs pass 81 files / +582 tests and Windows jobs pass 82 files / 573 tests with 13 declared skips. Windows arm64 verifies +the complete 85,213,511-byte runtime plus Node/PTY/watcher/resource settlement and retains only the +known hosted build-26200 rejection against required 26100. This is contract evidence, not live SSH/ +macOS or Rosetta proof; composition and every production/default path remain absent. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 cd65688bf4c..4e38e30ae98 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: implement the disconnected marked macOS product-version probe fixed by E-M5-DARWIN-VERSION-DETECTION-AUDIT-001**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **LOCAL GREEN / EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M5-DARWIN-VERSION-DETECTION-LOCAL-001` passes the 21-case purpose contract, 62 focused/workflow tests, 415 relay tests with three declared skips, 280 release-script tests, typecheck, lint, format, max-lines, diff, and protected-resolver gates. Both native workflow families require the suite, but exact Node 24/all-six native execution remains open until the implementation commit is pushed. Rosetta/process-translation detection remains a separate uncomposed gate. No host-evidence composer, product caller, setting, transfer/install, fallback classifier, tuple enablement, release publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the next smallest remaining disconnected host-evidence boundary after exact-head acceptance of macOS product-version detection**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **CI CLOSED / NEXT AUDIT — 2026-07-15, Codex implementation owner** — `E-M5-DARWIN-VERSION-DETECTION-CI-001` closes implementation commit `bcda04389`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. Windows arm64 verifies the complete runtime tree and exact-byte Node/PTY/watcher/resource-settlement smoke, then retains only the declared hosted build-26200 rejection against required 26100. Every native client runs the new 21-case contract. Rosetta/process translation, real SSH/macOS, host-evidence composition, product callers, settings, transfer/install, fallback, tuple enablement, publication, and default behavior remain absent. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -15319,6 +15319,49 @@ config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 1 implementation head. No real SSH/macOS host, Rosetta, full host composition, product caller, transfer/install, setting, fallback, tuple, publication, or default behavior is proven or added. +### E-M5-DARWIN-VERSION-DETECTION-CI-001 — exact-head native macOS version contract + +- Date/owner: 2026-07-15, Codex implementation owner. +- Source: implementation commit `bcda04389590e30bec2ac7f8e15516f937a2019f` on draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). Artifact run + [29484393590](https://github.com/stablyai/orca/actions/runs/29484393590) checks out that exact SHA + and uses Node 24.18.0. The aggregate conclusion is failure only because the already-declared + Windows arm64 floor job rejects hosted build 26200 against required 26100. +- Six primary native jobs: Windows x64 + [87575304056](https://github.com/stablyai/orca/actions/runs/29484393590/job/87575304056), Windows + arm64 [87575304065](https://github.com/stablyai/orca/actions/runs/29484393590/job/87575304065), + macOS arm64 [87575304087](https://github.com/stablyai/orca/actions/runs/29484393590/job/87575304087), + Linux arm64 [87575304100](https://github.com/stablyai/orca/actions/runs/29484393590/job/87575304100), + Linux x64 [87575304113](https://github.com/stablyai/orca/actions/runs/29484393590/job/87575304113), + and macOS x64 + [87575304141](https://github.com/stablyai/orca/actions/runs/29484393590/job/87575304141) pass checkout, + dependency install, native workflow contracts, exact inputs, two clean deterministic builds, + archive/tree verification, exact-byte Node/PTY/watcher smoke, full-size extraction/cache metrics, + and unpublished evidence upload. The new purpose suite passes 21/21 on every job. Each POSIX + client passes 81 files / 582 tests; each Windows client passes 82 files / 573 tests with 13 + declared skips. +- Supplemental Linux jobs: arm64 + [87576256001](https://github.com/stablyai/orca/actions/runs/29484393590/job/87576256001) and x64 + [87576256015](https://github.com/stablyai/orca/actions/runs/29484393590/job/87576256015) verify the + exact unpublished bytes and pass oldest-userland Node/PTY/watcher smoke; the documented kernel + gap remains explicit. +- Windows baseline jobs: x64 + [87577449489](https://github.com/stablyai/orca/actions/runs/29484393590/job/87577449489) passes on + Windows build 20348. Arm64 + [87577449408](https://github.com/stablyai/orca/actions/runs/29484393590/job/87577449408) verifies the + complete 85,213,511-byte / 42-file tree, Node 24.18.0 ABI 137, PTY resize/exit, five watcher + events, resource settlement after 2,000 ms, 5,884.1533 ms smoke at 48,582,656-byte RSS, and then + fails only the floor predicate: observed `10.0.26200`, required build 26100. This matches the + pre-existing declared runner gap; no new failure appears. +- Exact-head repository checks: PR Checks + [29484394474](https://github.com/stablyai/orca/actions/runs/29484394474), Golden E2E + [29484393471](https://github.com/stablyai/orca/actions/runs/29484393471), and computer-use + [29484393610](https://github.com/stablyai/orca/actions/runs/29484393610) pass. +- Boundary: this is portable contract and target-native client evidence, not live SSH/macOS or + Rosetta evidence. The module has no product caller. No host composer, setting, transfer/install, + fallback classifier, tuple enablement, publication, or default behavior is added; legacy remains + the sole production path and no tuple is enabled. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -15377,9 +15420,9 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit the locally proven disconnected marked macOS product-version package, push it to the draft -PR, and require exact-head all-six native artifact CI before advancing. Do not compose full host -evidence, add +Audit the next smallest disconnected host-evidence slice after exact-head acceptance of macOS +product-version detection. Do not compose full host evidence, provision production secrets or +environments, add Rosetta detection, provision production secrets/environments, add an Electron/startup consumer, embed unreviewed keys or manifest bytes, add SSH transfer/install, mode wiring, fallback, tuple enablement, release publication, or default behavior. From c8d4acb2cca0e4ddbab21bf6f85eda2a9fca1272 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 05:07:43 -0400 Subject: [PATCH 196/267] feat: detect remote macOS translation Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 25 +++- ...github-release-implementation-checklist.md | 106 ++++++++++++++++- ...relay-darwin-translation-detection.test.ts | 96 +++++++++++++++ .../ssh-relay-darwin-translation-detection.ts | 111 ++++++++++++++++++ 6 files changed, 333 insertions(+), 8 deletions(-) create mode 100644 src/main/ssh/ssh-relay-darwin-translation-detection.test.ts create mode 100644 src/main/ssh/ssh-relay-darwin-translation-detection.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index e2258a3a36b..4f9060735ae 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -265,6 +265,7 @@ jobs: src/main/ssh/ssh-relay-libc-detection.test.ts \ src/main/ssh/ssh-relay-linux-kernel-detection.test.ts \ src/main/ssh/ssh-relay-darwin-version-detection.test.ts \ + src/main/ssh/ssh-relay-darwin-translation-detection.test.ts \ src/main/ssh/ssh-relay-artifact-download.test.ts \ src/main/ssh/ssh-relay-artifact-extraction.test.ts \ src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts \ @@ -813,6 +814,7 @@ jobs: src/main/ssh/ssh-relay-libc-detection.test.ts ` src/main/ssh/ssh-relay-linux-kernel-detection.test.ts ` src/main/ssh/ssh-relay-darwin-version-detection.test.ts ` + src/main/ssh/ssh-relay-darwin-translation-detection.test.ts ` src/main/ssh/ssh-relay-artifact-download.test.ts ` src/main/ssh/ssh-relay-artifact-extraction.test.ts ` src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 4b8aa436ba8..8a4b8e51421 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -123,6 +123,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-libc-detection.test.ts', 'ssh-relay-linux-kernel-detection.test.ts', 'ssh-relay-darwin-version-detection.test.ts', + 'ssh-relay-darwin-translation-detection.test.ts', 'ssh-relay-artifact-download.test.ts', 'ssh-relay-artifact-extraction.test.ts', 'ssh-relay-artifact-cache-lock-release.test.ts', 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 04f4d9f9a0b..ec97b124f55 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 @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — audit the next smallest -remaining disconnected host-evidence boundary after exact-head acceptance of macOS product-version -detection, 2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — implement the +disconnected marked Darwin process-translation probe fixed by +`E-M5-DARWIN-TRANSLATION-DETECTION-AUDIT-001`, 2026-07-15, Codex implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -177,6 +177,25 @@ computer-use pass. Every native client runs the 21-case purpose suite; POSIX job the complete 85,213,511-byte runtime plus Node/PTY/watcher/resource settlement and retains only the known hosted build-26200 rejection against required 26100. This is contract evidence, not live SSH/ macOS or Rosetta proof; composition and every production/default path remain absent. +`E-M5-DARWIN-TRANSLATION-DETECTION-AUDIT-001` fixes the next smallest package: one disconnected, +15-second cancellable POSIX-shell/no-Node marked probe of `sysctl.proc_translated` plus +`hw.optional.arm64`. Explicit non-conflicting 1/0 values return translated/native; a missing +translation key with explicit non-arm64 hardware proves native Intel; missing/conflicting/ +malformed evidence remains unknown. The detector does not change selector types or compose an +unknown into a boolean. Live Rosetta SSH remains a separate required cell; product callers, +transfer/install, settings, fallback wiring, tuple enablement, publication, and defaults stay +absent. +`E-M5-DARWIN-TRANSLATION-DETECTION-LOCAL-RED-001` records the expected missing-module failure and +the native-workflow oracle's 6/7 result before implementation. Neither runner command contains the +new suite, so its source occurrence count is one instead of three. Protected resolver files remain +untouched; no selector, composer, caller, or default behavior is connected. +`E-M5-DARWIN-TRANSLATION-DETECTION-LOCAL-001` is green: 23/23 purpose tests, 85/85 focused+workflow +tests, 438/438 non-full-size relay tests with three declared skips, 280/280 release-script tests, +typecheck, lint, format, max-lines, diff, and protected-resolver gates pass. Explicit +non-conflicting Darwin sysctl evidence returns translated/native; Intel absent-key evidence is +handled without coercing arm64-capable or unknown probe loss to native. Both native workflow +families require the suite. Exact Node 24/all-six native proof is next; live Rosetta SSH, +composition, and all production/default behavior remain absent. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 4e38e30ae98..aec232e75bf 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the next smallest remaining disconnected host-evidence boundary after exact-head acceptance of macOS product-version detection**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **CI CLOSED / NEXT AUDIT — 2026-07-15, Codex implementation owner** — `E-M5-DARWIN-VERSION-DETECTION-CI-001` closes implementation commit `bcda04389`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. Windows arm64 verifies the complete runtime tree and exact-byte Node/PTY/watcher/resource-settlement smoke, then retains only the declared hosted build-26200 rejection against required 26100. Every native client runs the new 21-case contract. Rosetta/process translation, real SSH/macOS, host-evidence composition, product callers, settings, transfer/install, fallback, tuple enablement, publication, and default behavior remain absent. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: implement the disconnected marked Darwin process-translation probe fixed by E-M5-DARWIN-TRANSLATION-DETECTION-AUDIT-001**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **LOCAL GREEN / EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M5-DARWIN-TRANSLATION-DETECTION-LOCAL-001` passes 23 purpose tests, 85 focused/workflow tests, 438 relay tests with three declared skips, 280 release-script tests, typecheck, lint, format, max-lines, diff, and protected-resolver gates. The detector returns translated/native/unknown evidence without changing the selector type or composing host evidence. Exact Node 24/all-six native execution remains open. Live Rosetta SSH remains mandatory before any translated tuple could be considered and translated hosts remain legacy-only. No product caller, setting, transfer/install, fallback wiring, tuple enablement, publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -15362,6 +15362,102 @@ config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 1 fallback classifier, tuple enablement, publication, or default behavior is added; legacy remains the sole production path and no tuple is enabled. +### E-M5-DARWIN-TRANSLATION-DETECTION-AUDIT-001 — disconnected Darwin translation evidence + +- Date/owner: 2026-07-15, Codex implementation owner. +- Audit: current platform detection reports the remote process architecture from marked `uname -sm` + but cannot distinguish an x64 process on Intel hardware from an x64 process translated by Rosetta + on Apple Silicon. The selector already rejects explicit `processTranslated: true` as + `translated-process`, and the reviewed policy keeps Rosetta legacy-only until a separate live + Rosetta SSH cell passes. No desktop SSH module currently invokes `sysctl.proc_translated`. +- Finding: add one purpose-named `ssh-relay-darwin-translation-detection` module that consumes an + explicit SSH connection and optional cancellation signal, executes one 15-second POSIX-shell/ + no-Node marked probe, and returns `true`, `false`, or undefined. Query + `sysctl.proc_translated` and `hw.optional.arm64` within the same bounded command. An explicit + translation value of 1 is translated unless it conflicts with explicit non-arm64 hardware; an + explicit 0 is native. When the translation key is absent, explicit `hw.optional.arm64=0` proves + native Intel; missing translation on arm64-capable or unknown hardware remains unknown. Any + conflicting, malformed, duplicate, incomplete, noisy, or oversized evidence remains unknown. +- Local audit evidence: native macOS arm64 reports `uname -m` = `arm64`, + `/usr/sbin/sysctl -in sysctl.proc_translated` = `0`, and + `/usr/sbin/sysctl -in hw.optional.arm64` = `1`. This proves only the local native response, not + Rosetta or SSH execution. +- Safety contract: startup noise outside exact markers is ignored; only one complete unique line + containing the two exact bounded fields counts. Ordinary command failure is unavailable evidence; + `AbortError` propagates. The probe uses no Node, npm, Python, Perl, archive, checksum, or + localization-sensitive parsing. It does not change `SshRelayHostEvidence` or invent a value for + unknown evidence; conservative composition remains a separate package. +- Required RED: missing purpose module, native Apple Silicon, translated Apple Silicon, native + Intel-key-absent, unknown/missing/conflicting/malformed/multi-line/duplicate/unterminated/ + concatenated/oversized evidence, unavailable command, cancellation, exact signal/15-second exec, + no-Node command construction, and both native-workflow families requiring the suite. +- Deliberately absent: live Rosetta/Intel/macOS SSH cells, full host-evidence composition, Linux + libstdc++/GLIBCXX, Windows evidence, Electron/startup/product callers, transfer/install, settings, + fallback wiring, tuple enablement, publication, and defaults. Legacy remains the sole production + path and the primary HTML plan is unchanged. + +### E-M5-DARWIN-TRANSLATION-DETECTION-LOCAL-RED-001 — Darwin translation probe fails first + +- Date/owner: 2026-07-15, Codex implementation owner. +- Source: uncommitted RED tests atop evidence head `5e7f8e5f5`; protected resolver files have zero + diff. +- Purpose command: `pnpm vitest run +src/main/ssh/ssh-relay-darwin-translation-detection.test.ts` — expected FAIL, 1 failed suite / zero + collected tests because `ssh-relay-darwin-translation-detection` does not exist; 336 ms Vitest. +- Native-workflow command: `pnpm vitest run +config/scripts/ssh-relay-runtime-workflow.test.mjs` — expected FAIL, 1 file / 6 passed / 1 failed + in 393 ms. The source split has length 1 rather than the required 3 until both POSIX and + PowerShell native commands include the purpose suite. +- Environment: local macOS arm64, Node 26.0.0 / pnpm 10.24.0 with the expected Node-24 engine + warning. Exact Node 24 and all-six native execution remain required after local green. +- Boundary: failure-before-implementation evidence only. It authorizes only the audited disconnected + probe, purpose tests, and native-workflow inclusion; no selector/composer/product behavior is + connected. + +### E-M5-DARWIN-TRANSLATION-DETECTION-LOCAL-001 — bounded Darwin translation evidence + +- Date/owner: 2026-07-15, Codex implementation owner. +- Implementation: `ssh-relay-darwin-translation-detection.ts` executes one 15-second cancellable + marked POSIX command, guards `sysctl`, and captures `sysctl.proc_translated` plus + `hw.optional.arm64` in one exact bounded line. Explicit non-conflicting translation/native values + return true/false; absent translation with explicit non-arm64 hardware returns false for native + Intel; arm64-capable/unknown missing evidence and conflicts return undefined. Startup noise is + ignored outside markers; empty, multi-line, duplicate, unterminated, marker-concatenated, extra, + non-canonical, invalid, or oversized evidence returns undefined. Ordinary execution failure is + unavailable and cancellation propagates. No selector/composer is connected. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-darwin-translation-detection.test.ts` — PASS, 1 file / 23 + tests in 212 ms Vitest, 1.08 s wall, 132,104,192-byte maximum RSS, zero swaps. Translated/native + Apple Silicon, explicit native/translated signals, Intel absent-key behavior, malformed/conflict/ + unavailable/cancellation boundaries, exact 15-second signal-qualified exec, and no forbidden + runtime dependency pass. +- Focused plus workflow command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-darwin-translation-detection.test.ts +src/main/ssh/ssh-relay-darwin-version-detection.test.ts +src/main/ssh/ssh-relay-artifact-selector.test.ts +src/main/ssh/ssh-remote-platform-detection.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 5 files / 85 tests in 2.23 s Vitest, + 3.21 s wall, 136,921,088-byte maximum RSS, zero swaps. Both native workflow command families + require the purpose suite exactly once; current platform and selector behavior remains unchanged. +- Broader relay command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS, 36 files passed / 3 declared + skipped, 438 tests passed / 3 declared skipped in 16.43 s Vitest, 17.55 s wall, 303,955,968-byte + maximum RSS, zero swaps. Expected stale-lock and local Node 26 deprecation diagnostics remain. +- Release-script command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 --silent=true +config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 14.47 s Vitest, + 16.26 s wall, 195,428,352-byte maximum RSS, zero swaps. +- Static gates: `pnpm typecheck`, targeted `oxlint`, targeted `oxfmt --check` after formatting its two + reported files, `pnpm lint`, `pnpm run check:max-lines-ratchet`, and `git diff --check` pass. Full + lint includes switch exhaustiveness, 41 reliability gates, 355 grandfathered max-lines entries + with no new bypass, bundled-skill verification, 9,837 localization references and locale parity, + and zero coverage candidates. Its 26 warnings are in untouched files. Protected resolver files + have zero diff. +- Residual gap: exact Node 24/all-six native client execution, live Intel/Apple Silicon/Rosetta SSH, + host composition, and every production/default path remain unproven and disconnected. Unknown + evidence is not coerced to false. No tuple is enabled and legacy remains the only default. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -15420,9 +15516,9 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Audit the next smallest disconnected host-evidence slice after exact-head acceptance of macOS -product-version detection. Do not compose full host evidence, provision production secrets or -environments, add +Commit the locally proven disconnected Darwin process-translation package, push it to the draft PR, +and require exact-head all-six native artifact CI before advancing. Do not compose full host evidence, +provision production secrets or environments, add Rosetta detection, provision production secrets/environments, add an Electron/startup consumer, embed unreviewed keys or manifest bytes, add SSH transfer/install, mode wiring, fallback, tuple enablement, release publication, or default behavior. diff --git a/src/main/ssh/ssh-relay-darwin-translation-detection.test.ts b/src/main/ssh/ssh-relay-darwin-translation-detection.test.ts new file mode 100644 index 00000000000..f71f0cc2807 --- /dev/null +++ b/src/main/ssh/ssh-relay-darwin-translation-detection.test.ts @@ -0,0 +1,96 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { SshConnection } from './ssh-connection' + +const execCommandMock = vi.hoisted(() => vi.fn()) + +vi.mock('./ssh-relay-deploy-helpers', () => ({ + execCommand: execCommandMock +})) + +const { detectSshRelayDarwinProcessTranslation } = + await import('./ssh-relay-darwin-translation-detection') + +const conn = {} as SshConnection +const marker = '__ORCA_SSH_RELAY_DARWIN_TRANSLATION__' + +function segment(line: string): string { + return [`${marker} BEGIN`, line, `${marker} END`].join('\n') +} + +describe('detectSshRelayDarwinProcessTranslation', () => { + beforeEach(() => { + execCommandMock.mockReset() + }) + + it.each([ + { line: 'translated=1 arm64=1', expected: true, label: 'translated Apple Silicon' }, + { line: 'translated=1 arm64=', expected: true, label: 'authoritative translated signal' }, + { line: 'translated=0 arm64=1', expected: false, label: 'native Apple Silicon' }, + { line: 'translated=0 arm64=0', expected: false, label: 'explicit native Intel' }, + { line: 'translated=0 arm64=', expected: false, label: 'authoritative native signal' }, + { line: 'translated= arm64=0', expected: false, label: 'Intel without the translation key' } + ])('returns $expected for $label', async ({ line, expected }) => { + execCommandMock.mockResolvedValueOnce( + ['Last login: ignored', segment(line), 'post-command noise'].join('\n') + ) + + await expect(detectSshRelayDarwinProcessTranslation(conn)).resolves.toBe(expected) + }) + + it.each([ + ['', 'empty output'], + [segment('translated= arm64=1'), 'missing translation on arm64 hardware'], + [segment('translated= arm64='), 'missing both values'], + [segment('translated=1 arm64=0'), 'conflicting translated Intel evidence'], + [segment('translated=2 arm64=1'), 'invalid translation value'], + [segment('translated=1 arm64=2'), 'invalid hardware value'], + [segment('translated=1 arm64=1 extra=1'), 'extra field'], + [segment('translated=1 arm64=1'), 'non-canonical whitespace'], + [segment('translated=1\narm64=1'), 'multiple marked lines'], + [`${marker} BEGIN\ntranslated=1 arm64=1`, 'unterminated segment'], + [segment(`translated=${'1'.repeat(65)} arm64=1`), 'oversized value'], + [[segment('translated=1 arm64=1'), segment('translated=1 arm64=1')].join('\n'), 'duplicate'], + [ + `startup${marker} BEGIN\ntranslated=1 arm64=1\n${marker} END`, + 'marker concatenated to startup noise' + ], + ['translated=1 arm64=1', 'unmarked evidence'] + ])('returns unknown for %s', async (output) => { + execCommandMock.mockResolvedValueOnce(output) + + await expect(detectSshRelayDarwinProcessTranslation(conn)).resolves.toBeUndefined() + }) + + it('classifies an unavailable probe as unknown without inventing composition policy', async () => { + execCommandMock.mockRejectedValueOnce(new Error('remote command unavailable')) + + await expect(detectSshRelayDarwinProcessTranslation(conn)).resolves.toBeUndefined() + }) + + it('propagates cancellation and passes the signal into the single bounded probe', async () => { + const signal = new AbortController().signal + const abortError = Object.assign(new Error('cancelled'), { name: 'AbortError' }) + execCommandMock.mockRejectedValueOnce(abortError) + + await expect(detectSshRelayDarwinProcessTranslation(conn, { signal })).rejects.toBe(abortError) + expect(execCommandMock).toHaveBeenCalledTimes(1) + expect(execCommandMock).toHaveBeenCalledWith(conn, expect.any(String), { + signal, + timeoutMs: 15_000 + }) + }) + + it('constructs one marked POSIX-shell sysctl probe without runtime dependencies', async () => { + execCommandMock.mockResolvedValueOnce(segment('translated=0 arm64=1')) + + await detectSshRelayDarwinProcessTranslation(conn) + + const command = execCommandMock.mock.calls[0]?.[1] ?? '' + expect(command).toContain('command -v sysctl') + expect(command).toContain('sysctl -in sysctl.proc_translated') + expect(command).toContain('sysctl -in hw.optional.arm64') + expect(command).toContain(`${marker} BEGIN`) + expect(command).toContain(`${marker} END`) + expect(command).not.toMatch(/\b(?:node|npm|python|perl|tar|sha256sum|shasum)\b/u) + }) +}) diff --git a/src/main/ssh/ssh-relay-darwin-translation-detection.ts b/src/main/ssh/ssh-relay-darwin-translation-detection.ts new file mode 100644 index 00000000000..1e65369685d --- /dev/null +++ b/src/main/ssh/ssh-relay-darwin-translation-detection.ts @@ -0,0 +1,111 @@ +import { + getProcessOutputFields, + iterateProcessOutputLines +} from '../../shared/process-output-field-scanner' +import type { SshConnection } from './ssh-connection' +import { execCommand } from './ssh-relay-deploy-helpers' + +const PROBE_MARKER = '__ORCA_SSH_RELAY_DARWIN_TRANSLATION__' +const PROBE_TIMEOUT_MS = 15_000 +const EVIDENCE_MAX_CHARS = 64 + +const PROBE_COMMAND = [ + `printf '\n%s\n' '${PROBE_MARKER} BEGIN'`, + 'if command -v sysctl >/dev/null 2>&1; then', + ' translated=$(sysctl -in sysctl.proc_translated 2>/dev/null) || translated=', + ' arm64=$(sysctl -in hw.optional.arm64 2>/dev/null) || arm64=', + ` printf 'translated=%s arm64=%s\n' "$translated" "$arm64"`, + 'fi', + `printf '%s\n' '${PROBE_MARKER} END'` +].join('\n') + +function parseMarker(line: string): 'BEGIN' | 'END' | null { + const fields = getProcessOutputFields(line, 3) + if (fields.length !== 2 || fields[0] !== PROBE_MARKER) { + return null + } + return fields[1] === 'BEGIN' || fields[1] === 'END' ? fields[1] : null +} + +function classifyTranslation(line: string | undefined): boolean | undefined { + if (!line || line.length > EVIDENCE_MAX_CHARS) { + return undefined + } + const match = /^translated=(0|1)? arm64=(0|1)?$/.exec(line) + if (!match) { + return undefined + } + const translated = match[1] + const arm64 = match[2] + if (translated === '1') { + return arm64 === '0' ? undefined : true + } + if (translated === '0') { + return false + } + return arm64 === '0' ? false : undefined +} + +function parseTranslation(output: string): boolean | undefined { + let activeLines: string[] | null = null + let evidenceLine: string | undefined + let complete = false + let invalid = false + + for (const line of iterateProcessOutputLines(output)) { + const marker = parseMarker(line) + if (marker === 'BEGIN') { + if (activeLines || complete) { + invalid = true + } + activeLines = [] + continue + } + if (marker === 'END') { + if (!activeLines || activeLines.length !== 1 || complete) { + invalid = true + activeLines = null + continue + } + evidenceLine = activeLines[0] + complete = true + activeLines = null + continue + } + if (activeLines) { + if (activeLines.length === 1) { + invalid = true + } else { + activeLines.push(line) + } + } + } + + if (invalid || activeLines || !complete) { + return undefined + } + return classifyTranslation(evidenceLine) +} + +function isAbortError(error: unknown): boolean { + return error instanceof Error && error.name === 'AbortError' +} + +export async function detectSshRelayDarwinProcessTranslation( + conn: SshConnection, + { signal }: { signal?: AbortSignal } = {} +): Promise { + try { + const output = await execCommand(conn, PROBE_COMMAND, { + signal, + timeoutMs: PROBE_TIMEOUT_MS + }) + return parseTranslation(output) + } catch (error) { + // Why: cancellation must settle the caller's work instead of becoming compatibility evidence. + if (isAbortError(error)) { + throw error + } + return undefined + } +} From dcf26940e572733fe018f65cf175140f223e8930 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 05:23:48 -0400 Subject: [PATCH 197/267] docs: record relay Darwin translation CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 12 +++- ...github-release-implementation-checklist.md | 60 ++++++++++++++++++- 2 files changed, 67 insertions(+), 5 deletions(-) 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 ec97b124f55..e1fef0c7462 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 @@ -9,9 +9,8 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — implement the -disconnected marked Darwin process-translation probe fixed by -`E-M5-DARWIN-TRANSLATION-DETECTION-AUDIT-001`, 2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — audit the disconnected +Linux libstdc++/GLIBCXX host-evidence boundary, 2026-07-15, Codex implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -196,6 +195,13 @@ non-conflicting Darwin sysctl evidence returns translated/native; Intel absent-k handled without coercing arm64-capable or unknown probe loss to native. Both native workflow families require the suite. Exact Node 24/all-six native proof is next; live Rosetta SSH, composition, and all production/default behavior remain absent. +`E-M5-DARWIN-TRANSLATION-DETECTION-CI-001` closes implementation commit `c8d4acb2c`: all six +primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, +and computer-use pass. POSIX clients pass 82 files / 605 tests and Windows clients pass 83 files / +596 tests with 13 declared skips, including the 23-case purpose suite. Windows arm64 verifies the +complete 85,213,511-byte runtime plus Node/PTY/watcher/resource settlement and retains only the +declared hosted build-26200 rejection against required 26100. This is contract evidence, not live +SSH/Intel/Rosetta proof; composition and every production/default path remain absent. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 aec232e75bf..461d32b3840 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: implement the disconnected marked Darwin process-translation probe fixed by E-M5-DARWIN-TRANSLATION-DETECTION-AUDIT-001**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **LOCAL GREEN / EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M5-DARWIN-TRANSLATION-DETECTION-LOCAL-001` passes 23 purpose tests, 85 focused/workflow tests, 438 relay tests with three declared skips, 280 release-script tests, typecheck, lint, format, max-lines, diff, and protected-resolver gates. The detector returns translated/native/unknown evidence without changing the selector type or composing host evidence. Exact Node 24/all-six native execution remains open. Live Rosetta SSH remains mandatory before any translated tuple could be considered and translated hosts remain legacy-only. No product caller, setting, transfer/install, fallback wiring, tuple enablement, publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the disconnected Linux libstdc++/GLIBCXX host-evidence boundary**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **DARWIN TRANSLATION CI CLOSED / NEXT AUDIT ACTIVE — 2026-07-15, Codex implementation owner** — `E-M5-DARWIN-TRANSLATION-DETECTION-CI-001` closes exact-head portable contract proof on all six native clients, both Linux supplements, Windows x64, PR Checks, Golden E2E, and computer-use. Windows arm64 retains only the declared hosted build-26200 rejection against required 26100 after complete byte/tree/Node/PTY/watcher/resource proof. Live Rosetta SSH remains mandatory before any translated tuple could be considered and translated hosts remain legacy-only. The active audit may authorize only a disconnected Linux libstdc++/GLIBCXX probe and purpose tests; no product caller, setting, transfer/install, fallback wiring, tuple enablement, publication, or default behavior may be connected. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -15458,6 +15458,62 @@ config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 1 host composition, and every production/default path remain unproven and disconnected. Unknown evidence is not coerced to false. No tuple is enabled and legacy remains the only default. +### E-M5-DARWIN-TRANSLATION-DETECTION-CI-001 — exact-head native translation contract + +- Date/owner: 2026-07-15, Codex implementation owner. +- Source: implementation commit `c8d4acb2cca0e4ddbab21bf6f85eda2a9fca1272` on draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). Artifact run + [29486003082](https://github.com/stablyai/orca/actions/runs/29486003082) checks out that exact SHA + and uses Node 24.18.0. Its aggregate conclusion is failure only because the already-declared + Windows arm64 floor job rejects hosted build 26200 against required 26100. +- Six primary native jobs pass: Windows arm64 + [87580520842](https://github.com/stablyai/orca/actions/runs/29486003082/job/87580520842), macOS + arm64 [87580520848](https://github.com/stablyai/orca/actions/runs/29486003082/job/87580520848), + Linux arm64 [87580520849](https://github.com/stablyai/orca/actions/runs/29486003082/job/87580520849), + Windows x64 [87580520869](https://github.com/stablyai/orca/actions/runs/29486003082/job/87580520869), + macOS x64 [87580520874](https://github.com/stablyai/orca/actions/runs/29486003082/job/87580520874), + and Linux x64 + [87580520925](https://github.com/stablyai/orca/actions/runs/29486003082/job/87580520925). Every job + passes checkout, dependencies, the new 23-case purpose suite, exact inputs, two clean builds, + archive/tree verification, exact-byte Node/PTY/watcher smoke, full-size extraction/cache gates, + and unpublished evidence upload. POSIX clients pass 82 files / 605 tests; Windows clients pass 83 + files / 596 tests with 13 declared skips. +- Primary full-size metrics: extraction spans 1,277.58–3,499.69 ms and + 31,014,912–50,987,008 incremental RSS bytes for 31,776,498–37,168,778-byte archives, + 85,213,511–124,846,430 expanded bytes, and 34–42 files. Cold immutable publication spans + 1,747.60–4,960.67 ms and 41,283,584–59,977,728 incremental RSS bytes; warm verified lookup spans + 177.70–912.37 ms and 0–5,861,376 bytes. Active retention spans 10.40–46.41 ms and 0–2,748,416 + bytes; eviction spans 29.26–91.28 ms and 0–1,712,128 bytes, reclaiming each complete + 118,418,686–161,259,369-byte entry. All remain inside the existing budgets. +- Supplemental Linux jobs: x64 + [87581673366](https://github.com/stablyai/orca/actions/runs/29486003082/job/87581673366) passes in + 43 seconds and arm64 + [87581673420](https://github.com/stablyai/orca/actions/runs/29486003082/job/87581673420) passes in + 49 seconds. Both re-authenticate the exact unpublished runtime and pass the digest-pinned glibc + 2.28/libstdc++ 6.0.25 Node/PTY/watcher userland proof; the shared hosted kernel gap remains. +- Windows baseline jobs: x64 + [87582483921](https://github.com/stablyai/orca/actions/runs/29486003082/job/87582483921) passes on + Windows Server build 20348 after verifying 60 entries / 42 files / 96,527,161 bytes, Node + 24.18.0 ABI 137, PTY resize/exit, five watcher events, resource settlement after 2,000 ms, and + 5,354.977 ms smoke at 50,012,160-byte RSS. Arm64 + [87582484015](https://github.com/stablyai/orca/actions/runs/29486003082/job/87582484015) verifies 60 + entries / 42 files / 85,213,511 bytes, Node 24.18.0 ABI 137, the same PTY/watcher/resource + contract, and 5,914.7293 ms smoke at 49,098,752-byte RSS, then fails only `osBuild: false` for + observed 10.0.26200 versus required 26100. +- Exact-head adjacent runs pass: PR Checks + [29486003131](https://github.com/stablyai/orca/actions/runs/29486003131) job + [87580520678](https://github.com/stablyai/orca/actions/runs/29486003131/job/87580520678), Golden + E2E [29486003110](https://github.com/stablyai/orca/actions/runs/29486003110) macOS job + [87580520501](https://github.com/stablyai/orca/actions/runs/29486003110/job/87580520501) and Linux + job [87580520516](https://github.com/stablyai/orca/actions/runs/29486003110/job/87580520516), and + computer-use [29486003157](https://github.com/stablyai/orca/actions/runs/29486003157) Ubuntu job + [87580520599](https://github.com/stablyai/orca/actions/runs/29486003157/job/87580520599) and Windows + job [87580520621](https://github.com/stablyai/orca/actions/runs/29486003157/job/87580520621). +- Boundary: this is contract and target-native client evidence, not live SSH/Intel/Rosetta/macOS + remote proof. The detector remains disconnected. No host composer, product caller, setting, + transfer/install, fallback classifier, tuple enablement, publication, or default behavior is + added; legacy remains the sole production path and no tuple is enabled. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 156ea35c0e0beab83714655ee3f467545b0d11b5 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 05:35:48 -0400 Subject: [PATCH 198/267] feat: detect remote Linux libstdc++ ABI Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 22 +- ...github-release-implementation-checklist.md | 102 ++++++- ...sh-relay-linux-libstdcxx-detection.test.ts | 183 ++++++++++++ .../ssh-relay-linux-libstdcxx-detection.ts | 260 ++++++++++++++++++ 6 files changed, 566 insertions(+), 4 deletions(-) create mode 100644 src/main/ssh/ssh-relay-linux-libstdcxx-detection.test.ts create mode 100644 src/main/ssh/ssh-relay-linux-libstdcxx-detection.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 4f9060735ae..09c027644fb 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -266,6 +266,7 @@ jobs: src/main/ssh/ssh-relay-linux-kernel-detection.test.ts \ src/main/ssh/ssh-relay-darwin-version-detection.test.ts \ src/main/ssh/ssh-relay-darwin-translation-detection.test.ts \ + src/main/ssh/ssh-relay-linux-libstdcxx-detection.test.ts \ src/main/ssh/ssh-relay-artifact-download.test.ts \ src/main/ssh/ssh-relay-artifact-extraction.test.ts \ src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts \ @@ -815,6 +816,7 @@ jobs: src/main/ssh/ssh-relay-linux-kernel-detection.test.ts ` src/main/ssh/ssh-relay-darwin-version-detection.test.ts ` src/main/ssh/ssh-relay-darwin-translation-detection.test.ts ` + src/main/ssh/ssh-relay-linux-libstdcxx-detection.test.ts ` src/main/ssh/ssh-relay-artifact-download.test.ts ` src/main/ssh/ssh-relay-artifact-extraction.test.ts ` src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 8a4b8e51421..227d4f486a4 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -124,6 +124,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-linux-kernel-detection.test.ts', 'ssh-relay-darwin-version-detection.test.ts', 'ssh-relay-darwin-translation-detection.test.ts', + 'ssh-relay-linux-libstdcxx-detection.test.ts', 'ssh-relay-artifact-download.test.ts', 'ssh-relay-artifact-extraction.test.ts', 'ssh-relay-artifact-cache-lock-release.test.ts', 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 e1fef0c7462..3c06c95d5f4 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 @@ -9,8 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — audit the disconnected -Linux libstdc++/GLIBCXX host-evidence boundary, 2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — close exact-head native +CI evidence for the disconnected Linux libstdc++/GLIBCXX detector, 2026-07-15, Codex +implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -202,6 +203,23 @@ and computer-use pass. POSIX clients pass 82 files / 605 tests and Windows clien complete 85,213,511-byte runtime plus Node/PTY/watcher/resource settlement and retains only the declared hosted build-26200 rejection against required 26100. This is contract evidence, not live SSH/Intel/Rosetta proof; composition and every production/default path remain absent. +`E-M5-LINUX-LIBSTDCXX-DETECTION-AUDIT-001` fixes the next smallest package: one disconnected, +15-second cancellable marked Linux loader-cache probe using `ldconfig`, `readlink -f`, and bounded +binary-safe `grep -ao`. It does not require `strings`, which is absent from the audited minimal +Ubuntu image. Only complete, bounded, consistent physical libstdc++ and maximum GLIBCXX evidence +counts; loader overrides, missing tools, malformed/duplicate/overflow evidence, and conflicting +multilib candidates return unknown. Host composition, product callers, transfer/install, settings, +fallback, tuple enablement, publication, and defaults stay absent. +`E-M5-LINUX-LIBSTDCXX-DETECTION-LOCAL-RED-001` records the expected missing-module failure and the +native-workflow oracle's 6/7 result. Neither runner family contains the new suite yet; no product +caller or default behavior is connected. +`E-M5-LINUX-LIBSTDCXX-DETECTION-LOCAL-001` is green: 23/23 purpose tests, 142/142 focused+workflow +tests, 461/461 broader relay tests with three declared skips, 280/280 release-script tests, +typecheck, lint, format, max-lines, diff, and protected-resolver gates pass. The single bounded probe +refuses loader overrides, requires strict consistent loader-cache/physical-file/GLIBCXX evidence, +and has exact POSIX syntax proof without `strings` or a runtime dependency. Both native workflow +families require the suite. Exact Node 24/all-six native proof is next; host composition, live SSH, +and every production/default path remain absent. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 461d32b3840..5e67eb07f2b 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the disconnected Linux libstdc++/GLIBCXX host-evidence boundary**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **DARWIN TRANSLATION CI CLOSED / NEXT AUDIT ACTIVE — 2026-07-15, Codex implementation owner** — `E-M5-DARWIN-TRANSLATION-DETECTION-CI-001` closes exact-head portable contract proof on all six native clients, both Linux supplements, Windows x64, PR Checks, Golden E2E, and computer-use. Windows arm64 retains only the declared hosted build-26200 rejection against required 26100 after complete byte/tree/Node/PTY/watcher/resource proof. Live Rosetta SSH remains mandatory before any translated tuple could be considered and translated hosts remain legacy-only. The active audit may authorize only a disconnected Linux libstdc++/GLIBCXX probe and purpose tests; no product caller, setting, transfer/install, fallback wiring, tuple enablement, publication, or default behavior may be connected. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: close exact-head native CI evidence for the disconnected Linux libstdc++/GLIBCXX detector**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **LOCAL GREEN / EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M5-LINUX-LIBSTDCXX-DETECTION-LOCAL-001` passes the 23-case purpose contract, 142 focused/workflow cases, 461 broader relay tests with three declared skips, 280 release-script tests, typecheck, lint, format, max-lines, diff, and protected-resolver gates. The detector remains disconnected and returns unknown for loader overrides, missing tools, malformed/overflow evidence, or conflicting candidates. Exact Node 24/all-six native execution remains open. No host composer, product caller, setting, transfer/install, fallback wiring, tuple enablement, publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -15514,6 +15514,104 @@ config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 1 transfer/install, fallback classifier, tuple enablement, publication, or default behavior is added; legacy remains the sole production path and no tuple is enabled. +### E-M5-LINUX-LIBSTDCXX-DETECTION-AUDIT-001 — disconnected loader ABI evidence + +- Date/owner: 2026-07-15, Codex implementation owner. +- Audit: the selector already requires both the reviewed physical libstdc++ 6.0.25 floor and + `GLIBCXX_3.4.25`, but no desktop SSH module acquires either value. Requiring `strings` would be a + portability regression: the unmodified `ubuntu:22.04` arm64 image has `ldconfig`, `readlink`, + `grep`, and `sed` but no `strings`. Its loader cache resolves + `/usr/lib/aarch64-linux-gnu/libstdc++.so.6.0.30`, and binary-safe `grep -ao` yields bounded + `GLIBCXX_3.4` through `GLIBCXX_3.4.30` evidence. +- Oldest-floor audit: the existing digest-pinned Rocky Linux 8.9 image + `docker.io/library/rockylinux@sha256:2d05a9266523bbf24f33ebc3a9832e4d5fd74b973c220f2204ca802286aa275d` + resolves `/usr/lib64/libstdc++.so.6.0.25` and yields `GLIBCXX_3.4` through + `GLIBCXX_3.4.25`. Both Docker probes ran with `--network none`; Rocky x64 ran through Docker + Desktop emulation on the local macOS arm64 controller. An Ubuntu repetition with + `LD_LIBRARY_PATH=/tmp` intentionally produced no candidate evidence. +- Finding: add one purpose-named `ssh-relay-linux-libstdcxx-detection` module that consumes an + explicit SSH connection and optional cancellation signal. One 15-second marked POSIX-shell probe + locates `ldconfig` in `PATH`, `/sbin`, or `/usr/sbin`, resolves loader-cache `libstdc++.so.6` + candidates with `readlink -f`, and scans version symbols with `LC_ALL=C grep -ao`. Cap the probe + at eight candidate libraries and 256 symbols per library. The parser accepts only bounded safe + absolute paths, exact `libstdc++.so.X.Y.Z` basenames, exact `GLIBCXX_X.Y[.Z]` symbols, complete + nesting, and a single consistent version pair across distinct candidates. +- Safety contract: missing tools/candidates, non-empty `LD_LIBRARY_PATH` or `LD_PRELOAD`, duplicate + paths, malformed/incomplete/duplicate/overflow segments, and conflicting multilib evidence return + unknown. Ordinary command failure is unavailable compatibility evidence; `AbortError` propagates. + The probe uses no Node, npm, Python, Perl, tar, checksum tool, `strings`, package manager, + compiler, or locale-sensitive package output. The later staged bundled-Node execution remains the + authoritative pre-install compatibility proof; this probe only supports conservative pre-download + selection and does not settle the future launch-environment sanitization contract. +- Required RED: missing purpose module; valid Rocky/Ubuntu and consistent-multilib evidence; + unavailable/override/empty/noisy/malformed/unsafe/duplicate/conflicting/overflow rejection; + cancellation and exact 15-second signal-qualified exec; exact no-`strings` command construction; + and both native workflow families requiring the suite. +- Deliberately absent: live SSH/Rocky/Ubuntu/multilib cells, host-evidence composition, libc/kernel/ + macOS/Windows callers, Electron/startup, cache acquisition, transfer/install, launch-environment + policy, settings, fallback, tuple enablement, publication, and defaults. Legacy remains the sole + production path and the primary HTML plan is unchanged. + +### E-M5-LINUX-LIBSTDCXX-DETECTION-LOCAL-RED-001 — loader ABI probe fails first + +- Date/owner: 2026-07-15, Codex implementation owner. +- Source: uncommitted RED tests atop evidence head `dcf26940e`; protected resolver files have zero + diff. +- Purpose command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-linux-libstdcxx-detection.test.ts` — expected FAIL, one failed suite / zero + collected tests in 274 ms because `ssh-relay-linux-libstdcxx-detection` does not exist. +- Native-workflow command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +config/scripts/ssh-relay-runtime-workflow.test.mjs` — expected FAIL, one file / six passed / one + failed in 348 ms. The new suite occurs once in the workflow oracle instead of three times until + both POSIX and PowerShell native commands are wired. +- Environment/boundary: local macOS arm64, Node 26.0.0 / pnpm 10.24.0. This is + failure-before-implementation evidence only; exact Node 24/all-six proof remains required after + local green. It authorizes only the audited disconnected probe, purpose tests, and workflow + inclusion; no composer/product/default behavior is connected. + +### E-M5-LINUX-LIBSTDCXX-DETECTION-LOCAL-001 — bounded loader ABI evidence + +- Date/owner: 2026-07-15, Codex implementation owner. +- Implementation: `ssh-relay-linux-libstdcxx-detection.ts` executes one 15-second cancellable marked + POSIX command. It refuses `LD_LIBRARY_PATH`/`LD_PRELOAD`, locates the glibc loader cache, resolves + at most eight `libstdc++.so.6` candidates, and emits at most 256 binary-safe GLIBCXX matches per + library. The streaming parser accepts only safe normalized absolute paths, exact physical + basenames, numeric ABI symbols, complete marker nesting, unique paths, and a single consistent + version pair. It computes the maximum GLIBCXX version without trusting output order. Missing + tools/candidates, ordinary exec failure, malformed/duplicate/conflicting/overflow evidence, or + loader overrides return undefined; cancellation propagates. The module has no production import. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-linux-libstdcxx-detection.test.ts` — PASS, one file / 23 + tests in 229 ms Vitest, 1.28 s wall, 132,055,040-byte maximum RSS, zero swaps. Rocky 8 floor, + Ubuntu 22.04, consistent/conflicting multilib, unordered maximum, missing/override/malformed/ + unsafe/duplicate/overflow evidence, cancellation, exact 15-second signal-qualified exec, + forbidden-dependency absence, and exact POSIX syntax pass. The POSIX syntax case is a declared + Windows skip when the purpose suite runs there. +- Focused compatibility/workflow command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1` plus the new purpose suite, libc, kernel, Darwin version, + Darwin translation, selector, platform detection, and native-workflow suites — PASS, eight files / + 142 tests in 5.06 s Vitest, 7.36 s wall, 134,283,264-byte maximum RSS, zero swaps. Both native + workflow families require the new suite exactly once. +- Broader relay command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS, 37 files passed / three + declared skipped, 461 tests passed / three declared skipped in 25.67 s Vitest, 28.53 s wall, + 236,781,568-byte maximum RSS, zero swaps. The expected stale-lock and local Node 26 deprecation + diagnostics remain unchanged. +- Release-script command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 --silent=true config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 + tests in 17.82 s Vitest, 19.97 s wall, 195,592,192-byte maximum RSS, zero swaps. +- Static gates: `pnpm typecheck`, targeted `oxlint`, targeted `oxfmt --check`, `pnpm lint`, + `pnpm run check:max-lines-ratchet`, and `git diff --check` pass. Full lint includes switch + exhaustiveness, 41 reliability gates, 355 grandfathered max-lines entries with no new bypass, + bundled-skill verification, 9,837 localization references and locale parity, and zero coverage + candidates. Its 26 warnings are in untouched files. Protected resolver files have zero diff. +- Boundary/residual gaps: the exact command has POSIX syntax proof and the audited primitives + produce the expected Rocky/Ubuntu ABI evidence, but this package is not live SSH/distro/multilib + proof and does not establish remote output/channel limits. Exact Node 24 on all six native clients + remains required. Host composition, future launch-environment sanitization, Electron/startup, + product SSH, cache acquisition, transfer/install, settings, fallback, tuple enablement, + publication, and defaults remain absent; legacy remains the sole production path. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/src/main/ssh/ssh-relay-linux-libstdcxx-detection.test.ts b/src/main/ssh/ssh-relay-linux-libstdcxx-detection.test.ts new file mode 100644 index 00000000000..47a8b20c782 --- /dev/null +++ b/src/main/ssh/ssh-relay-linux-libstdcxx-detection.test.ts @@ -0,0 +1,183 @@ +import { spawnSync } from 'node:child_process' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { SshConnection } from './ssh-connection' + +const execCommandMock = vi.hoisted(() => vi.fn()) + +vi.mock('./ssh-relay-deploy-helpers', () => ({ + execCommand: execCommandMock +})) + +const { detectSshRelayLinuxLibstdcxx } = await import('./ssh-relay-linux-libstdcxx-detection') + +const conn = {} as SshConnection +const marker = '__ORCA_SSH_RELAY_LIBSTDCXX__' + +function library(path: string, ...symbols: string[]): string { + const file = path.slice(path.lastIndexOf('/') + 1) + return [ + `${marker} LIBRARY_BEGIN`, + `path=${path}`, + `file=${file}`, + ...symbols, + `${marker} LIBRARY_END` + ].join('\n') +} + +function segment(...libraries: string[]): string { + return [`${marker} BEGIN`, ...libraries, `${marker} END`].join('\n') +} + +describe('detectSshRelayLinuxLibstdcxx', () => { + beforeEach(() => { + execCommandMock.mockReset() + }) + + it.each([ + { + label: 'Rocky 8 floor', + output: library( + '/usr/lib64/libstdc++.so.6.0.25', + 'GLIBCXX_3.4', + 'GLIBCXX_3.4.24', + 'GLIBCXX_3.4.25' + ), + expected: { libstdcxxVersion: '6.0.25', glibcxxVersion: '3.4.25' } + }, + { + label: 'Ubuntu 22.04', + output: library( + '/usr/lib/aarch64-linux-gnu/libstdc++.so.6.0.30', + 'GLIBCXX_3.4.25', + 'GLIBCXX_3.4.30', + 'GLIBCXX_3.4.29' + ), + expected: { libstdcxxVersion: '6.0.30', glibcxxVersion: '3.4.30' } + }, + { + label: 'consistent multilib candidates', + output: [ + library('/usr/lib64/libstdc++.so.6.0.25', 'GLIBCXX_3.4.25'), + library('/usr/lib/libstdc++.so.6.0.25', 'GLIBCXX_3.4.25') + ].join('\n'), + expected: { libstdcxxVersion: '6.0.25', glibcxxVersion: '3.4.25' } + } + ])('returns the strict maximum ABI pair for $label', async ({ output, expected }) => { + execCommandMock.mockResolvedValueOnce( + ['Last login: ignored', segment(output), 'post-command noise'].join('\n') + ) + + await expect(detectSshRelayLinuxLibstdcxx(conn)).resolves.toEqual(expected) + }) + + it.each([ + ['', 'empty output'], + [segment(), 'no loader candidate or refused loader override'], + [library('/usr/lib64/libstdc++.so.6.0.25', 'GLIBCXX_3.4.25'), 'unmarked library'], + [ + `${marker} BEGIN\n${library('/usr/lib64/libstdc++.so.6.0.25', 'GLIBCXX_3.4.25')}`, + 'unterminated outer segment' + ], + [ + segment(`${marker} LIBRARY_BEGIN\npath=/usr/lib64/libstdc++.so.6.0.25`), + 'unterminated library segment' + ], + [segment(library('../libstdc++.so.6.0.25', 'GLIBCXX_3.4.25')), 'non-absolute path'], + [ + segment(library(`/usr/${'a'.repeat(1025)}/libstdc++.so.6.0.25`, 'GLIBCXX_3.4.25')), + 'oversized path' + ], + [segment(library('/usr/lib64/libstdc++.so.6.bad', 'GLIBCXX_3.4.25')), 'malformed filename'], + [segment(library('/usr/lib64/libstdc++.so.6.0.25')), 'missing GLIBCXX symbols'], + [segment(library('/usr/lib64/libstdc++.so.6.0.25', 'GLIBCXX_3.4.bad')), 'malformed symbol'], + [ + segment( + library('/usr/lib64/libstdc++.so.6.0.25', 'GLIBCXX_3.4.25'), + library('/usr/lib/libstdc++.so.6.0.30', 'GLIBCXX_3.4.30') + ), + 'conflicting multilib candidates' + ], + [ + segment( + library('/usr/lib64/libstdc++.so.6.0.25', 'GLIBCXX_3.4.25'), + library('/usr/lib64/libstdc++.so.6.0.25', 'GLIBCXX_3.4.25') + ), + 'duplicate resolved path' + ], + [segment(`${marker} LIBRARY_OVERFLOW`), 'candidate overflow'], + [ + segment( + library( + '/usr/lib64/libstdc++.so.6.0.25', + ...Array.from({ length: 257 }, () => 'GLIBCXX_3.4.25') + ) + ), + 'symbol count overflow' + ], + [ + segment( + library('/usr/lib64/libstdc++.so.6.0.25', 'GLIBCXX_3.4.25', `${marker} SYMBOL_OVERFLOW`) + ), + 'explicit symbol overflow' + ], + [ + `startup${marker} BEGIN\n${library('/usr/lib64/libstdc++.so.6.0.25', 'GLIBCXX_3.4.25')}\n${marker} END`, + 'marker concatenated to startup noise' + ] + ])('returns unknown for %s', async (output) => { + execCommandMock.mockResolvedValueOnce(output) + + await expect(detectSshRelayLinuxLibstdcxx(conn)).resolves.toBeUndefined() + }) + + it('classifies an unavailable probe as unknown without inventing composition policy', async () => { + execCommandMock.mockRejectedValueOnce(new Error('remote command unavailable')) + + await expect(detectSshRelayLinuxLibstdcxx(conn)).resolves.toBeUndefined() + }) + + it('propagates cancellation and passes the signal into the single bounded probe', async () => { + const signal = new AbortController().signal + const abortError = Object.assign(new Error('cancelled'), { name: 'AbortError' }) + execCommandMock.mockRejectedValueOnce(abortError) + + await expect(detectSshRelayLinuxLibstdcxx(conn, { signal })).rejects.toBe(abortError) + expect(execCommandMock).toHaveBeenCalledTimes(1) + expect(execCommandMock).toHaveBeenCalledWith(conn, expect.any(String), { + signal, + timeoutMs: 15_000 + }) + }) + + it('constructs one bounded loader-cache probe without strings or runtime dependencies', async () => { + execCommandMock.mockResolvedValueOnce( + segment(library('/usr/lib64/libstdc++.so.6.0.25', 'GLIBCXX_3.4.25')) + ) + + await detectSshRelayLinuxLibstdcxx(conn) + + const command = execCommandMock.mock.calls[0]?.[1] ?? '' + expect(command).toContain('ldconfig') + expect(command).toContain('readlink -f') + expect(command).toContain("grep -ao 'GLIBCXX_[0-9][0-9.]*'") + expect(command).toContain('LD_LIBRARY_PATH') + expect(command).toContain('LD_PRELOAD') + expect(command).toContain('256') + expect(command).toContain('8') + expect(command).toContain(`${marker} BEGIN`) + expect(command).toContain(`${marker} END`) + expect(command).not.toMatch( + /\b(?:node|npm|python|perl|tar|sha256sum|shasum|strings|gcc|g\+\+|rpm|dpkg|apk)\b/u + ) + }) + + it.skipIf(process.platform === 'win32')('constructs valid POSIX shell syntax', async () => { + execCommandMock.mockResolvedValueOnce(segment()) + + await detectSshRelayLinuxLibstdcxx(conn) + + const command = execCommandMock.mock.calls[0]?.[1] ?? '' + const result = spawnSync('/bin/sh', ['-n'], { input: command, encoding: 'utf8' }) + expect({ status: result.status, stderr: result.stderr }).toEqual({ status: 0, stderr: '' }) + }) +}) diff --git a/src/main/ssh/ssh-relay-linux-libstdcxx-detection.ts b/src/main/ssh/ssh-relay-linux-libstdcxx-detection.ts new file mode 100644 index 00000000000..de1b8f2b8d4 --- /dev/null +++ b/src/main/ssh/ssh-relay-linux-libstdcxx-detection.ts @@ -0,0 +1,260 @@ +import { + getProcessOutputFields, + iterateProcessOutputLines +} from '../../shared/process-output-field-scanner' +import type { SshConnection } from './ssh-connection' +import type { SshRelayLinuxHostEvidence } from './ssh-relay-artifact-selector' +import { execCommand } from './ssh-relay-deploy-helpers' + +export type SshRelayLinuxLibstdcxxEvidence = Required< + Pick +> + +type LibraryCandidate = SshRelayLinuxLibstdcxxEvidence & { path: string } +type ProbeMarker = 'BEGIN' | 'END' | 'LIBRARY_BEGIN' | 'LIBRARY_END' | 'INVALID' + +const PROBE_MARKER = '__ORCA_SSH_RELAY_LIBSTDCXX__' +const PROBE_TIMEOUT_MS = 15_000 +const MAX_LIBRARIES = 8 +const MAX_SYMBOLS_PER_LIBRARY = 256 +const MAX_LIBRARY_PATH_CHARS = 1024 + +// Why: loader overrides make cache evidence ambiguous; staged bundled Node remains authoritative. +const PROBE_COMMAND = [ + `printf '\n%s\n' '${PROBE_MARKER} BEGIN'`, + 'if [ -z "${LD_LIBRARY_PATH-}" ] && [ -z "${LD_PRELOAD-}" ] && command -v readlink >/dev/null 2>&1 && command -v grep >/dev/null 2>&1; then', + ' if command -v ldconfig >/dev/null 2>&1; then orca_ldconfig=ldconfig', + ' elif [ -x /sbin/ldconfig ]; then orca_ldconfig=/sbin/ldconfig', + ' elif [ -x /usr/sbin/ldconfig ]; then orca_ldconfig=/usr/sbin/ldconfig', + ' else orca_ldconfig=', + ' fi', + ' if [ -n "$orca_ldconfig" ]; then', + ' "$orca_ldconfig" -p 2>/dev/null | while IFS= read -r orca_line; do', + ' case "$orca_line" in', + " *libstdc++.so.6*'=>'*)", + ' orca_path=${orca_line##*=> }', + ' orca_real=$(readlink -f "$orca_path" 2>/dev/null) || continue', + ' orca_file=${orca_real##*/}', + ' case "$orca_file" in libstdc++.so.*.*.*) ;; *) continue ;; esac', + ' orca_libraries=$((${orca_libraries:-0} + 1))', + ` if [ "$orca_libraries" -gt ${MAX_LIBRARIES} ]; then`, + ` printf '%s\n' '${PROBE_MARKER} LIBRARY_OVERFLOW'`, + ' break', + ' fi', + ` printf '%s\n' '${PROBE_MARKER} LIBRARY_BEGIN'`, + ' printf \'path=%s\\nfile=%s\\n\' "$orca_real" "$orca_file"', + ' LC_ALL=C grep -ao \'GLIBCXX_[0-9][0-9.]*\' "$orca_real" 2>/dev/null | {', + ' orca_symbols=0', + ' while IFS= read -r orca_symbol; do', + ' orca_symbols=$((orca_symbols + 1))', + ` if [ "$orca_symbols" -gt ${MAX_SYMBOLS_PER_LIBRARY} ]; then`, + ` printf '%s\n' '${PROBE_MARKER} SYMBOL_OVERFLOW'`, + ' break', + ' fi', + ' printf \'%s\\n\' "$orca_symbol"', + ' done', + ' }', + ` printf '%s\n' '${PROBE_MARKER} LIBRARY_END'`, + ' ;;', + ' esac', + ' done', + ' fi', + 'fi', + `printf '%s\n' '${PROBE_MARKER} END'` +].join('\n') + +function parseMarker(line: string): ProbeMarker | null { + const fields = getProcessOutputFields(line, 3) + if (fields.length !== 2 || fields[0] !== PROBE_MARKER) { + return null + } + switch (fields[1]) { + case 'BEGIN': + case 'END': + case 'LIBRARY_BEGIN': + case 'LIBRARY_END': + return fields[1] + case 'LIBRARY_OVERFLOW': + case 'SYMBOL_OVERFLOW': + return 'INVALID' + default: + return null + } +} + +function parseVersion(value: string, minimumComponents: number): number[] | null { + if (!/^\d+(?:\.\d+){1,2}$/.test(value)) { + return null + } + const components = value.split('.').map(Number) + return components.length >= minimumComponents && components.every(Number.isSafeInteger) + ? components + : null +} + +function compareVersions(left: number[], right: number[]): number { + const length = Math.max(left.length, right.length) + for (let index = 0; index < length; index += 1) { + const difference = (left[index] ?? 0) - (right[index] ?? 0) + if (difference !== 0) { + return difference + } + } + return 0 +} + +function isSafeLibraryPath(value: string): boolean { + if ( + value.length > MAX_LIBRARY_PATH_CHARS || + !/^\/[0-9A-Za-z_+./-]+$/.test(value) || + value.includes('//') + ) { + return false + } + return value.split('/').every((part) => part !== '.' && part !== '..') +} + +function parseLibraryCandidate(lines: string[]): LibraryCandidate | null { + if (lines.length < 3 || lines.length > MAX_SYMBOLS_PER_LIBRARY + 2) { + return null + } + const path = lines[0].startsWith('path=') ? lines[0].slice('path='.length) : '' + const file = lines[1].startsWith('file=') ? lines[1].slice('file='.length) : '' + if (!isSafeLibraryPath(path) || path.slice(path.lastIndexOf('/') + 1) !== file) { + return null + } + const fileMatch = /^libstdc\+\+\.so\.(\d+\.\d+\.\d+)$/.exec(file) + if (!fileMatch || !parseVersion(fileMatch[1], 3)) { + return null + } + + let maximumSymbol: { version: string; components: number[] } | null = null + for (const line of lines.slice(2)) { + const symbolMatch = /^GLIBCXX_(\d+\.\d+(?:\.\d+)?)$/.exec(line) + if (!symbolMatch) { + return null + } + const components = parseVersion(symbolMatch[1], 2) + if (!components) { + return null + } + if (!maximumSymbol || compareVersions(components, maximumSymbol.components) > 0) { + maximumSymbol = { version: symbolMatch[1], components } + } + } + if (!maximumSymbol) { + return null + } + return { + path, + libstdcxxVersion: fileMatch[1], + glibcxxVersion: maximumSymbol.version + } +} + +function parseLibstdcxxEvidence(output: string): SshRelayLinuxLibstdcxxEvidence | undefined { + let outerActive = false + let outerComplete = false + let libraryLines: string[] | null = null + let invalid = false + const candidates: LibraryCandidate[] = [] + + for (const line of iterateProcessOutputLines(output)) { + const marker = parseMarker(line) + if (marker === 'BEGIN') { + if (outerActive || outerComplete || libraryLines) { + invalid = true + } + outerActive = true + continue + } + if (marker === 'END') { + if (!outerActive || outerComplete || libraryLines) { + invalid = true + } + outerActive = false + outerComplete = true + continue + } + if (marker === 'LIBRARY_BEGIN') { + if (!outerActive || libraryLines) { + invalid = true + } + libraryLines = [] + continue + } + if (marker === 'LIBRARY_END') { + if (!outerActive || !libraryLines) { + invalid = true + libraryLines = null + continue + } + const candidate = parseLibraryCandidate(libraryLines) + if (!candidate) { + invalid = true + } else { + candidates.push(candidate) + } + libraryLines = null + continue + } + if (marker === 'INVALID') { + invalid = true + continue + } + if (libraryLines) { + if (libraryLines.length >= MAX_SYMBOLS_PER_LIBRARY + 2) { + invalid = true + } else { + libraryLines.push(line) + } + } else if (outerActive) { + invalid = true + } + } + + if ( + invalid || + outerActive || + !outerComplete || + libraryLines || + candidates.length === 0 || + candidates.length > MAX_LIBRARIES + ) { + return undefined + } + const paths = new Set(candidates.map((candidate) => candidate.path)) + const versions = new Set( + candidates.map((candidate) => `${candidate.libstdcxxVersion}:${candidate.glibcxxVersion}`) + ) + if (paths.size !== candidates.length || versions.size !== 1) { + return undefined + } + return { + libstdcxxVersion: candidates[0].libstdcxxVersion, + glibcxxVersion: candidates[0].glibcxxVersion + } +} + +function isAbortError(error: unknown): boolean { + return error instanceof Error && error.name === 'AbortError' +} + +export async function detectSshRelayLinuxLibstdcxx( + conn: SshConnection, + { signal }: { signal?: AbortSignal } = {} +): Promise { + try { + const output = await execCommand(conn, PROBE_COMMAND, { + signal, + timeoutMs: PROBE_TIMEOUT_MS + }) + return parseLibstdcxxEvidence(output) + } catch (error) { + // Why: cancellation must settle the caller's work instead of becoming compatibility evidence. + if (isAbortError(error)) { + throw error + } + return undefined + } +} From 1182bcdc5e3df9034f06cf796bd2db6afb36f56d Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 05:53:16 -0400 Subject: [PATCH 199/267] test: keep relay cache suites client-offline Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 18 ++++- ...github-release-implementation-checklist.md | 68 ++++++++++++++++--- .../ssh-relay-artifact-acquisition.test.ts | 11 ++- ...sh-relay-artifact-cache-population.test.ts | 6 ++ 4 files changed, 91 insertions(+), 12 deletions(-) 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 3c06c95d5f4..2fbfeeadaef 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 @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — close exact-head native -CI evidence for the disconnected Linux libstdc++/GLIBCXX detector, 2026-07-15, Codex -implementation owner.** +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — close replacement all-six +native evidence after the client-offline cache-test correction, 2026-07-15, Codex implementation +owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -220,6 +220,18 @@ refuses loader overrides, requires strict consistent loader-cache/physical-file/ and has exact POSIX syntax proof without `strings` or a runtime dependency. Both native workflow families require the suite. Exact Node 24/all-six native proof is next; host composition, live SSH, and every production/default path remain absent. +`E-M5-LINUX-LIBSTDCXX-DETECTION-CI-RED-001` records exact-head run `29487742612`: the native Darwin +x64 job passes the new 23-case detector suite, then two dependency-injected cache unit suites collect +zero cases because their transitive Electron import attempts an uninstalled-binary network fetch. +The job exits with two failed / 81 passed files and 612 passed tests. This is a client-offline test +isolation defect; rerun-until-green is rejected. Add only purpose-scoped Electron mocks to those two +unit suites, retain real downloader/integration coverage, and repeat all-six native CI. Production +code, host composition, SSH, settings, fallback, tuple enablement, and defaults remain unchanged. +`E-M5-LINUX-LIBSTDCXX-DETECTION-CLIENT-OFFLINE-CORRECTION-LOCAL-001` is green: the two +dependency-injected cache suites now mock Electron and assert `net.fetch` is never called. Focused +proof passes 46/46, broader relay proof passes 461/461 with three declared skips, release scripts +pass 280/280, and typecheck, lint, format, max-lines, diff, and protected-resolver gates pass. +Production code remains unchanged. Replacement exact Node 24/all-six native CI is next. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 5e67eb07f2b..980a5485502 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: close exact-head native CI evidence for the disconnected Linux libstdc++/GLIBCXX detector**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **LOCAL GREEN / EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M5-LINUX-LIBSTDCXX-DETECTION-LOCAL-001` passes the 23-case purpose contract, 142 focused/workflow cases, 461 broader relay tests with three declared skips, 280 release-script tests, typecheck, lint, format, max-lines, diff, and protected-resolver gates. The detector remains disconnected and returns unknown for loader overrides, missing tools, malformed/overflow evidence, or conflicting candidates. Exact Node 24/all-six native execution remains open. No host composer, product caller, setting, transfer/install, fallback wiring, tuple enablement, publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: close replacement all-six native evidence after the client-offline cache-test correction**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **CLIENT-OFFLINE TEST CORRECTION LOCAL GREEN / REPLACEMENT EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M5-LINUX-LIBSTDCXX-DETECTION-CLIENT-OFFLINE-CORRECTION-LOCAL-001` adds purpose-scoped Electron mocks and proves the dependency-injected cache suites never call `electron.net.fetch`. Focused 46-case, broader 461-case, release-script 280-case, typecheck, lint, formatting, max-lines, diff, and protected-resolver gates pass. Replacement all-six Node 24 CI remains required; the failed run is not accepted. No host composer, product caller, setting, transfer/install, fallback wiring, tuple enablement, publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -15612,6 +15612,58 @@ config/vitest.config.ts --maxWorkers=1` plus the new purpose suite, libc, kernel product SSH, cache acquisition, transfer/install, settings, fallback, tuple enablement, publication, and defaults remain absent; legacy remains the sole production path. +### E-M5-LINUX-LIBSTDCXX-DETECTION-CI-RED-001 — cache unit suites attempt Electron installation + +- Date/owner: 2026-07-15, Codex implementation owner. +- Source/run: exact implementation head `156ea35c0`; SSH Relay Runtime Artifacts run + `29487742612`, Darwin x64 job `87586158910`. +- Passing detector evidence: the new `ssh-relay-linux-libstdcxx-detection` suite passes all 23 cases + in 31 ms on the native `macos-15-intel` job. The job reaches 81 passing files and 612 passing + tests before the adjacent failure; this is not accepted as the required all-six gate. +- Failure: `ssh-relay-artifact-cache-population.test.ts` and + `ssh-relay-artifact-acquisition.test.ts` each collect zero tests. Both consume only injected + operations but import the production download module transitively; without an Electron mock, + `electron/index.js` starts `install-electron` because artifact CI intentionally installs source + dependencies with `--ignore-scripts`. Two bounded fetch attempts report `TypeError: fetch failed`, + followed by `Electron failed to install correctly`; the job exits 1 with two failed / 81 passed + files and 612 passed tests in 49.39 s. +- Classification/correction gate: this is a test-isolation and client-offline determinism defect. + Rerunning until the Electron download happens to succeed would hide the dependency and is not + acceptable evidence. Add a purpose-scoped `electron.net.fetch` mock to the two dependency-injected + unit suites, retain the real Electron fetch coverage in downloader/integration suites, then pass + focused local tests and a replacement all-six exact-head run. Production code and behavior must + remain unchanged. + +### E-M5-LINUX-LIBSTDCXX-DETECTION-CLIENT-OFFLINE-CORRECTION-LOCAL-001 — cache suites are network-independent + +- Date/owner: 2026-07-15, Codex implementation owner. +- Correction: `ssh-relay-artifact-cache-population.test.ts` and + `ssh-relay-artifact-acquisition.test.ts` now provide a purpose-scoped `electron.net.fetch` mock + before importing their dependency-injected composition modules. Every case asserts the mock was + never called. Downloader and real cold→warm integration suites retain their existing explicit + Electron fetch mocks and byte-stream behavior; production code is unchanged. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1` plus the two corrected suites, the Linux libstdc++ purpose suite, and the native + workflow oracle — PASS, four files / 46 tests in 1.52 s Vitest, 2.73 s wall, + 138,412,032-byte maximum RSS, zero swaps. No Electron binary installation or fetch occurs. +- Broader relay command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS, 37 files / 461 tests with + three declared skipped files/tests in 24.58 s Vitest, 27.49 s wall, 248,905,728-byte maximum RSS, + zero swaps. The expected stale-lock and local Node 26 deprecation diagnostics remain unchanged. A + first invocation quoted the glob, correctly found no files, and is not treated as evidence; this + recorded command uses shell expansion. +- Release-script command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 --silent=true config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 + tests in 20.37 s Vitest, 22.12 s wall, 190,201,856-byte maximum RSS, zero swaps. +- Static gates: `pnpm typecheck`, `pnpm lint`, `pnpm run check:max-lines-ratchet`, targeted `oxlint`, + targeted `oxfmt --check`, and `git diff --check` pass. Full lint retains the same 26 warnings in + untouched files, 41 reliability gates, 355 grandfathered max-lines entries with no new bypass, + 9,837 localization references and locale parity, and zero localization candidates. Protected + resolver files have zero diff. +- Boundary/residual gaps: local macOS arm64 uses Node 26.0.0 / pnpm 10.24.0; replacement exact Node + 24 on all six native clients remains mandatory. This correction changes tests only and proves no + production SSH, cache, fallback, or default behavior. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -15670,12 +15722,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit the locally proven disconnected Darwin process-translation package, push it to the draft PR, -and require exact-head all-six native artifact CI before advancing. Do not compose full host evidence, -provision production secrets or environments, add -Rosetta detection, provision production secrets/environments, add an Electron/startup consumer, -embed unreviewed keys or manifest bytes, add SSH transfer/install, mode wiring, fallback, tuple -enablement, release publication, or default behavior. +Commit the client-offline cache-test correction proven by +`E-M5-LINUX-LIBSTDCXX-DETECTION-CLIENT-OFFLINE-CORRECTION-LOCAL-001`, push it to the draft PR, and +require replacement exact-head all-six native artifact CI before advancing. Do not compose full host +evidence, provision production secrets or environments, add an Electron/startup consumer, embed +unreviewed keys or manifest bytes, add SSH transfer/install, mode wiring, fallback, tuple enablement, +release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. diff --git a/src/main/ssh/ssh-relay-artifact-acquisition.test.ts b/src/main/ssh/ssh-relay-artifact-acquisition.test.ts index ca6615777c7..15e59c91e66 100644 --- a/src/main/ssh/ssh-relay-artifact-acquisition.test.ts +++ b/src/main/ssh/ssh-relay-artifact-acquisition.test.ts @@ -2,7 +2,11 @@ import { join } from 'node:path' import { tmpdir } from 'node:os' import nacl from 'tweetnacl' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +// Why: injected cache composition tests must stay client-offline and never install Electron. +const electronNetFetchMock = vi.hoisted(() => vi.fn()) +vi.mock('electron', () => ({ net: { fetch: electronNetFetchMock } })) import { acquireSshRelayArtifact, @@ -83,10 +87,15 @@ const operations = { } beforeEach(() => { + electronNetFetchMock.mockReset() operations.resolve.mockReset() operations.populate.mockReset() }) +afterEach(() => { + expect(electronNetFetchMock).not.toHaveBeenCalled() +}) + describe('SSH relay artifact warm/cold acquisition', () => { it('returns unavailable without attempting cold population', async () => { operations.resolve.mockResolvedValue({ diff --git a/src/main/ssh/ssh-relay-artifact-cache-population.test.ts b/src/main/ssh/ssh-relay-artifact-cache-population.test.ts index dbb37a17a24..dc3d64d6143 100644 --- a/src/main/ssh/ssh-relay-artifact-cache-population.test.ts +++ b/src/main/ssh/ssh-relay-artifact-cache-population.test.ts @@ -5,6 +5,10 @@ import { join } from 'node:path' import nacl from 'tweetnacl' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +// Why: injected cache composition tests must stay client-offline and never install Electron. +const electronNetFetchMock = vi.hoisted(() => vi.fn()) +vi.mock('electron', () => ({ net: { fetch: electronNetFetchMock } })) + import { populateSshRelayArtifactCache, type SshRelayArtifactCachePopulationOperations @@ -80,6 +84,7 @@ const operations = { } beforeEach(() => { + electronNetFetchMock.mockReset() operations.download.mockReset() operations.publish.mockReset() operations.acquireInUseLease.mockReset() @@ -91,6 +96,7 @@ afterEach(async () => { .splice(0) .map((directory) => rm(directory, { recursive: true, force: true })) ) + expect(electronNetFetchMock).not.toHaveBeenCalled() }) async function expectEmptyDownloadStaging(cacheRoot: string): Promise { From 6209151dd9e817ee765eeee2f7020e879571707b Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 06:10:34 -0400 Subject: [PATCH 200/267] docs: record relay Linux ABI CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 13 +++- ...github-release-implementation-checklist.md | 66 ++++++++++++++++--- 2 files changed, 68 insertions(+), 11 deletions(-) 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 2fbfeeadaef..e6678d8a323 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 @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — close replacement all-six -native evidence after the client-offline cache-test correction, 2026-07-15, Codex implementation -owner.** +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — audit the disconnected +Windows build/OpenSSH/PowerShell/.NET compatibility-evidence boundary, 2026-07-15, Codex +implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -232,6 +232,13 @@ dependency-injected cache suites now mock Electron and assert `net.fetch` is nev proof passes 46/46, broader relay proof passes 461/461 with three declared skips, release scripts pass 280/280, and typecheck, lint, format, max-lines, diff, and protected-resolver gates pass. Production code remains unchanged. Replacement exact Node 24/all-six native CI is next. +`E-M5-LINUX-LIBSTDCXX-DETECTION-CI-001` closes exact head `1182bcdc5`: all six primary native Node +24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. +POSIX clients pass 83 files / 628 tests and Windows clients pass 84 files / 618 tests with 14 +declared skips. Windows arm64 completes 60-entry/42-file/85,213,511-byte tree, Node/PTY/watcher/ +resource proof and retains only the declared hosted build-26200 rejection against required 26100. +Full-size extraction/cache metrics stay inside budget. This is not live SSH/remote evidence; the +detector stays disconnected, every tuple stays disabled, and legacy remains the sole default. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 980a5485502..50583e5d2f2 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: close replacement all-six native evidence after the client-offline cache-test correction**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **CLIENT-OFFLINE TEST CORRECTION LOCAL GREEN / REPLACEMENT EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M5-LINUX-LIBSTDCXX-DETECTION-CLIENT-OFFLINE-CORRECTION-LOCAL-001` adds purpose-scoped Electron mocks and proves the dependency-injected cache suites never call `electron.net.fetch`. Focused 46-case, broader 461-case, release-script 280-case, typecheck, lint, formatting, max-lines, diff, and protected-resolver gates pass. Replacement all-six Node 24 CI remains required; the failed run is not accepted. No host composer, product caller, setting, transfer/install, fallback wiring, tuple enablement, publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the disconnected Windows build/OpenSSH/PowerShell/.NET compatibility-evidence boundary**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **LINUX LIBSTDCXX CI CLOSED / WINDOWS COMPATIBILITY AUDIT ACTIVE — 2026-07-15, Codex implementation owner** — `E-M5-LINUX-LIBSTDCXX-DETECTION-CI-001` closes the detector and client-offline correction on all six primary native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use. Windows arm64 completes exact bytes/tree/Node/PTY/watcher/resource proof and retains only the declared hosted build-26200 rejection against required 26100. The next package may audit only one disconnected Windows compatibility probe and purpose tests. No host composer, product caller, setting, transfer/install, fallback wiring, tuple enablement, publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -15664,6 +15664,56 @@ config/vitest.config.ts --maxWorkers=1` plus the new purpose suite, libc, kernel 24 on all six native clients remains mandatory. This correction changes tests only and proves no production SSH, cache, fallback, or default behavior. +### E-M5-LINUX-LIBSTDCXX-DETECTION-CI-001 — replacement native loader ABI and client-offline proof + +- Date/owner: 2026-07-15, Codex implementation owner. +- Source/run: exact head `1182bcdc5e3df9034f06cf796bd2db6afb36f56d`; SSH Relay Runtime + Artifacts run `29488827561`. All six primary jobs and both Linux oldest-userland supplements pass. + Windows x64 baseline passes; Windows arm64 retains only the declared hosted-build mismatch after + completing the full runtime proof. +- Native source-contract jobs: + + | Tuple | Job | Requested/resolved runner | Architecture | Contract result | + | ------------------- | ------------- | ---------------------------------------------------- | ------------ | ----------------------------- | + | `linux-x64-glibc` | `87589717854` | `ubuntu-24.04`; `ubuntu24` `20260705.232.1` | X64 | 83 files / 628 tests pass | + | `linux-arm64-glibc` | `87589717829` | `ubuntu-24.04-arm`; `ubuntu24-arm64` `20260706.52.2` | ARM64 | 83 files / 628 tests pass | + | `darwin-x64` | `87589717843` | `macos-15-intel`; `macos15` `20260629.0276.1` | X64 | 83 files / 628 tests pass | + | `darwin-arm64` | `87589717839` | `macos-15`; `macos15` `20260706.0213.1` | ARM64 | 83 files / 628 tests pass | + | `win32-x64` | `87589717812` | `windows-2022`; `win22` `20260706.237.1` | X64 | 84 files / 618 pass / 14 skip | + | `win32-arm64` | `87589717826` | `windows-11-arm`; `win11-arm64` `20260706.102.1` | ARM64 | 84 files / 618 pass / 14 skip | + + Every job records GitHub-hosted execution, Node 24.18.0, and the exact source commit. The detector + purpose suite passes 23/23 on POSIX and 22/22 with the one declared POSIX-syntax skip on Windows. + The corrected cache suites collect and pass normally without an Electron install/download. + +- Oldest-userland supplements: x64 job `87590861147` on `ubuntu24` `20260705.232.1` and arm64 job + `87590861158` on `ubuntu24-arm64` `20260706.52.2` pass the digest-pinned glibc 2.28 / libstdc++ + 6.0.25 evidence. Exact kernel 4.18 remains the already-declared residual gap. +- Full-size desktop metrics across the six primary jobs: extraction is 1,278.08–3,734.91 ms with + 32,448,512–45,395,968 incremental RSS; cold cache is 1,749.64–5,442.50 ms with + 40,873,984–48,943,104 incremental RSS; warm cache is 174.30–862.23 ms with 0–6,070,272 + incremental RSS; active retention is 11.65–42.60 ms; eviction is 25.03–90.56 ms. All remain + within the recorded desktop time, 80 MiB incremental-memory, and 2 GiB cache budgets. +- Windows x64 baseline: job `87591565355`, `windows-2022` / `win22` `20260714.244.1`, X64, build + 20348 — PASS. It verifies 60 entries / 42 files / 96,527,161 expanded bytes, Node 24.18.0 ABI + 137, PTY exit/resize behavior, five watcher events, and two-second resource settlement. Smoke is + 5,310.5246 ms / 50,552,832 RSS; complete verification is 6,473.4959 ms. The declared 19045 or + 20348 build contract qualifies. +- Windows arm64 baseline: job `87591565305`, `windows-11-arm` / `win11-arm64` + `20260714.109.1`, ARM64, build 26200. Before the expected final rejection it verifies 60 entries / + 42 files / 85,213,511 expanded bytes, Node 24.18.0 ABI 137, PTY exit/resize behavior, five watcher + events, and two-second resource settlement. Smoke is 6,064.0717 ms / 49,815,552 RSS; complete + verification is 8,098.9394 ms. Platform and architecture checks pass; only build 26200 differs + from the reviewed 26100 evidence cell, so this remains a residual hosted-runner gap and not a + tuple enablement. +- Adjacent exact-head proof: PR Checks run `29488826783`, job `87589715319`; Golden E2E run + `29488826838`, macOS job `87589715177` and Linux job `87589715191`; computer-use run + `29488826924`, Windows job `87589715474` and Ubuntu job `87589715539` — all pass. +- Boundary/residual gaps: this is portable source/native-client contract evidence, not live SSH, + Rocky/Ubuntu multilib, Windows remote, transfer, install, fallback, or bundled launch evidence. + The detector remains disconnected, every tuple remains disabled, and legacy remains the sole + production/default path. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -15722,12 +15772,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit the client-offline cache-test correction proven by -`E-M5-LINUX-LIBSTDCXX-DETECTION-CLIENT-OFFLINE-CORRECTION-LOCAL-001`, push it to the draft PR, and -require replacement exact-head all-six native artifact CI before advancing. Do not compose full host -evidence, provision production secrets or environments, add an Electron/startup consumer, embed -unreviewed keys or manifest bytes, add SSH transfer/install, mode wiring, fallback, tuple enablement, -release publication, or default behavior. +Audit only the smallest disconnected Windows compatibility-evidence boundary for the already-reviewed +OS build, OpenSSH for Windows, Windows PowerShell, and .NET Framework selector fields. Record the +exact acquisition/parsing/failure contract before adding purpose tests or implementation. Do not +compose full host evidence, provision production secrets or environments, add an Electron/startup +consumer, embed unreviewed keys or manifest bytes, add SSH transfer/install, mode wiring, fallback, +tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. From 30bf908f2477d3f4b568811469e04f2009c13b28 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 06:29:46 -0400 Subject: [PATCH 201/267] feat: detect Windows relay compatibility Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 23 +- ...github-release-implementation-checklist.md | 122 ++++++++++- ...ay-windows-compatibility-detection.test.ts | 192 +++++++++++++++++ ...h-relay-windows-compatibility-detection.ts | 201 ++++++++++++++++++ 6 files changed, 531 insertions(+), 10 deletions(-) create mode 100644 src/main/ssh/ssh-relay-windows-compatibility-detection.test.ts create mode 100644 src/main/ssh/ssh-relay-windows-compatibility-detection.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 09c027644fb..4541f8301c2 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -267,6 +267,7 @@ jobs: src/main/ssh/ssh-relay-darwin-version-detection.test.ts \ src/main/ssh/ssh-relay-darwin-translation-detection.test.ts \ src/main/ssh/ssh-relay-linux-libstdcxx-detection.test.ts \ + src/main/ssh/ssh-relay-windows-compatibility-detection.test.ts \ src/main/ssh/ssh-relay-artifact-download.test.ts \ src/main/ssh/ssh-relay-artifact-extraction.test.ts \ src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts \ @@ -817,6 +818,7 @@ jobs: src/main/ssh/ssh-relay-darwin-version-detection.test.ts ` src/main/ssh/ssh-relay-darwin-translation-detection.test.ts ` src/main/ssh/ssh-relay-linux-libstdcxx-detection.test.ts ` + src/main/ssh/ssh-relay-windows-compatibility-detection.test.ts ` src/main/ssh/ssh-relay-artifact-download.test.ts ` src/main/ssh/ssh-relay-artifact-extraction.test.ts ` src/main/ssh/ssh-relay-artifact-cache-lock-release.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 227d4f486a4..e5055c9dd7a 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -125,6 +125,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-darwin-version-detection.test.ts', 'ssh-relay-darwin-translation-detection.test.ts', 'ssh-relay-linux-libstdcxx-detection.test.ts', + 'ssh-relay-windows-compatibility-detection.test.ts', 'ssh-relay-artifact-download.test.ts', 'ssh-relay-artifact-extraction.test.ts', 'ssh-relay-artifact-cache-lock-release.test.ts', 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 e6678d8a323..b37d243bcf3 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 @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — audit the disconnected -Windows build/OpenSSH/PowerShell/.NET compatibility-evidence boundary, 2026-07-15, Codex -implementation owner.** +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — prove the locally-green, +disconnected Windows build/OpenSSH/PowerShell/.NET compatibility detector on exact-head native +runners, 2026-07-15, Codex implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -239,6 +239,23 @@ declared skips. Windows arm64 completes 60-entry/42-file/85,213,511-byte tree, N resource proof and retains only the declared hosted build-26200 rejection against required 26100. Full-size extraction/cache metrics stay inside budget. This is not live SSH/remote evidence; the detector stays disconnected, every tuple stays disabled, and legacy remains the sole default. +`E-M5-WINDOWS-COMPATIBILITY-DETECTION-AUDIT-001` fixes the next smallest package: one disconnected, +15-second cancellable encoded Windows PowerShell probe for the already-reviewed OS build, OpenSSH +for Windows, Windows PowerShell, and .NET Framework selector fields. Only one complete, bounded, +strict four-field segment counts; missing tools/registry data, malformed/duplicate/unknown/overflow +evidence, or ordinary command failure returns unknown, while cancellation propagates. Native +Windows syntax/execution and both workflow families are required. Host composition, live SSH, +transfer capability, settings, fallback, tuple enablement, publication, and defaults stay absent. +`E-M5-WINDOWS-COMPATIBILITY-DETECTION-RED-001` proves both gates fail before implementation at head +`6209151dd`: the purpose suite cannot import the absent detector (zero collected tests), while the +workflow oracle passes 6/7 and rejects the suite's single native-family occurrence against the two +required occurrences. No production caller or protected-resolver diff exists at the RED gate. +`E-M5-WINDOWS-COMPATIBILITY-DETECTION-LOCAL-001` is locally green: 26 purpose cases pass with the +native-Windows execution case declared skipped on macOS; 168/168 focused cases, 487/487 broader +relay cases, and 280/280 release-script cases pass. Typecheck, full lint, reliability, max-lines, +localization, format, diff, and protected-resolver gates pass. The module remains disconnected; +native Windows execution and all-six Node 24 proof are the next gate, while live SSH, composition, +settings, fallback wiring, tuple enablement, publication, defaults, and SignPath remain absent. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 50583e5d2f2..7bb157b4b75 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the disconnected Windows build/OpenSSH/PowerShell/.NET compatibility-evidence boundary**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **LINUX LIBSTDCXX CI CLOSED / WINDOWS COMPATIBILITY AUDIT ACTIVE — 2026-07-15, Codex implementation owner** — `E-M5-LINUX-LIBSTDCXX-DETECTION-CI-001` closes the detector and client-offline correction on all six primary native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use. Windows arm64 completes exact bytes/tree/Node/PTY/watcher/resource proof and retains only the declared hosted build-26200 rejection against required 26100. The next package may audit only one disconnected Windows compatibility probe and purpose tests. No host composer, product caller, setting, transfer/install, fallback wiring, tuple enablement, publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: prove the locally-green disconnected Windows build/OpenSSH/PowerShell/.NET compatibility detector on exact-head native runners**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **WINDOWS COMPATIBILITY LOCAL GREEN / EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M5-WINDOWS-COMPATIBILITY-DETECTION-LOCAL-001` proves the bounded strict detector, purpose cases, workflow-family oracle, broader relay/release regressions, and static gates locally. The native Windows execution case remains a declared macOS skip and must pass on Windows x64 and arm64 runners before this package closes. No host composer, product caller, setting, transfer/install, fallback wiring, tuple enablement, publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -147,11 +147,11 @@ same change as the work it records. local gates, and all-six native Node 24 proof are green under E-M5-ARTIFACT-CACHE-ROOT-LOCAL-RED-001, E-M5-ARTIFACT-CACHE-ROOT-LOCAL-001, and E-M5-ARTIFACT-CACHE-ROOT-CI-001. No Electron or product caller exists. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit the next - smallest remaining resolver/cache boundary after exact-head acceptance of disconnected warm/cold - acquisition. The eventual startup caller, not an SSH module, must supply - `getCanonicalUserDataPath()` captured before `app.setName()`; keep proxy integration, SSH, mode, - tuple, publication, fallback, and defaults separately gated. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — exact-head native proof + for the disconnected Windows build/OpenSSH/PowerShell/.NET compatibility detector under + E-M5-WINDOWS-COMPATIBILITY-DETECTION-LOCAL-001. Native Windows PowerShell execution must pass on + x64 and arm64; keep host composition, live SSH, transfer/install, settings, fallback wiring, + tuple enablement, publication, defaults, and SignPath separately gated. - 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 @@ -15714,6 +15714,114 @@ config/vitest.config.ts --maxWorkers=1` plus the new purpose suite, libc, kernel The detector remains disconnected, every tuple remains disabled, and legacy remains the sole production/default path. +### E-M5-WINDOWS-COMPATIBILITY-DETECTION-AUDIT-001 — disconnected bootstrap compatibility evidence + +- Date/owner: 2026-07-15, Codex implementation owner. +- Audit: the selector already requires four Windows values fixed by Milestone 1 — OS build, + OpenSSH for Windows version, Windows PowerShell version, and .NET Framework release — but no + desktop SSH module acquires them. Existing remote platform detection establishes only OS and + architecture and must remain unchanged by this package. +- Finding: add one purpose-named `ssh-relay-windows-compatibility-detection` module that consumes an + explicit SSH connection and optional cancellation signal. One 15-second encoded, noninteractive + Windows PowerShell command reads `CurrentBuildNumber` and .NET Framework v4 `Release` from the + 64-bit local-machine registry, reads `$PSVersionTable.PSVersion`, and invokes the exact discovered + `sshd.exe -V` binary only to extract an `X.YpZ` OpenSSH version. It emits only one fixed marked + segment with four key/value lines; raw registry values and native command output are never emitted. +- Parsing/safety contract: accept only one complete segment containing exactly one of each field. + OS build and .NET release must be positive safe integers, PowerShell must be a strict two-to-four- + component numeric version, and OpenSSH must be exact `X.YpZ`; each line and segment is bounded. + Missing `sshd`, registry keys, fields, or PowerShell data; malformed/duplicate/unknown fields; + incomplete/duplicate/nested markers; oversized values; and ordinary command failure return + unknown. `AbortError` propagates. Use the existing UTF-16LE `powerShellCommand` boundary with + `wrapCommand: false`; do not interpolate remote values or paths into command source. +- Required RED/green proof: valid Windows 10/Server 2022 x64 and Windows 11 arm64 evidence; + unordered fields; strict noisy/empty/missing/malformed/duplicate/unknown/overflow rejection; + cancellation and exact signal-qualified 15-second exec; encoded noninteractive command + construction; absence of Node, npm, Python, Perl, tar, checksum tools, or GitHub/network access; + native Windows PowerShell execution; and both native workflow families requiring the suite. +- Deliberately absent: process-translation policy, full host-evidence composition, selector changes, + live Windows SSH/OpenSSH-service evidence, semantic file-transfer capability probing, Electron/ + startup, cache acquisition, transfer/install, settings, fallback, tuple enablement, publication, + and defaults. SignPath is unrelated to this package and remains deferred; legacy remains the sole + production path. + +### E-M5-WINDOWS-COMPATIBILITY-DETECTION-RED-001 — purpose and workflow gates fail before implementation + +- Date/owner: 2026-07-15, Codex implementation owner; local macOS arm64 worktree at exact head + `6209151dd9e817ee765eeee2f7020e879571707b` plus the uncommitted audit/test gate. +- Purpose RED command: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts src/main/ssh/ssh-relay-windows-compatibility-detection.test.ts`. + Result: expected exit 1; Vitest 1.13 seconds, 3.93 seconds wall, 131,874,816-byte maximum RSS. + Collection stops at the awaited purpose import with only `Cannot find module +'/src/main/ssh/ssh-relay-windows-compatibility-detection'`; zero tests collect because no + implementation module exists. +- Workflow-oracle RED command: + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts config/scripts/ssh-relay-runtime-workflow.test.mjs`. + Result: expected exit 1; 6 pass / 1 fail, Vitest 1.41 seconds, 4.14 seconds wall, + 131,956,736-byte maximum RSS. The new purpose suite has one workflow occurrence, so the portable + native-family oracle receives split length 2 and rejects it against required length 3. This proves + the Windows workflow family is not yet wired. +- Interpretation: both failures are narrow and expected. No implementation module, product import, + production caller, selector/default change, or second workflow occurrence exists at this RED gate; + the protected Node/npm resolver diff remains empty. + +### E-M5-WINDOWS-COMPATIBILITY-DETECTION-LOCAL-001 — strict bounded Windows compatibility evidence + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0, + pnpm 10.24.0, atop head `6209151dd9e817ee765eeee2f7020e879571707b` plus this uncommitted + package. The expected Node-24 engine warning means exact Node 24 remains a CI gate. +- Implementation: `ssh-relay-windows-compatibility-detection.ts` executes one signal-qualified, + 15-second, UTF-16LE encoded Windows PowerShell command with `wrapCommand: false`. It reads the + 64-bit HKLM Windows build and .NET Framework v4 release keys, reads the current Windows + PowerShell version, discovers the exact `sshd.exe` application, and bounds its `-V` capture to + eight records/4,096 characters before accepting one normalized OpenSSH-for-Windows version. It + emits one fixed six-line marked segment and never emits raw registry or native-command output. + The streaming parser accepts exactly four unique fields, positive safe build/release integers, + a two-to-four-component numeric PowerShell version, exact `X.YpZ` OpenSSH, and at most 128 + characters per field line. Missing, empty, malformed, duplicate, unknown, nested, incomplete, + oversized, ambiguous, unsafe-integer, or ordinary command-failure evidence returns undefined; + `AbortError` propagates. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-windows-compatibility-detection.test.ts` — PASS, one file, + 26 passed / one declared macOS skip in 572 ms Vitest, 16.45 seconds wall under concurrent local + load, 132,448,256-byte maximum RSS, zero swaps. It covers Windows 10 x64, Server 2022 x64, + Windows 11 arm64, unordered evidence, every strict rejection class above, ordinary failure, + cancellation, exact exec options, encoding, registry view, bounded exact-binary version capture, + and forbidden runtime/network dependency absence. The skipped case executes the exact encoded + probe under native Windows PowerShell, requires exit 0, and requires exactly six bounded output + lines; only native Windows CI may satisfy it. +- Workflow-oracle command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, one file / seven tests + in 1.04 seconds Vitest, 3.24 seconds wall, 132,759,552-byte maximum RSS, zero swaps. The suite now + occurs exactly once in both POSIX and PowerShell native contract commands. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1` plus the Windows purpose suite, libc, kernel, Darwin version, Darwin translation, + libstdc++, selector, platform detection, and workflow-oracle suites — PASS, nine files, 168 passed + / one declared macOS skip in 5.38 seconds Vitest, 7.61 seconds wall, 138,067,968-byte maximum RSS, + zero swaps. +- Broader relay command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS, 38 files passed / three + declared skipped files, 487 tests passed / four declared platform skips in 42.76 seconds Vitest, + 46.28 seconds wall, 251,150,336-byte maximum RSS, zero swaps. The unchanged stale-lock diagnostic + and local Node 26 `module.register()` deprecation warning remain expected. +- Release-script command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 --silent=true +config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 30.55 seconds Vitest, + 33.07 seconds wall, 190,562,304-byte maximum RSS, zero swaps. +- Static gates: `pnpm typecheck`; `pnpm lint`; explicit `pnpm run check:max-lines-ratchet`; + targeted `pnpm exec oxlint`; targeted `pnpm exec oxfmt --check`; `git diff --check`; and the + protected-resolver empty-diff assertion pass. Full lint includes switch exhaustiveness, 41 + reliability gates, 355 grandfathered max-lines entries with no new bypass, bundled-skill checks, + 9,837 localization references and locale parity, and zero localization coverage candidates. Its + 26 warnings are in untouched files. Both new purpose-named files stay under 300 lines. +- Boundary/residual gaps: the module has no import outside its purpose test and no production caller. + Local macOS does not execute native Windows PowerShell, prove the hosted runner has complete + `sshd.exe` evidence, or establish live Windows SSH/OpenSSH-service behavior. Exact-head all-six + Node 24 client proof is mandatory next. Full host composition, process translation policy, + semantic transfer capability, Electron/startup, cache acquisition, transfer/install, settings, + fallback wiring, tuple enablement, release publication, defaults, and SignPath remain absent; + legacy remains the sole production/default path. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/src/main/ssh/ssh-relay-windows-compatibility-detection.test.ts b/src/main/ssh/ssh-relay-windows-compatibility-detection.test.ts new file mode 100644 index 00000000000..f9f29e960e3 --- /dev/null +++ b/src/main/ssh/ssh-relay-windows-compatibility-detection.test.ts @@ -0,0 +1,192 @@ +import { spawnSync } from 'node:child_process' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { SshConnection } from './ssh-connection' + +const execCommandMock = vi.hoisted(() => vi.fn()) + +vi.mock('./ssh-relay-deploy-helpers', () => ({ + execCommand: execCommandMock +})) + +const { detectSshRelayWindowsCompatibility } = + await import('./ssh-relay-windows-compatibility-detection') + +const conn = {} as SshConnection +const marker = '__ORCA_SSH_RELAY_WINDOWS_COMPATIBILITY__' + +function segment( + fields: string[] = [ + 'build=19045', + 'openSshVersion=8.1p1', + 'powerShellVersion=5.1.19041.5608', + 'dotNetFrameworkRelease=528040' + ] +): string { + return [`${marker} BEGIN`, ...fields, `${marker} END`].join('\n') +} + +function decodePowerShellCommand(command: string): { encoded: string; script: string } { + const encoded = command.match(/-EncodedCommand\s+([A-Za-z0-9+/=]+)/)?.[1] + if (!encoded) { + throw new Error('missing encoded PowerShell command') + } + return { encoded, script: Buffer.from(encoded, 'base64').toString('utf16le') } +} + +describe('detectSshRelayWindowsCompatibility', () => { + beforeEach(() => { + execCommandMock.mockReset() + }) + + it.each([ + { + label: 'Windows 10 x64 floor', + output: segment(), + expected: { + build: 19045, + openSshVersion: '8.1p1', + powerShellVersion: '5.1.19041.5608', + dotNetFrameworkRelease: 528040 + } + }, + { + label: 'Windows Server 2022 x64', + output: segment([ + 'dotNetFrameworkRelease=528449', + 'powerShellVersion=5.1.20348.2849', + 'openSshVersion=9.5p1', + 'build=20348' + ]), + expected: { + build: 20348, + openSshVersion: '9.5p1', + powerShellVersion: '5.1.20348.2849', + dotNetFrameworkRelease: 528449 + } + }, + { + label: 'Windows 11 arm64 floor', + output: segment([ + 'build=26100', + 'openSshVersion=9.5p1', + 'powerShellVersion=5.1.26100.2161', + 'dotNetFrameworkRelease=533320' + ]), + expected: { + build: 26100, + openSshVersion: '9.5p1', + powerShellVersion: '5.1.26100.2161', + dotNetFrameworkRelease: 533320 + } + } + ])('returns complete strict evidence for $label', async ({ output, expected }) => { + execCommandMock.mockResolvedValueOnce(`startup noise\n${output}\ntrailing noise`) + + await expect(detectSshRelayWindowsCompatibility(conn)).resolves.toEqual(expected) + }) + + it.each([ + ['', 'empty output'], + [`build=19045\nopenSshVersion=8.1p1`, 'unmarked fields'], + [`${marker} BEGIN\nbuild=19045`, 'unterminated segment'], + [segment().replace(`${marker} END`, ''), 'missing end marker'], + [segment().replace('build=19045\n', ''), 'missing field'], + [segment().replace('build=19045', 'build='), 'empty field'], + [segment().replace('build=19045', 'build=19045\nbuild=20348'), 'duplicate field'], + [segment().replace('build=19045', 'futureField=1'), 'unknown field'], + [segment().replace('build=19045', 'build=19.045'), 'malformed build'], + [segment().replace('build=19045', 'build=9007199254740992'), 'unsafe build integer'], + [segment().replace('openSshVersion=8.1p1', 'openSshVersion=OpenSSH_8.1p1'), 'noisy SSH'], + [segment().replace('openSshVersion=8.1p1', 'openSshVersion=8.1'), 'malformed SSH'], + [segment().replace('powerShellVersion=5.1.19041.5608', 'powerShellVersion=5'), 'short PS'], + [ + segment().replace('powerShellVersion=5.1.19041.5608', 'powerShellVersion=5.1.2.3.4'), + 'long PS' + ], + [ + segment().replace('dotNetFrameworkRelease=528040', 'dotNetFrameworkRelease=-1'), + 'negative .NET release' + ], + [segment(Array.from({ length: 5 }, (_, index) => `unknown${index}=1`)), 'too many lines'], + [segment().replace('build=19045', `build=${'1'.repeat(129)}`), 'oversized line'], + [`${segment()}\n${segment()}`, 'duplicate segment'], + [`${marker} BEGIN\n${segment()}\n${marker} END`, 'nested segment'], + [`startup${marker} BEGIN\n${segment().split('\n').slice(1).join('\n')}`, 'joined marker'] + ])('returns unknown for %s', async (output) => { + execCommandMock.mockResolvedValueOnce(output) + + await expect(detectSshRelayWindowsCompatibility(conn)).resolves.toBeUndefined() + }) + + it('classifies an unavailable probe as unknown', async () => { + execCommandMock.mockRejectedValueOnce(new Error('remote PowerShell unavailable')) + + await expect(detectSshRelayWindowsCompatibility(conn)).resolves.toBeUndefined() + }) + + it('propagates cancellation through one signal-qualified bounded probe', async () => { + const signal = new AbortController().signal + const abortError = Object.assign(new Error('cancelled'), { name: 'AbortError' }) + execCommandMock.mockRejectedValueOnce(abortError) + + await expect(detectSshRelayWindowsCompatibility(conn, { signal })).rejects.toBe(abortError) + expect(execCommandMock).toHaveBeenCalledTimes(1) + expect(execCommandMock).toHaveBeenCalledWith(conn, expect.any(String), { + signal, + timeoutMs: 15_000, + wrapCommand: false + }) + }) + + it('constructs one encoded noninteractive probe with bounded native-only acquisition', async () => { + execCommandMock.mockResolvedValueOnce(segment()) + + await detectSshRelayWindowsCompatibility(conn) + + const command = execCommandMock.mock.calls[0]?.[1] ?? '' + expect(command).toMatch( + /^powershell\.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand /u + ) + const { script } = decodePowerShellCommand(command) + expect(script).toContain('[Microsoft.Win32.RegistryView]::Registry64') + expect(script).toContain('CurrentBuildNumber') + expect(script).toContain('NDP\\v4\\Full') + expect(script).toContain("Get-Command -Name 'sshd.exe' -CommandType Application") + expect(script).toContain('& $sshdPath -V') + expect(script).toContain('Select-Object -First 8') + expect(script).toContain('$versionOutput.Length -le 4096') + expect(script).toContain('$matches.Count -eq 1') + expect(script).toContain('$PSVersionTable.PSVersion') + expect(script).toContain(`${marker} BEGIN`) + expect(script).toContain(`${marker} END`) + expect(script).not.toMatch( + /\b(?:node|npm|python|perl|tar|sha256sum|shasum|git|github|invoke-webrequest|curl)\b/iu + ) + }) + + it.runIf(process.platform === 'win32')( + 'executes the encoded probe under native Windows PowerShell with bounded marker output', + async () => { + execCommandMock.mockResolvedValueOnce(segment()) + await detectSshRelayWindowsCompatibility(conn) + const { encoded } = decodePowerShellCommand(execCommandMock.mock.calls[0]?.[1] ?? '') + + const result = spawnSync( + 'powershell.exe', + ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded], + { encoding: 'utf8', timeout: 15_000 } + ) + + expect({ status: result.status, error: result.error, stderr: result.stderr }).toEqual({ + status: 0, + error: undefined, + stderr: '' + }) + const outputLines = result.stdout.split(/\r?\n/u).filter(Boolean) + expect(outputLines[0]).toBe(`${marker} BEGIN`) + expect(outputLines.at(-1)).toBe(`${marker} END`) + expect(outputLines).toHaveLength(6) + expect(outputLines.every((line) => line.length <= 128)).toBe(true) + } + ) +}) diff --git a/src/main/ssh/ssh-relay-windows-compatibility-detection.ts b/src/main/ssh/ssh-relay-windows-compatibility-detection.ts new file mode 100644 index 00000000000..3f3b1e62857 --- /dev/null +++ b/src/main/ssh/ssh-relay-windows-compatibility-detection.ts @@ -0,0 +1,201 @@ +import { + getProcessOutputFields, + iterateProcessOutputLines +} from '../../shared/process-output-field-scanner' +import type { SshConnection } from './ssh-connection' +import { powerShellCommand } from './ssh-remote-powershell' +import type { SshRelayWindowsHostEvidence } from './ssh-relay-artifact-selector' +import { execCommand } from './ssh-relay-deploy-helpers' + +export type SshRelayWindowsCompatibilityEvidence = Required< + Pick< + SshRelayWindowsHostEvidence, + 'build' | 'openSshVersion' | 'powerShellVersion' | 'dotNetFrameworkRelease' + > +> + +type EvidenceField = keyof SshRelayWindowsCompatibilityEvidence +type ProbeMarker = 'BEGIN' | 'END' + +const PROBE_MARKER = '__ORCA_SSH_RELAY_WINDOWS_COMPATIBILITY__' +const PROBE_TIMEOUT_MS = 15_000 +const MAX_FIELD_LINE_CHARS = 128 +const EXPECTED_FIELD_COUNT = 4 + +// Why: registry values must come from the OS view that owns Windows compatibility contracts, +// independent of the architecture of the PowerShell process launched by sshd. +const PROBE_SCRIPT = [ + "$build = ''", + "$openSshVersion = ''", + "$powerShellVersion = ''", + "$dotNetFrameworkRelease = ''", + 'try {', + ' $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Registry64)', + ' try {', + " $currentVersionKey = $baseKey.OpenSubKey('SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion')", + ' if ($null -ne $currentVersionKey) {', + ' try {', + " $candidate = [string]$currentVersionKey.GetValue('CurrentBuildNumber')", + " if ($candidate -match '^[1-9][0-9]{0,15}$') { $build = $candidate }", + ' } finally { $currentVersionKey.Dispose() }', + ' }', + " $frameworkKey = $baseKey.OpenSubKey('SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full')", + ' if ($null -ne $frameworkKey) {', + ' try {', + " $candidate = [string]$frameworkKey.GetValue('Release')", + " if ($candidate -match '^[1-9][0-9]{0,15}$') { $dotNetFrameworkRelease = $candidate }", + ' } finally { $frameworkKey.Dispose() }', + ' }', + ' } finally { $baseKey.Dispose() }', + '} catch {}', + 'try {', + ' $candidate = [string]$PSVersionTable.PSVersion', + " if ($candidate -match '^[0-9]{1,10}(?:\\.[0-9]{1,10}){1,3}$') { $powerShellVersion = $candidate }", + '} catch {}', + 'try {', + " $sshd = Get-Command -Name 'sshd.exe' -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1", + ' if ($null -ne $sshd -and $sshd.Source) {', + ' $sshdPath = [string]$sshd.Source', + ' $versionOutput = & $sshdPath -V 2>&1 | Select-Object -First 8 | Out-String', + ' if ($versionOutput.Length -le 4096) {', + " $matches = [regex]::Matches($versionOutput, '(?:^|[^0-9A-Za-z])OpenSSH_for_Windows_([0-9]{1,10}\\.[0-9]{1,10}p[0-9]{1,10})(?:[^0-9A-Za-z]|$)')", + ' if ($matches.Count -eq 1) { $openSshVersion = $matches[0].Groups[1].Value }', + ' }', + ' }', + '} catch {}', + // Why: emit only normalized bounded fields; native and registry command output is never relayed. + `Write-Output ("\`n${PROBE_MARKER} BEGIN")`, + "Write-Output ('build=' + $build)", + "Write-Output ('openSshVersion=' + $openSshVersion)", + "Write-Output ('powerShellVersion=' + $powerShellVersion)", + "Write-Output ('dotNetFrameworkRelease=' + $dotNetFrameworkRelease)", + `Write-Output '${PROBE_MARKER} END'` +].join('\n') + +function parseMarker(line: string): ProbeMarker | null { + const fields = getProcessOutputFields(line, 3) + if (fields.length !== 2 || fields[0] !== PROBE_MARKER) { + return null + } + return fields[1] === 'BEGIN' || fields[1] === 'END' ? fields[1] : null +} + +function parsePositiveSafeInteger(value: string): number | null { + if (!/^[1-9]\d{0,15}$/u.test(value)) { + return null + } + const parsed = Number(value) + return Number.isSafeInteger(parsed) ? parsed : null +} + +function parseNumericVersion(value: string, pattern: RegExp): string | null { + if (!pattern.test(value)) { + return null + } + return value.split(/[.p]/u).every((part) => Number.isSafeInteger(Number(part))) ? value : null +} + +function parseField(line: string): { field: EvidenceField; value: string | number } | undefined { + const match = /^(build|openSshVersion|powerShellVersion|dotNetFrameworkRelease)=(.*)$/u.exec(line) + if (!match) { + return undefined + } + const field = match[1] as EvidenceField + const rawValue = match[2] + switch (field) { + case 'build': + case 'dotNetFrameworkRelease': { + const value = parsePositiveSafeInteger(rawValue) + return value === null ? undefined : { field, value } + } + case 'openSshVersion': { + const value = parseNumericVersion(rawValue, /^\d+\.\d+p\d+$/u) + return value === null ? undefined : { field, value } + } + case 'powerShellVersion': { + const value = parseNumericVersion(rawValue, /^\d+(?:\.\d+){1,3}$/u) + return value === null ? undefined : { field, value } + } + } +} + +function parseWindowsCompatibilityEvidence( + output: string +): SshRelayWindowsCompatibilityEvidence | undefined { + let active = false + let complete = false + let fieldCount = 0 + let invalid = false + const evidence: Partial = {} + + for (const line of iterateProcessOutputLines(output)) { + const marker = parseMarker(line) + if (marker === 'BEGIN') { + if (active || complete) { + invalid = true + } + active = true + continue + } + if (marker === 'END') { + if (!active || complete) { + invalid = true + } + active = false + complete = true + continue + } + if (!active) { + continue + } + fieldCount += 1 + if (line.length > MAX_FIELD_LINE_CHARS || fieldCount > EXPECTED_FIELD_COUNT) { + invalid = true + continue + } + const parsed = parseField(line) + if (!parsed || evidence[parsed.field] !== undefined) { + invalid = true + continue + } + Object.assign(evidence, { [parsed.field]: parsed.value }) + } + + if ( + invalid || + active || + !complete || + fieldCount !== EXPECTED_FIELD_COUNT || + evidence.build === undefined || + evidence.openSshVersion === undefined || + evidence.powerShellVersion === undefined || + evidence.dotNetFrameworkRelease === undefined + ) { + return undefined + } + return evidence as SshRelayWindowsCompatibilityEvidence +} + +function isAbortError(error: unknown): boolean { + return error instanceof Error && error.name === 'AbortError' +} + +export async function detectSshRelayWindowsCompatibility( + conn: SshConnection, + { signal }: { signal?: AbortSignal } = {} +): Promise { + try { + const output = await execCommand(conn, powerShellCommand(PROBE_SCRIPT), { + signal, + timeoutMs: PROBE_TIMEOUT_MS, + wrapCommand: false + }) + return parseWindowsCompatibilityEvidence(output) + } catch (error) { + // Why: cancellation must settle bootstrap work rather than becoming compatibility evidence. + if (isAbortError(error)) { + throw error + } + return undefined + } +} From c489ee9f712c275b755fa33d2bf72f99253b53b2 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 06:36:15 -0400 Subject: [PATCH 202/267] test: accept bounded PowerShell startup progress Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 15 ++++- ...github-release-implementation-checklist.md | 64 +++++++++++++++++-- ...ay-windows-compatibility-detection.test.ts | 46 ++++++++++++- 3 files changed, 113 insertions(+), 12 deletions(-) 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 b37d243bcf3..87d8b6b78eb 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 @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — prove the locally-green, -disconnected Windows build/OpenSSH/PowerShell/.NET compatibility detector on exact-head native -runners, 2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — run replacement +exact-head native proof for the corrected Windows first-use progress assertion, 2026-07-15, Codex +implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -256,6 +256,15 @@ relay cases, and 280/280 release-script cases pass. Typecheck, full lint, reliab localization, format, diff, and protected-resolver gates pass. The module remains disconnected; native Windows execution and all-six Node 24 proof are the next gate, while live SSH, composition, settings, fallback wiring, tuple enablement, publication, defaults, and SignPath remain absent. +`E-M5-WINDOWS-COMPATIBILITY-DETECTION-CI-RED-001` records the first native x64 gate at exact head +`30bf908f2`: 84 files/644 tests pass, including every detector parser/command case, and only the +native execution assertion fails. The encoded probe exits 0 with the exact bounded stdout; hosted +Windows adds bounded first-use progress CLIXML on stderr. The narrow correction accepts only empty +stderr or that bounded progress shape with no error record; production code remains unchanged. +`E-M5-WINDOWS-COMPATIBILITY-DETECTION-CORRECTION-LOCAL-001` passes 31 purpose cases plus the seven- +case workflow oracle, with only native execution skipped on macOS. Five explicit cases accept empty/ +bounded-known progress and reject unknown, error, or oversized stderr. Typecheck, targeted lint/ +format, max-lines, diff, and protected-resolver gates pass; replacement native CI is mandatory. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 7bb157b4b75..4f84bd00532 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: prove the locally-green disconnected Windows build/OpenSSH/PowerShell/.NET compatibility detector on exact-head native runners**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **WINDOWS COMPATIBILITY LOCAL GREEN / EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M5-WINDOWS-COMPATIBILITY-DETECTION-LOCAL-001` proves the bounded strict detector, purpose cases, workflow-family oracle, broader relay/release regressions, and static gates locally. The native Windows execution case remains a declared macOS skip and must pass on Windows x64 and arm64 runners before this package closes. No host composer, product caller, setting, transfer/install, fallback wiring, tuple enablement, publication, or default behavior is connected. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: run replacement exact-head native proof for the corrected Windows first-use progress assertion**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **WINDOWS COMPATIBILITY CORRECTION LOCAL GREEN / REPLACEMENT CI NEXT — 2026-07-15, Codex implementation owner** — `E-M5-WINDOWS-COMPATIBILITY-DETECTION-CORRECTION-LOCAL-001` accepts only empty or bounded known first-use progress stderr without an error record and rejects unknown, error, or oversized stderr. Purpose/workflow and static gates pass locally; require replacement native x64/arm64 plus all-six proof. Production code and every product/default path remain unchanged. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -147,11 +147,11 @@ same change as the work it records. local gates, and all-six native Node 24 proof are green under E-M5-ARTIFACT-CACHE-ROOT-LOCAL-RED-001, E-M5-ARTIFACT-CACHE-ROOT-LOCAL-001, and E-M5-ARTIFACT-CACHE-ROOT-CI-001. No Electron or product caller exists. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — exact-head native proof - for the disconnected Windows build/OpenSSH/PowerShell/.NET compatibility detector under - E-M5-WINDOWS-COMPATIBILITY-DETECTION-LOCAL-001. Native Windows PowerShell execution must pass on - x64 and arm64; keep host composition, live SSH, transfer/install, settings, fallback wiring, - tuple enablement, publication, defaults, and SignPath separately gated. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — replacement exact-head + native proof for the locally-green test-only correction under + E-M5-WINDOWS-COMPATIBILITY-DETECTION-CORRECTION-LOCAL-001. Require native Windows x64 and arm64 + plus all-six client proof; keep host composition, live SSH, transfer/install, settings, fallback + wiring, tuple enablement, publication, defaults, and SignPath separately gated. - 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 @@ -15822,6 +15822,56 @@ config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 3 fallback wiring, tuple enablement, release publication, defaults, and SignPath remain absent; legacy remains the sole production/default path. +### E-M5-WINDOWS-COMPATIBILITY-DETECTION-CI-RED-001 — hosted first-use progress is not probe failure + +- Date/owner/source: 2026-07-15, Codex implementation owner; exact implementation head + `30bf908f2477d3f4b568811469e04f2009c13b28`, SSH Relay Runtime Artifacts run + [29491051007](https://github.com/stablyai/orca/actions/runs/29491051007), Windows x64 job + [87597009005](https://github.com/stablyai/orca/actions/runs/29491051007/job/87597009005), hosted + Windows Server 2022 / Node 24.18.0. +- Passing evidence: the runtime contract reaches 84 passed files, 644 passed tests, and 14 declared + skips. Every parser, strict-rejection, cancellation, command-construction, and dependency-absence + detector case passes. The native encoded probe itself completes in 8,035 ms with process status + 0, no spawn error, and the exact six bounded stdout lines required by the test. +- Sole failure: one native-execution assertion requires `stderr === ''`. Hosted Windows PowerShell + emits a bounded `#< CLIXML` progress stream containing only three `S="progress"` records for + `Preparing modules for first use.` before the encoded script starts. It contains no `S="Error"` + record. The production `execCommand` success path returns stdout on exit 0 and does not parse + stderr as detector evidence, so this is a purpose-test portability defect rather than a + production detector or runtime failure. +- Correction contract: retain process status 0, no spawn error, exact six-line/128-character stdout, + and the existing 15-second process bound. Accept stderr only when empty or at most 4,096 + characters of first-use CLIXML containing the known progress record and no PowerShell error + record. Unknown, oversized, or error CLIXML must still fail the native test. Production code, + workflow composition, and every product/default path remain unchanged. Replacement native x64 + and arm64 plus all-six Node 24 proof is mandatory. + +### E-M5-WINDOWS-COMPATIBILITY-DETECTION-CORRECTION-LOCAL-001 — bounded known progress only + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0 / + pnpm 10.24.0, test-only correction atop exact implementation head `30bf908f2`. Production source + and native workflow commands are unchanged. +- Correction: `isExpectedPowerShellStartupStderr` accepts empty stderr or at most 4,096 characters + starting with `#< CLIXML`, containing the observed `S="progress"` and `Preparing modules for first +use.` record, and containing no `S="Error"` record. Five explicit local cases accept empty/known + progress and reject unknown text, error CLIXML, and oversized progress. The native case retains + process status 0, no spawn error, exact six stdout lines, 128-character line bounds, and the + 15-second process timeout. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-windows-compatibility-detection.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, two files, 38 passed / one declared + macOS skip in 651 ms Vitest, 1.94 seconds wall, 133,087,232-byte maximum RSS, zero swaps. This is + 31 purpose assertions plus the seven-case native-workflow oracle. +- Static commands: `pnpm typecheck`; targeted `pnpm exec oxlint`; targeted `pnpm exec oxfmt --check`; + `pnpm run check:max-lines-ratchet`; `git diff --check`; and the protected-resolver empty-diff + assertion pass. The expected local Node-24 engine warning remains; exact Node 24 is the native CI + gate. +- Boundary: this corrects only the native test's handling of hosted PowerShell startup progress. It + does not relax the production parser, change the detector command, add a product import/caller, or + alter any legacy/default behavior. Replacement x64/arm64 and all-six exact-head proof remains + mandatory; live SSH, host composition, settings/fallback, tuple enablement, publication, and + SignPath remain absent. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/src/main/ssh/ssh-relay-windows-compatibility-detection.test.ts b/src/main/ssh/ssh-relay-windows-compatibility-detection.test.ts index f9f29e960e3..bc16b6f735c 100644 --- a/src/main/ssh/ssh-relay-windows-compatibility-detection.test.ts +++ b/src/main/ssh/ssh-relay-windows-compatibility-detection.test.ts @@ -33,6 +33,20 @@ function decodePowerShellCommand(command: string): { encoded: string; script: st return { encoded, script: Buffer.from(encoded, 'base64').toString('utf16le') } } +function isExpectedPowerShellStartupStderr(stderr: string): boolean { + if (stderr === '') { + return true + } + // Why: hosted Windows emits bounded first-use progress CLIXML before the encoded script starts. + return ( + stderr.length <= 4096 && + stderr.startsWith('#< CLIXML') && + stderr.includes('S="progress"') && + stderr.includes('Preparing modules for first use.') && + !stderr.includes('S="Error"') + ) +} + describe('detectSshRelayWindowsCompatibility', () => { beforeEach(() => { execCommandMock.mockReset() @@ -164,6 +178,30 @@ describe('detectSshRelayWindowsCompatibility', () => { ) }) + it.each([ + { label: 'empty', stderr: '', expected: true }, + { + label: 'bounded first-use progress', + stderr: + '#< CLIXML\r\nPreparing modules for first use.', + expected: true + }, + { label: 'unknown', stderr: 'unexpected stderr', expected: false }, + { + label: 'error CLIXML', + stderr: + '#< CLIXML\r\nPreparing modules for first use.', + expected: false + }, + { + label: 'oversized progress', + stderr: `#< CLIXML\r\nPreparing modules for first use.${'x'.repeat(4096)}`, + expected: false + } + ])('classifies $label PowerShell startup stderr', ({ stderr, expected }) => { + expect(isExpectedPowerShellStartupStderr(stderr)).toBe(expected) + }) + it.runIf(process.platform === 'win32')( 'executes the encoded probe under native Windows PowerShell with bounded marker output', async () => { @@ -177,10 +215,14 @@ describe('detectSshRelayWindowsCompatibility', () => { { encoding: 'utf8', timeout: 15_000 } ) - expect({ status: result.status, error: result.error, stderr: result.stderr }).toEqual({ + expect({ + status: result.status, + error: result.error, + expectedStartupStderr: isExpectedPowerShellStartupStderr(result.stderr) + }).toEqual({ status: 0, error: undefined, - stderr: '' + expectedStartupStderr: true }) const outputLines = result.stdout.split(/\r?\n/u).filter(Boolean) expect(outputLines[0]).toBe(`${marker} BEGIN`) From cfe56d0fdb356a468d19dd67ff7eb01fc48955c7 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 06:54:33 -0400 Subject: [PATCH 203/267] docs: record Windows compatibility CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 10 ++- ...github-release-implementation-checklist.md | 68 +++++++++++++++++-- 2 files changed, 68 insertions(+), 10 deletions(-) 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 87d8b6b78eb..6e8f935c9d6 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 @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — run replacement -exact-head native proof for the corrected Windows first-use progress assertion, 2026-07-15, Codex -implementation owner.** +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — audit the smallest +disconnected full host-evidence composition boundary after native acceptance of every individual +compatibility detector, 2026-07-15, Codex implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -265,6 +265,10 @@ stderr or that bounded progress shape with no error record; production code rema case workflow oracle, with only native execution skipped on macOS. Five explicit cases accept empty/ bounded-known progress and reject unknown, error, or oversized stderr. Typecheck, targeted lint/ format, max-lines, diff, and protected-resolver gates pass; replacement native CI is mandatory. +`E-M5-WINDOWS-COMPATIBILITY-DETECTION-CI-001` closes exact head `c489ee9f7`: all six primary native +clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. +Both Windows architectures pass all 32 detector cases under native Windows PowerShell. Windows +arm64 completes full runtime proof and retains only the declared hosted build-26200 rejection against 26100. No live SSH, composer, product caller, setting/fallback, tuple, publication, or default exists. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 4f84bd00532..c01b7e9fb07 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: run replacement exact-head native proof for the corrected Windows first-use progress assertion**. Keep every capability disconnected from host-evidence composition and every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **WINDOWS COMPATIBILITY CORRECTION LOCAL GREEN / REPLACEMENT CI NEXT — 2026-07-15, Codex implementation owner** — `E-M5-WINDOWS-COMPATIBILITY-DETECTION-CORRECTION-LOCAL-001` accepts only empty or bounded known first-use progress stderr without an error record and rejects unknown, error, or oversized stderr. Purpose/workflow and static gates pass locally; require replacement native x64/arm64 plus all-six proof. Production code and every product/default path remain unchanged. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the smallest disconnected full host-evidence composition boundary after native acceptance of every individual detector**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **WINDOWS COMPATIBILITY CI CLOSED / HOST COMPOSITION AUDIT NEXT — 2026-07-15, Codex implementation owner** — `E-M5-WINDOWS-COMPATIBILITY-DETECTION-CI-001` proves both native Windows architectures pass the strict detector, all six primary clients and both Linux supplements pass, Windows x64 baseline passes, and Windows arm64 retains only the declared build-26200/26100 runner mismatch after full runtime proof. Audit only the next disconnected composition boundary; do not add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -147,11 +147,11 @@ same change as the work it records. local gates, and all-six native Node 24 proof are green under E-M5-ARTIFACT-CACHE-ROOT-LOCAL-RED-001, E-M5-ARTIFACT-CACHE-ROOT-LOCAL-001, and E-M5-ARTIFACT-CACHE-ROOT-CI-001. No Electron or product caller exists. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — replacement exact-head - native proof for the locally-green test-only correction under - E-M5-WINDOWS-COMPATIBILITY-DETECTION-CORRECTION-LOCAL-001. Require native Windows x64 and arm64 - plus all-six client proof; keep host composition, live SSH, transfer/install, settings, fallback - wiring, tuple enablement, publication, defaults, and SignPath separately gated. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit the smallest + disconnected full host-evidence composition boundary after + E-M5-WINDOWS-COMPATIBILITY-DETECTION-CI-001 closes every individual detector on native clients. + Keep live SSH, transfer/install, settings, fallback wiring, tuple enablement, publication, + defaults, and SignPath separately gated. - 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 @@ -15872,6 +15872,60 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, two files, 38 pass mandatory; live SSH, host composition, settings/fallback, tuple enablement, publication, and SignPath remain absent. +### E-M5-WINDOWS-COMPATIBILITY-DETECTION-CI-001 — both native Windows detector contracts close + +- Date/owner/source: 2026-07-15, Codex implementation owner; exact correction head + `c489ee9f712c275b755fa33d2bf72f99253b53b2` on draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). Replacement SSH Relay Runtime Artifacts run + [29491431906](https://github.com/stablyai/orca/actions/runs/29491431906) checks out that exact SHA + and uses Node 24.18.0. +- All six primary native jobs pass: Windows x64 + [87598281195](https://github.com/stablyai/orca/actions/runs/29491431906/job/87598281195), Windows + arm64 [87598281158](https://github.com/stablyai/orca/actions/runs/29491431906/job/87598281158), + Darwin arm64 [87598281186](https://github.com/stablyai/orca/actions/runs/29491431906/job/87598281186), + Darwin x64 [87598281198](https://github.com/stablyai/orca/actions/runs/29491431906/job/87598281198), + Linux arm64 [87598281211](https://github.com/stablyai/orca/actions/runs/29491431906/job/87598281211), + and Linux x64 [87598281215](https://github.com/stablyai/orca/actions/runs/29491431906/job/87598281215). +- Native detector proof: each Windows job passes the 32-case + `ssh-relay-windows-compatibility-detection` suite under native Windows PowerShell — x64 in 2,213 + ms and arm64 in 3,610 ms. Each Windows native contract passes 85 files / 650 tests with 14 + declared skips. Each POSIX native contract passes 84 files / 659 tests with the one native-Windows + execution skip. This closes encoded syntax/execution, registry access, `sshd.exe -V`, bounded + first-use progress handling, strict parsing/rejection, cancellation, and command-construction on + both Windows architectures; it is still client/native-command evidence rather than live SSH. +- Both Linux oldest-userland supplements pass: x64 + [87599209993](https://github.com/stablyai/orca/actions/runs/29491431906/job/87599209993) and arm64 + [87599209998](https://github.com/stablyai/orca/actions/runs/29491431906/job/87599209998). The Windows + x64 oldest-baseline job + [87600150054](https://github.com/stablyai/orca/actions/runs/29491431906/job/87600150054) passes. +- The Windows arm64 oldest-baseline job + [87600150042](https://github.com/stablyai/orca/actions/runs/29491431906/job/87600150042) verifies the + full 60-entry / 42-file / 85,213,511-expanded-byte tree and content identity, Node v24.18.0 ABI + 137, PTY exit/resize, five watcher events, and two-second resource settlement. Smoke takes + 6,101.9951 ms at 50,077,696-byte RSS and complete verification takes 8,303.8299 ms. Its only + rejection is the previously declared hosted OS build 26200 against required build 26100; platform + and arm64 architecture checks pass and no new residual gap is reported. +- Full-size extraction stays at 1,319.09–3,574.53 ms and 31,191,040–47,452,160 incremental RSS + bytes across the six primary clients. Cold immutable-cache publication stays at + 1,789.86–4,013.22 ms and 32,522,240–50,397,184 incremental RSS bytes; warm lookup stays at + 191.14–589.48 ms and 0–6,123,520 incremental RSS bytes. Retention stays at 12.53–46.41 ms and + eviction at 32.54–91.28 ms. Every measurement remains within its existing budget. +- Adjacent exact-head checks pass: PR Checks run + [29491430511](https://github.com/stablyai/orca/actions/runs/29491430511), job + [87598223319](https://github.com/stablyai/orca/actions/runs/29491430511/job/87598223319); Golden E2E + run [29491430552](https://github.com/stablyai/orca/actions/runs/29491430552), Linux job + [87598223336](https://github.com/stablyai/orca/actions/runs/29491430552/job/87598223336) and macOS + job [87598223374](https://github.com/stablyai/orca/actions/runs/29491430552/job/87598223374); + computer-use run [29491430223](https://github.com/stablyai/orca/actions/runs/29491430223), Windows + job [87598222661](https://github.com/stablyai/orca/actions/runs/29491430223/job/87598222661) and + Ubuntu job [87598222670](https://github.com/stablyai/orca/actions/runs/29491430223/job/87598222670). +- Boundary/residual gaps: the detector remains imported only by its purpose test, with no host + composer or product caller. Hosted native PowerShell does not prove live Windows OpenSSH service, + remote startup noise/channel behavior, system-SSH/SFTP transfer capability, or representative + remote Windows versions. Full host composition, live SSH, transfer/install, settings/fallback, + tuple enablement, release publication, defaults, and SignPath remain absent. Legacy remains the + sole production/default path and every tuple stays disabled. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From a7fd19de5f2d0b8b7243259fa0a2ea669b5323d7 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 07:07:51 -0400 Subject: [PATCH 204/267] feat: compose SSH relay host evidence Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 25 +- ...github-release-implementation-checklist.md | 123 +++++++++- .../ssh-relay-host-evidence-detection.test.ts | 229 ++++++++++++++++++ .../ssh/ssh-relay-host-evidence-detection.ts | 92 +++++++ 6 files changed, 458 insertions(+), 14 deletions(-) create mode 100644 src/main/ssh/ssh-relay-host-evidence-detection.test.ts create mode 100644 src/main/ssh/ssh-relay-host-evidence-detection.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 4541f8301c2..3cf63d7a535 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -264,6 +264,7 @@ jobs: src/main/ssh/ssh-relay-artifact-selector.test.ts \ src/main/ssh/ssh-relay-libc-detection.test.ts \ src/main/ssh/ssh-relay-linux-kernel-detection.test.ts \ + src/main/ssh/ssh-relay-host-evidence-detection.test.ts \ src/main/ssh/ssh-relay-darwin-version-detection.test.ts \ src/main/ssh/ssh-relay-darwin-translation-detection.test.ts \ src/main/ssh/ssh-relay-linux-libstdcxx-detection.test.ts \ @@ -815,6 +816,7 @@ jobs: src/main/ssh/ssh-relay-artifact-selector.test.ts ` src/main/ssh/ssh-relay-libc-detection.test.ts ` src/main/ssh/ssh-relay-linux-kernel-detection.test.ts ` + src/main/ssh/ssh-relay-host-evidence-detection.test.ts ` src/main/ssh/ssh-relay-darwin-version-detection.test.ts ` src/main/ssh/ssh-relay-darwin-translation-detection.test.ts ` src/main/ssh/ssh-relay-linux-libstdcxx-detection.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index e5055c9dd7a..81348800f38 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -122,6 +122,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-artifact-selector.test.ts', 'ssh-relay-libc-detection.test.ts', 'ssh-relay-linux-kernel-detection.test.ts', + 'ssh-relay-host-evidence-detection.test.ts', 'ssh-relay-darwin-version-detection.test.ts', 'ssh-relay-darwin-translation-detection.test.ts', 'ssh-relay-linux-libstdcxx-detection.test.ts', 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 6e8f935c9d6..08c622088dc 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 @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — audit the smallest -disconnected full host-evidence composition boundary after native acceptance of every individual -compatibility detector, 2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — disconnected host- +evidence composition is locally green; exact-head all-six native CI is next, 2026-07-15, Codex +implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -269,6 +269,25 @@ format, max-lines, diff, and protected-resolver gates pass; replacement native C clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. Both Windows architectures pass all 32 detector cases under native Windows PowerShell. Windows arm64 completes full runtime proof and retains only the declared hosted build-26200 rejection against 26100. No live SSH, composer, product caller, setting/fallback, tuple, publication, or default exists. +`E-M5-HOST-EVIDENCE-COMPOSITION-AUDIT-001` fixes the next disconnected boundary: consume an explicit +canonical detected platform, SSH connection, and signal; run only that OS family's already-proven +detectors with at most three concurrent bounded channels; and return frozen selector-shaped evidence. +Darwin translation unknown returns no evidence rather than inventing native/translated state. +Inconsistent platform identity, product/startup callers, selector changes, live SSH, settings, +fallback, transfer/install, tuple enablement, publication, defaults, and SignPath stay absent. +RED is recorded before implementation: the purpose suite exits 1 because its module is absent +(0 tests, 316 ms, 132,235,264-byte max RSS), and the workflow oracle exits 1 because the suite has +only its POSIX native-family occurrence (6 passed/1 failed, 409 ms, 131,743,744-byte max RSS). +Pre-RED diff and protected-resolver checks pass. +`E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001` is green: 20/20 purpose and 193/193 focused assertions, +512/512 broader relay assertions, 280/280 release-script assertions, typecheck, lint, format, +max-lines, diff, no-product-import, and protected-resolver gates pass. The disconnected composer +validates canonical platform identity before probes; invokes only Linux's three, Darwin's two, or +Windows' one accepted bounded detector; runs multi-probe families concurrently; preserves +conservative unknowns; refuses unknown Darwin translation; propagates cancellation/errors; and +deeply freezes evidence. It is required in both native workflow families. Exact-head all-six Node 24 +client proof remains mandatory; no live SSH, product caller, settings/fallback, transfer/install, +tuple, publication, default, or SignPath work is included. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 c01b7e9fb07..7d61e908426 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: audit the smallest disconnected full host-evidence composition boundary after native acceptance of every individual detector**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **WINDOWS COMPATIBILITY CI CLOSED / HOST COMPOSITION AUDIT NEXT — 2026-07-15, Codex implementation owner** — `E-M5-WINDOWS-COMPATIBILITY-DETECTION-CI-001` proves both native Windows architectures pass the strict detector, all six primary clients and both Linux supplements pass, Windows x64 baseline passes, and Windows arm64 retains only the declared build-26200/26100 runner mismatch after full runtime proof. Audit only the next disconnected composition boundary; do not add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: disconnected host-evidence composition is locally green; exact-head native CI is the next gate**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **HOST COMPOSITION LOCAL GREEN / NATIVE CI NEXT — 2026-07-15, Codex implementation owner** — `E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001` proves canonical six-tuple mapping, conservative unknowns, Darwin translation policy, bounded concurrency, cancellation/error propagation, deep freezing, workflow-family inclusion, and all local static/broad/release gates. Commit and push this isolated package, then require exact-head all-six native client proof before accepting it or opening another package. Do not add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -147,11 +147,10 @@ same change as the work it records. local gates, and all-six native Node 24 proof are green under E-M5-ARTIFACT-CACHE-ROOT-LOCAL-RED-001, E-M5-ARTIFACT-CACHE-ROOT-LOCAL-001, and E-M5-ARTIFACT-CACHE-ROOT-CI-001. No Electron or product caller exists. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit the smallest - disconnected full host-evidence composition boundary after - E-M5-WINDOWS-COMPATIBILITY-DETECTION-CI-001 closes every individual detector on native clients. - Keep live SSH, transfer/install, settings, fallback wiring, tuple enablement, publication, - defaults, and SignPath separately gated. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — disconnected full host- + evidence composition is locally green under E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001. Exact-head + all-six native client proof is the next acceptance gate. Keep live SSH, transfer/install, + settings, fallback wiring, tuple enablement, publication, defaults, and SignPath separately gated. - 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 @@ -15926,6 +15925,108 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, two files, 38 pass tuple enablement, release publication, defaults, and SignPath remain absent. Legacy remains the sole production/default path and every tuple stays disabled. +### E-M5-HOST-EVIDENCE-COMPOSITION-AUDIT-001 — disconnected platform-specific evidence composition + +- Date/owner: 2026-07-15, Codex implementation owner. +- Audit: complete reads of the canonical remote-platform type/detector and tests, all six accepted + platform-specific compatibility detectors and purpose suites, selector host-evidence union and + conservative legacy classifications, native workflow commands/oracle, and the current production- + caller graph. The detector functions remain imported only by purpose tests and no host composer + exists. The existing platform detector has no cancellation input and must remain unchanged here. +- Finding: add one purpose-named `ssh-relay-host-evidence-detection` module consuming an explicit + already-detected `RemoteHostPlatform`, SSH connection, and optional cancellation signal. First + require the platform object's relay-platform identity to agree with its canonical OS/architecture; + inconsistent input returns undefined without a probe. Then invoke only the accepted detector family: + Linux kernel/libc/libstdc++ concurrently (at most three channels), Darwin version/translation + concurrently (at most two), or the single Windows compatibility detector. Return a deeply frozen + `SshRelayHostEvidence` object with canonical OS/architecture. +- Conservative mapping: Linux and Windows are not subject to the reviewed Darwin/Rosetta process- + translation policy, so their composed `processTranslated` value is false. Linux preserves + `{ family: 'unknown' }` and missing optional fields for later selector classification. A missing + complete Windows segment yields Windows evidence with all optional compatibility fields absent so + selection remains legacy. Darwin preserves explicit true/false translation and optional version; + unknown translation returns no host evidence because inventing either boolean could select a native + artifact or misdiagnose a translated process. No selector/type change is needed. +- Failure/concurrency contract: pass the exact signal to every invoked detector. Pre-abort and + `AbortError` propagate; unexpected detector rejection propagates unchanged rather than becoming + fallback eligibility. Ordinary probe failures retain each accepted detector's existing unknown + return. All OS-specific calls start before composition awaits their results, bounding the composed + probe to the slowest existing 15-second detector instead of their sum. No unsupported detector, + platform re-probe, retry, network/download, Node, or filesystem operation is introduced. +- Required RED/green proof: canonical Linux/Darwin/Windows x64 and arm64 mapping; exact detector + family/call count; complete and missing/unknown fields; native/translated/unknown Darwin behavior; + inconsistent platform rejection with zero probes; deep freezing; all relevant calls starting + concurrently; exact signal forwarding; cancellation/unexpected rejection propagation; absence of + production imports; and both native workflow families requiring the purpose suite. +- Deliberately absent: platform-detection modification, full startup/official-manifest/cache + orchestration, live SSH/remote cells, semantic transfer capability, transfer/install, settings, + fallback classification/wiring, Electron, `ORCA_RELAY_PATH`, tuple enablement, publication, + defaults, and SignPath. Legacy remains the sole production/default path and every tuple stays + disabled. +- RED evidence, 2026-07-15: + - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-host-evidence-detection.test.ts` — EXPECTED FAIL, exit 1: purpose suite + could not import absent `ssh-relay-host-evidence-detection`; zero tests collected in 316 ms, + 1.76 s wall time, 132,235,264-byte maximum RSS, 96,490,992-byte peak memory footprint. + - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +config/scripts/ssh-relay-runtime-workflow.test.mjs` — EXPECTED FAIL, exit 1: 6 passed and the + native-family inclusion oracle failed because the purpose suite had only the POSIX workflow + occurrence (`source.split(...)` length 2 instead of 3); 409 ms suite duration, 1.84 s wall time, + 131,743,744-byte maximum RSS, 95,851,800-byte peak memory footprint. + - Scope check before RED: `git diff --check` passed; protected + `ssh-remote-node-resolution.ts` and `.test.ts` diff remained empty. No implementation or product + caller existed. + +### E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001 — bounded immutable host evidence is locally green + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0, + pnpm 10.24.0, atop pushed head `cfe56d0fd` plus this isolated uncommitted package. The expected + Node-24 engine warning means exact Node 24 remains a native CI gate. +- Implementation: the new 92-line `ssh-relay-host-evidence-detection.ts` accepts only an explicit + already-detected platform, connection, and optional signal. It rejects canonical OS/architecture + inconsistency before any probe; starts only the selected OS family's accepted bounded detectors; + runs Linux's three and Darwin's two probes concurrently; preserves Linux/Windows unknowns for + conservative legacy selection; refuses to invent Darwin translation; propagates cancellation and + unexpected rejection; and returns deeply frozen selector-shaped evidence. It has no production + import or caller. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-host-evidence-detection.test.ts` — PASS, one file / 20 tests in + 337 ms Vitest, 1.69 seconds wall, 132,431,872-byte maximum RSS, zero swaps. It covers all six + canonical tuples and exact family call counts, full/unknown/missing mapping, native/translated/ + unknown Darwin behavior, inconsistent input with zero probes, deep freezing, concurrency, exact + signal forwarding, cancellation, and unexpected rejection. +- Workflow-oracle command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, one file / seven tests in + 384 ms Vitest, 1.73 seconds wall, 132,972,544-byte maximum RSS, zero swaps. The purpose suite now + occurs exactly once in both POSIX and PowerShell native contract commands. +- Post-format focused command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1` plus the composer, Windows compatibility, libc, kernel, + Darwin version/translation, libstdc++, selector, platform detection, and workflow-oracle suites — + PASS, 10 files / 193 passed / one declared native-Windows skip in 4.66 seconds Vitest, 7.18 seconds + wall, 138,493,952-byte maximum RSS, zero swaps. +- Broader relay command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS, 39 files passed / three + declared skipped files, 512 tests passed / four declared platform skips in 25.68 seconds Vitest, + 27.42 seconds wall, 281,608,192-byte maximum RSS, zero swaps. Only the established stale-lock + diagnostic and local Node 26 `module.register()` deprecation warning appear. +- Release-script command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 --silent=true config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests + in 24.51 seconds Vitest, 27.61 seconds wall, 188,825,600-byte maximum RSS, zero swaps. +- Static gates: `/usr/bin/time -l pnpm typecheck` passes in 5.81 seconds wall with + 1,262,075,904-byte maximum RSS. `/usr/bin/time -l pnpm lint` passes in 27.03 seconds wall with + 2,034,597,888-byte maximum RSS; all switch-exhaustiveness, 41 reliability, 355-entry max-lines, + bundled-skill, 9,837 localization-reference, locale-parity, and zero-candidate localization gates + pass, with the existing 26 warnings only in untouched files. Explicit max-lines, targeted + `oxlint`, targeted `oxfmt --check`, `git diff --check`, no-production-import, and protected- + resolver empty-diff checks pass. The 92-line implementation and 229-line purpose suite add no + max-lines suppression or vague module. +- Boundary/residual gaps: this is local mock/composition proof, not live SSH or native detector + execution. Exact-head all-six Node 24 client proof is mandatory next. Platform detection, + manifest/cache startup composition, live SSH/remote cells, semantic transfer capability, + transfer/install, Electron/settings, fallback classification/wiring, `ORCA_RELAY_PATH`, tuple + enablement, publication, defaults, and SignPath remain absent. Legacy remains the sole production/ + default path and every tuple stays disabled. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -15984,10 +16085,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Audit only the smallest disconnected Windows compatibility-evidence boundary for the already-reviewed -OS build, OpenSSH for Windows, Windows PowerShell, and .NET Framework selector fields. Record the -exact acquisition/parsing/failure contract before adding purpose tests or implementation. Do not -compose full host evidence, provision production secrets or environments, add an Electron/startup +Commit and push only the locally green disconnected host-evidence composition package under +E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001, then require exact-head all-six Node 24 native client proof +and adjacent PR checks before accepting it or auditing the next package. Do not modify platform +detection or selectors, provision production secrets or environments, add an Electron/startup consumer, embed unreviewed keys or manifest bytes, add SSH transfer/install, mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets diff --git a/src/main/ssh/ssh-relay-host-evidence-detection.test.ts b/src/main/ssh/ssh-relay-host-evidence-detection.test.ts new file mode 100644 index 00000000000..130f62c54ad --- /dev/null +++ b/src/main/ssh/ssh-relay-host-evidence-detection.test.ts @@ -0,0 +1,229 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import type { SshConnection } from './ssh-connection' +import { detectSshRelayHostEvidence } from './ssh-relay-host-evidence-detection' +import { getRemoteHostPlatform, type RemoteHostPlatform } from './ssh-remote-platform' + +const detectorMocks = vi.hoisted(() => ({ + kernel: vi.fn(), + libc: vi.fn(), + libstdcxx: vi.fn(), + darwinVersion: vi.fn(), + darwinTranslation: vi.fn(), + windowsCompatibility: vi.fn() +})) + +vi.mock('./ssh-relay-linux-kernel-detection', () => ({ + detectSshRelayLinuxKernelRelease: detectorMocks.kernel +})) +vi.mock('./ssh-relay-libc-detection', () => ({ + detectSshRelayLinuxLibc: detectorMocks.libc +})) +vi.mock('./ssh-relay-linux-libstdcxx-detection', () => ({ + detectSshRelayLinuxLibstdcxx: detectorMocks.libstdcxx +})) +vi.mock('./ssh-relay-darwin-version-detection', () => ({ + detectSshRelayDarwinVersion: detectorMocks.darwinVersion +})) +vi.mock('./ssh-relay-darwin-translation-detection', () => ({ + detectSshRelayDarwinProcessTranslation: detectorMocks.darwinTranslation +})) +vi.mock('./ssh-relay-windows-compatibility-detection', () => ({ + detectSshRelayWindowsCompatibility: detectorMocks.windowsCompatibility +})) + +const connection = {} as SshConnection + +beforeEach(() => { + vi.resetAllMocks() + detectorMocks.kernel.mockResolvedValue('6.8.0-63-generic') + detectorMocks.libc.mockResolvedValue({ family: 'glibc', version: '2.39' }) + detectorMocks.libstdcxx.mockResolvedValue({ + libstdcxxVersion: '6.0.33', + glibcxxVersion: '3.4.33' + }) + detectorMocks.darwinVersion.mockResolvedValue('15.5') + detectorMocks.darwinTranslation.mockResolvedValue(false) + detectorMocks.windowsCompatibility.mockResolvedValue({ + build: 26100, + openSshVersion: '9.8p1', + powerShellVersion: '5.1', + dotNetFrameworkRelease: 533325 + }) +}) + +function allDetectorCallCounts(): number[] { + return Object.values(detectorMocks).map((mock) => mock.mock.calls.length) +} + +describe('SSH relay host evidence detection', () => { + it.each([ + ['linux-x64', 'linux', 'x64', [1, 1, 1, 0, 0, 0]], + ['linux-arm64', 'linux', 'arm64', [1, 1, 1, 0, 0, 0]], + ['darwin-x64', 'darwin', 'x64', [0, 0, 0, 1, 1, 0]], + ['darwin-arm64', 'darwin', 'arm64', [0, 0, 0, 1, 1, 0]], + ['win32-x64', 'win32', 'x64', [0, 0, 0, 0, 0, 1]], + ['win32-arm64', 'win32', 'arm64', [0, 0, 0, 0, 0, 1]] + ] as const)( + 'maps canonical %s and invokes only its detector family', + async (relayPlatform, os, architecture, expectedCalls) => { + const result = await detectSshRelayHostEvidence( + getRemoteHostPlatform(relayPlatform), + connection + ) + + expect(result).toMatchObject({ os, architecture, processTranslated: false }) + expect(allDetectorCallCounts()).toEqual(expectedCalls) + } + ) + + it('composes complete Linux evidence and deeply freezes owned values', async () => { + const result = await detectSshRelayHostEvidence(getRemoteHostPlatform('linux-x64'), connection) + + expect(result).toEqual({ + os: 'linux', + architecture: 'x64', + processTranslated: false, + kernelVersion: '6.8.0-63-generic', + libc: { family: 'glibc', version: '2.39' }, + libstdcxxVersion: '6.0.33', + glibcxxVersion: '3.4.33' + }) + expect(Object.isFrozen(result)).toBe(true) + expect(result?.os === 'linux' && Object.isFrozen(result.libc)).toBe(true) + }) + + it('preserves unknown Linux libc and missing optional detector evidence', async () => { + detectorMocks.kernel.mockResolvedValue(undefined) + detectorMocks.libc.mockResolvedValue({ family: 'unknown' }) + detectorMocks.libstdcxx.mockResolvedValue(undefined) + + await expect( + detectSshRelayHostEvidence(getRemoteHostPlatform('linux-arm64'), connection) + ).resolves.toEqual({ + os: 'linux', + architecture: 'arm64', + processTranslated: false, + libc: { family: 'unknown' } + }) + }) + + it.each([false, true])('preserves Darwin process translation %s', async (translated) => { + detectorMocks.darwinTranslation.mockResolvedValue(translated) + + await expect( + detectSshRelayHostEvidence(getRemoteHostPlatform('darwin-arm64'), connection) + ).resolves.toEqual({ + os: 'darwin', + architecture: 'arm64', + processTranslated: translated, + version: '15.5' + }) + }) + + it('keeps a known Darwin translation result when the version is unavailable', async () => { + detectorMocks.darwinVersion.mockResolvedValue(undefined) + + await expect( + detectSshRelayHostEvidence(getRemoteHostPlatform('darwin-x64'), connection) + ).resolves.toEqual({ + os: 'darwin', + architecture: 'x64', + processTranslated: false + }) + }) + + it('returns no Darwin evidence when process translation is unknown', async () => { + detectorMocks.darwinTranslation.mockResolvedValue(undefined) + + await expect( + detectSshRelayHostEvidence(getRemoteHostPlatform('darwin-arm64'), connection) + ).resolves.toBeUndefined() + }) + + it('preserves complete Windows compatibility evidence', async () => { + await expect( + detectSshRelayHostEvidence(getRemoteHostPlatform('win32-arm64'), connection) + ).resolves.toEqual({ + os: 'win32', + architecture: 'arm64', + processTranslated: false, + build: 26100, + openSshVersion: '9.8p1', + powerShellVersion: '5.1', + dotNetFrameworkRelease: 533325 + }) + }) + + it('returns conservative Windows evidence when compatibility detection is unavailable', async () => { + detectorMocks.windowsCompatibility.mockResolvedValue(undefined) + + await expect( + detectSshRelayHostEvidence(getRemoteHostPlatform('win32-x64'), connection) + ).resolves.toEqual({ + os: 'win32', + architecture: 'x64', + processTranslated: false + }) + }) + + it('rejects inconsistent platform identity without starting any detector', async () => { + const inconsistent = { + ...getRemoteHostPlatform('linux-x64'), + os: 'win32' + } as RemoteHostPlatform + + await expect(detectSshRelayHostEvidence(inconsistent, connection)).resolves.toBeUndefined() + expect(allDetectorCallCounts()).toEqual([0, 0, 0, 0, 0, 0]) + }) + + it.each([ + ['linux-x64', ['kernel', 'libc', 'libstdcxx']], + ['darwin-x64', ['darwinVersion', 'darwinTranslation']] + ] as const)( + 'starts all %s family probes before awaiting a result', + async (relayPlatform, names) => { + const settlements = names.map(() => Promise.withResolvers()) + names.forEach((name, index) => + detectorMocks[name].mockReturnValue(settlements[index].promise) + ) + + const pending = detectSshRelayHostEvidence(getRemoteHostPlatform(relayPlatform), connection) + + expect(names.map((name) => detectorMocks[name].mock.calls.length)).toEqual(names.map(() => 1)) + settlements.forEach((settlement) => settlement.resolve(undefined)) + await pending + } + ) + + it('forwards the exact signal to every selected detector', async () => { + const controller = new AbortController() + + await detectSshRelayHostEvidence(getRemoteHostPlatform('linux-x64'), connection, { + signal: controller.signal + }) + + for (const mock of [detectorMocks.kernel, detectorMocks.libc, detectorMocks.libstdcxx]) { + expect(mock).toHaveBeenCalledWith(connection, { signal: controller.signal }) + } + }) + + it('propagates cancellation without converting it to host evidence', async () => { + const abortError = new Error('cancel host evidence') + abortError.name = 'AbortError' + detectorMocks.windowsCompatibility.mockRejectedValue(abortError) + + await expect( + detectSshRelayHostEvidence(getRemoteHostPlatform('win32-x64'), connection) + ).rejects.toBe(abortError) + }) + + it('propagates unexpected detector rejection unchanged', async () => { + const failure = new Error('unexpected kernel detector failure') + detectorMocks.kernel.mockRejectedValue(failure) + + await expect( + detectSshRelayHostEvidence(getRemoteHostPlatform('linux-x64'), connection) + ).rejects.toBe(failure) + }) +}) diff --git a/src/main/ssh/ssh-relay-host-evidence-detection.ts b/src/main/ssh/ssh-relay-host-evidence-detection.ts new file mode 100644 index 00000000000..deb984e31a1 --- /dev/null +++ b/src/main/ssh/ssh-relay-host-evidence-detection.ts @@ -0,0 +1,92 @@ +import type { SshConnection } from './ssh-connection' +import { detectSshRelayDarwinProcessTranslation } from './ssh-relay-darwin-translation-detection' +import { detectSshRelayDarwinVersion } from './ssh-relay-darwin-version-detection' +import { detectSshRelayLinuxLibc } from './ssh-relay-libc-detection' +import { detectSshRelayLinuxKernelRelease } from './ssh-relay-linux-kernel-detection' +import { detectSshRelayLinuxLibstdcxx } from './ssh-relay-linux-libstdcxx-detection' +import { detectSshRelayWindowsCompatibility } from './ssh-relay-windows-compatibility-detection' +import type { SshRelayHostEvidence } from './ssh-relay-artifact-selector' +import { getRemoteHostPlatform, type RemoteHostPlatform } from './ssh-remote-platform' + +type DetectionOptions = { signal?: AbortSignal } + +function isCanonicalPlatform(platform: RemoteHostPlatform): boolean { + const canonical = getRemoteHostPlatform(platform.relayPlatform) + return canonical.os === platform.os && canonical.arch === platform.arch +} + +async function detectLinuxHostEvidence( + platform: RemoteHostPlatform, + connection: SshConnection, + options: DetectionOptions +): Promise { + // Why: independent bounded probes run together so composition adds no serial bootstrap latency. + const [kernelVersion, libc, libstdcxx] = await Promise.all([ + detectSshRelayLinuxKernelRelease(connection, options), + detectSshRelayLinuxLibc(connection, options), + detectSshRelayLinuxLibstdcxx(connection, options) + ]) + const frozenLibc = Object.freeze({ ...libc }) + return Object.freeze({ + os: 'linux', + architecture: platform.arch, + processTranslated: false, + ...(kernelVersion === undefined ? {} : { kernelVersion }), + libc: frozenLibc, + ...(libstdcxx === undefined ? {} : libstdcxx) + }) +} + +async function detectDarwinHostEvidence( + platform: RemoteHostPlatform, + connection: SshConnection, + options: DetectionOptions +): Promise { + const [version, processTranslated] = await Promise.all([ + detectSshRelayDarwinVersion(connection, options), + detectSshRelayDarwinProcessTranslation(connection, options) + ]) + // Why: guessing native or translated could select incompatible executable bytes. + if (processTranslated === undefined) { + return undefined + } + return Object.freeze({ + os: 'darwin', + architecture: platform.arch, + processTranslated, + ...(version === undefined ? {} : { version }) + }) +} + +async function detectWindowsHostEvidence( + platform: RemoteHostPlatform, + connection: SshConnection, + options: DetectionOptions +): Promise { + const compatibility = await detectSshRelayWindowsCompatibility(connection, options) + return Object.freeze({ + os: 'win32', + architecture: platform.arch, + processTranslated: false, + ...compatibility + }) +} + +export async function detectSshRelayHostEvidence( + platform: RemoteHostPlatform, + connection: SshConnection, + options: DetectionOptions = {} +): Promise { + // Why: inconsistent caller evidence must not start probes or select a runtime for another tuple. + if (!isCanonicalPlatform(platform)) { + return undefined + } + + if (platform.os === 'linux') { + return detectLinuxHostEvidence(platform, connection, options) + } + if (platform.os === 'darwin') { + return detectDarwinHostEvidence(platform, connection, options) + } + return detectWindowsHostEvidence(platform, connection, options) +} From e76c5f9e8560230f756f05c9e29cf01660a2027a Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 07:26:32 -0400 Subject: [PATCH 205/267] docs: record host evidence composition CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 13 +++- ...github-release-implementation-checklist.md | 77 ++++++++++++++++--- 2 files changed, 75 insertions(+), 15 deletions(-) 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 08c622088dc..62aeccc7d99 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 @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 5 / Work Package 4 desktop resolver/cache — disconnected host- -evidence composition is locally green; exact-head all-six native CI is next, 2026-07-15, Codex -implementation owner.** +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — host-evidence +composition CI is closed; audit only a disconnected transport-neutral verified source-tree contract +next, 2026-07-15, Codex implementation owner.** `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, @@ -288,6 +288,13 @@ conservative unknowns; refuses unknown Darwin translation; propagates cancellati deeply freezes evidence. It is required in both native workflow families. Exact-head all-six Node 24 client proof remains mandatory; no live SSH, product caller, settings/fallback, transfer/install, tuple, publication, default, or SignPath work is included. +`E-M5-HOST-EVIDENCE-COMPOSITION-CI-001` closes exact implementation head `a7fd19de5`: all six +primary native Node 24 clients pass the 20-case composer suite, both Linux supplements and Windows +x64 baseline pass, and PR Checks, Golden E2E, and computer-use pass. Windows arm64 completes full +tree/Node/PTY/watcher/resource proof and retains only the declared hosted build-26200 rejection +against required build 26100. Full-size extraction/cache metrics remain within their existing +budgets. This is native client composition evidence, not live SSH; no product caller, transfer, +settings/fallback, tuple, publication, default, or SignPath behavior exists. `E-M5-ARTIFACT-CACHE-ROOT-CI-001` closes the pure cache-root contract at exact head `aefcaa9a9`: all six primary native Node 24 jobs, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass; Windows arm64 build 26200 remains correctly gated against 26100. The 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 7d61e908426..24629de2703 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 5 / Work Package 4 desktop resolver/cache — **In progress — 2026-07-15, Codex implementation owner: disconnected host-evidence composition is locally green; exact-head native CI is the next gate**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **HOST COMPOSITION LOCAL GREEN / NATIVE CI NEXT — 2026-07-15, Codex implementation owner** — `E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001` proves canonical six-tuple mapping, conservative unknowns, Darwin translation policy, bounded concurrency, cancellation/error propagation, deep freezing, workflow-family inclusion, and all local static/broad/release gates. Commit and push this isolated package, then require exact-head all-six native client proof before accepting it or opening another package. Do not add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: audit only the smallest disconnected transport-neutral verified source-tree contract after host-composition CI closure**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **HOST COMPOSITION CI CLOSED / TRANSFER CONTRACT AUDIT NEXT — 2026-07-15, Codex implementation owner** — `E-M5-HOST-EVIDENCE-COMPOSITION-CI-001` closes exact-head all-six native client proof, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 retains only the declared build-26200/26100 runner mismatch after full runtime proof. Audit only a pure verified source-tree contract next. Do not open SSH channels, transfer/install bytes, add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -147,10 +147,12 @@ same change as the work it records. local gates, and all-six native Node 24 proof are green under E-M5-ARTIFACT-CACHE-ROOT-LOCAL-RED-001, E-M5-ARTIFACT-CACHE-ROOT-LOCAL-001, and E-M5-ARTIFACT-CACHE-ROOT-CI-001. No Electron or product caller exists. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — disconnected full host- - evidence composition is locally green under E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001. Exact-head - all-six native client proof is the next acceptance gate. Keep live SSH, transfer/install, - settings, fallback wiring, tuple enablement, publication, defaults, and SignPath separately gated. +- Completed package: disconnected full host-evidence composition is green locally and on every + primary native client under E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001 and + E-M5-HOST-EVIDENCE-COMPOSITION-CI-001. It has no product caller. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit only Milestone + 6's smallest transport-neutral verified source-tree contract. Do not open SSH channels, transfer + or install bytes, or add product/settings/fallback/tuple/publication/default behavior. - 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 @@ -16027,6 +16029,57 @@ config/vitest.config.ts --maxWorkers=1` plus the composer, Windows compatibility enablement, publication, defaults, and SignPath remain absent. Legacy remains the sole production/ default path and every tuple stays disabled. +### E-M5-HOST-EVIDENCE-COMPOSITION-CI-001 — all six native clients accept host composition + +- Date/owner/source: 2026-07-15, Codex implementation owner; exact implementation commit + `a7fd19de5f2d0b8b7243259fa0a2ea669b5323d7` on draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). SSH Relay Runtime Artifacts run + [29493317298](https://github.com/stablyai/orca/actions/runs/29493317298) checks out that exact SHA + and uses Node 24.18.0. +- All six primary native jobs pass: Windows x64 + [87604401012](https://github.com/stablyai/orca/actions/runs/29493317298/job/87604401012), Windows + arm64 [87604401042](https://github.com/stablyai/orca/actions/runs/29493317298/job/87604401042), + Darwin arm64 [87604401020](https://github.com/stablyai/orca/actions/runs/29493317298/job/87604401020), + Darwin x64 [87604401024](https://github.com/stablyai/orca/actions/runs/29493317298/job/87604401024), + Linux arm64 [87604401102](https://github.com/stablyai/orca/actions/runs/29493317298/job/87604401102), + and Linux x64 [87604401073](https://github.com/stablyai/orca/actions/runs/29493317298/job/87604401073). +- Native composition proof: each primary client passes all 20 purpose cases in 9–19 ms. Both Windows + jobs pass 86 files / 670 tests with 14 declared skips; all four POSIX jobs pass 85 files / 679 + tests with one declared native-Windows skip. This executes the canonical six-tuple mapping, + conservative missing evidence, Darwin translation policy, concurrent family startup, signal/ + rejection propagation, and deep-freeze contract under every supported client OS/architecture. +- Both Linux oldest-userland supplements pass: arm64 + [87605280551](https://github.com/stablyai/orca/actions/runs/29493317298/job/87605280551) and x64 + [87605280565](https://github.com/stablyai/orca/actions/runs/29493317298/job/87605280565). The Windows + x64 oldest-baseline job + [87606310152](https://github.com/stablyai/orca/actions/runs/29493317298/job/87606310152) passes. +- The Windows arm64 oldest-baseline job + [87606310179](https://github.com/stablyai/orca/actions/runs/29493317298/job/87606310179) first + verifies the 60-entry / 42-file / 85,213,511-expanded-byte tree and content identity, Node + v24.18.0 ABI 137, PTY exit 23 and 37x101 resize, five watcher events, and two-second resource + settlement. Smoke takes 5,816.3487 ms at 48,713,728-byte RSS and full verification takes + 7,868.3114 ms. Its only rejection is observed Windows build 26200 against the required build + 26100; platform and arm64 architecture checks pass and residual gaps are empty. +- Full-size extraction remains inside its existing budgets at 1,119.77–2,601.01 ms and + 29,687,808–47,091,712 incremental RSS bytes. Cold immutable-cache publication stays at + 1,347.36–5,064.87 ms and 34,258,944–52,510,720 incremental RSS bytes; warm lookup stays at + 128.45–873.41 ms and 131,072–5,505,024 incremental RSS bytes. Retention stays at 8.97–45.85 ms and + eviction at 15.92–90.46 ms. Every measurement passes its existing budget. +- Adjacent exact-head checks pass: PR Checks run + [29493317294](https://github.com/stablyai/orca/actions/runs/29493317294), job + [87604344252](https://github.com/stablyai/orca/actions/runs/29493317294/job/87604344252); Golden E2E + run [29493317248](https://github.com/stablyai/orca/actions/runs/29493317248), Linux job + [87604343968](https://github.com/stablyai/orca/actions/runs/29493317248/job/87604343968) and macOS + job [87604343988](https://github.com/stablyai/orca/actions/runs/29493317248/job/87604343988); + computer-use run [29493317269](https://github.com/stablyai/orca/actions/runs/29493317269), Ubuntu + job [87604343943](https://github.com/stablyai/orca/actions/runs/29493317269/job/87604343943) and + Windows job [87604343949](https://github.com/stablyai/orca/actions/runs/29493317269/job/87604343949). +- Boundary/residual gaps: this is native client composition proof, not live SSH remote behavior. The + composer still has no product caller. Remote distro/OS evidence, `MaxSessions=1`, live channel + behavior, transport capability, transfer/install, Electron/settings, fallback wiring, + `ORCA_RELAY_PATH`, tuple enablement, publication, defaults, and SignPath remain open. Legacy + remains the sole production/default path and every tuple stays disabled. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -16085,12 +16138,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the locally green disconnected host-evidence composition package under -E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001, then require exact-head all-six Node 24 native client proof -and adjacent PR checks before accepting it or auditing the next package. Do not modify platform -detection or selectors, provision production secrets or environments, add an Electron/startup -consumer, embed unreviewed keys or manifest bytes, add SSH transfer/install, mode wiring, fallback, -tuple enablement, release publication, or default behavior. +Audit only Milestone 6's smallest disconnected transport-neutral verified source-tree contract. +Determine the exact authenticated cache-entry/manifest inputs, immutable file ordering, mode and +size/count limits, ownership/cancellation semantics, and which later transfer families consume it +before adding tests or implementation. Do not open an SSH channel, transfer or install bytes, modify +platform detection/selectors, add an Electron/startup consumer, mode wiring, fallback, tuple +enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. From 2f38700b9844a5e8fd1afa6750d7793bcd57e69c Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 07:51:53 -0400 Subject: [PATCH 206/267] feat: define SSH relay runtime source tree Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 23 +- ...github-release-implementation-checklist.md | 149 +++++++++++- .../ssh/ssh-relay-artifact-acquisition.ts | 16 +- .../ssh/ssh-relay-runtime-source-tree.test.ts | 225 ++++++++++++++++++ src/main/ssh/ssh-relay-runtime-source-tree.ts | 123 ++++++++++ 7 files changed, 517 insertions(+), 22 deletions(-) create mode 100644 src/main/ssh/ssh-relay-runtime-source-tree.test.ts create mode 100644 src/main/ssh/ssh-relay-runtime-source-tree.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 3cf63d7a535..f8a144310e6 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -282,6 +282,7 @@ jobs: src/main/ssh/ssh-relay-artifact-cache-population-integration.test.ts \ src/main/ssh/ssh-relay-artifact-acquisition.test.ts \ src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts \ + src/main/ssh/ssh-relay-runtime-source-tree.test.ts \ src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts \ src/main/ssh/ssh-relay-official-manifest.test.ts \ src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts \ @@ -834,6 +835,7 @@ jobs: src/main/ssh/ssh-relay-artifact-cache-population-integration.test.ts ` src/main/ssh/ssh-relay-artifact-acquisition.test.ts ` src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts ` + src/main/ssh/ssh-relay-runtime-source-tree.test.ts ` src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts ` src/main/ssh/ssh-relay-official-manifest.test.ts ` src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 81348800f38..444ee12cc19 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -140,6 +140,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-artifact-cache-population-integration.test.ts', 'ssh-relay-artifact-acquisition.test.ts', 'ssh-relay-artifact-acquisition-integration.test.ts', + 'ssh-relay-runtime-source-tree.test.ts', 'ssh-relay-compiled-manifest-trust.test.ts', 'ssh-relay-official-manifest.test.ts', 'ssh-relay-manifest-accepted-keys.test.ts', 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 62aeccc7d99..d4dd156931f 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 @@ -9,9 +9,26 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — host-evidence -composition CI is closed; audit only a disconnected transport-neutral verified source-tree contract -next, 2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected +transport-neutral source-tree contract is locally green; exact-head native proof next, 2026-07-15, +Codex implementation owner.** +`E-M6-SOURCE-TREE-CONTRACT-AUDIT-001` fixes a pure ready-acquisition → immutable descriptor +boundary: exact verified manifest/cache identity and signed limits, deterministic ASCII ordering, +client-native paths, borrowed live-lease assertion, and later shared use by pre-scan plus SFTP, +POSIX tar/no-tar, and Windows PowerShell/.NET transfer. It performs no I/O and owns no cleanup; +filesystem enumeration, streaming, SSH, remote writes/install, product/mode/fallback/default wiring, +publication, and SignPath remain absent. +`E-M6-SOURCE-TREE-CONTRACT-LOCAL-RED-001` records the expected missing-module failure for ten purpose +cases and the native-workflow oracle's six-pass/one-fail result: the suite occurs only in the oracle, +not either runner command. `E-M6-SOURCE-TREE-CONTRACT-LOCAL-CONTENTION-001` rejects an overlapping +local broad run whose existing suites hit 30-second timeouts under four concurrent heavy commands; +timeouts were not relaxed. `E-M6-SOURCE-TREE-CONTRACT-LOCAL-001` is green after isolated reruns: +10/10 purpose, 85/85 focused, 522/522 broader relay with four declared skips, 280/280 release-script, +typecheck, full lint, format, max-lines, diff, protected-resolver, and no-product-import gates pass. +The pure 123-line module rechecks ready acquisition identity/aggregates/root, freezes deterministic +signed descriptors, and borrows without releasing the live cache lease. Exact-head native proof is +next; filesystem pre-scan/streaming, SSH, remote writes/install, product/mode/fallback/default wiring, +publication, and SignPath remain absent. `E-M5-ARTIFACT-CACHE-RESOLUTION-CI-001` closes warm-cache commit `22031fa68` on all six native clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 remains the expected build-26200 rejection against 26100. `E-M5-ARTIFACT-CACHE-RESOLUTION-LOCAL-001` passes 9/9 purpose tests, 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 24629de2703..e577c4bcd58 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: audit only the smallest disconnected transport-neutral verified source-tree contract after host-composition CI closure**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **HOST COMPOSITION CI CLOSED / TRANSFER CONTRACT AUDIT NEXT — 2026-07-15, Codex implementation owner** — `E-M5-HOST-EVIDENCE-COMPOSITION-CI-001` closes exact-head all-six native client proof, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 retains only the declared build-26200/26100 runner mismatch after full runtime proof. Audit only a pure verified source-tree contract next. Do not open SSH channels, transfer/install bytes, add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: exact-head native proof for the locally green disconnected transport-neutral verified source-tree contract**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **SOURCE-TREE CONTRACT LOCAL GREEN / EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-SOURCE-TREE-CONTRACT-LOCAL-001` passes purpose, workflow, focused, broad relay, release-script, typecheck, lint, format, max-lines, diff, protected-resolver, and no-product-caller gates after separately rerunning two contention-invalidated broad commands. Commit and push the isolated package, then require exact-head all-six native proof before checking the Milestone 6 item or advancing to pre-scan. Do not enumerate the filesystem, open SSH channels, transfer/install bytes, add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -150,9 +150,10 @@ same change as the work it records. - Completed package: disconnected full host-evidence composition is green locally and on every primary native client under E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001 and E-M5-HOST-EVIDENCE-COMPOSITION-CI-001. It has no product caller. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit only Milestone - 6's smallest transport-neutral verified source-tree contract. Do not open SSH channels, transfer - or install bytes, or add product/settings/fallback/tuple/publication/default behavior. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — exact-head native proof + for Milestone 6's locally green pure transport-neutral verified source-tree contract. Do not + enumerate the filesystem, open SSH channels, transfer or install bytes, or add product/settings/ + fallback/tuple/publication/default behavior. - 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 @@ -1153,7 +1154,11 @@ Current behavior must not be assumed adequate for a much larger runtime: ### Shared transfer contract -- [ ] Define one transport-neutral source-tree contract based on the verified manifest. +- [ ] Define one transport-neutral source-tree contract based on the verified manifest. **In + progress — 2026-07-15, Codex implementation owner; audit complete under + E-M6-SOURCE-TREE-CONTRACT-AUDIT-001; RED and local green proof recorded under + E-M6-SOURCE-TREE-CONTRACT-LOCAL-RED-001 and E-M6-SOURCE-TREE-CONTRACT-LOCAL-001; exact-head + native proof required before completion.** - [ ] Pre-scan and reject local source mutation, symlinks, special files, extra files, and path collisions before creating remote files. - [ ] Stream files with bounded buffers; prohibit whole-tree memory materialization. @@ -16080,6 +16085,126 @@ config/vitest.config.ts --maxWorkers=1` plus the composer, Windows compatibility `ORCA_RELAY_PATH`, tuple enablement, publication, defaults, and SignPath remain open. Legacy remains the sole production/default path and every tuple stays disabled. +### E-M6-SOURCE-TREE-CONTRACT-AUDIT-001 — disconnected authenticated source-tree projection + +- Date/owner: 2026-07-15, Codex implementation owner. +- Exact input boundary: accept only the `ready` result from the existing artifact acquisition. That + result already binds a selected tuple from a signature-verified immutable manifest, a strict + fully verified cache entry, and a live content-scoped in-use lease. Do not accept a raw manifest, + tuple, cache path, archive, or caller-created file list. +- Immutable projection: recheck tuple/content identity, cache aggregate counts, and the canonical + `runtime` root before exposure. Project the authenticated entries into deeply frozen directory + and file descriptors with client-native absolute paths. Use one explicit ASCII path order, + independent of manifest insertion order and filesystem enumeration order. Preserve signed file + role, exact size, SHA-256, and only the declared 0644/0755 modes. +- Limits: inherit the already authenticated schema ceilings rather than adding a second policy: + 5,000 total entries, 250 MiB per file, 350 MiB expanded bytes, and exact signed file-count/ + expanded-byte aggregates. A mismatch fails closed before a descriptor is returned. +- Ownership/cancellation: the projection is synchronous and performs no filesystem or network I/O, + so it has no cancellation point and creates no resource to clean up. It exposes only a borrowed + lease-ownership assertion; it never releases the acquisition's lease. The later orchestration + owner must retain and release that lease only after pre-scan, the selected transfer family, and + cleanup have all settled. The next asynchronous pre-scan/transfer package must accept one exact + `AbortSignal`, check it throughout, and assert the borrowed lease before and after source reads. +- Later consumers: the same contract is the sole expected-tree input for pre-scan and for bounded + SFTP, POSIX tar/no-tar system-SSH, and Windows PowerShell/.NET system-SSH transfer. Transport code + may choose framing/concurrency but may not reorder or weaken the authenticated descriptors. +- Required RED/green proof: Linux and Windows signed tuples; deterministic ordering and native path + construction; exact modes/roles/sizes/hashes/aggregates; deep freezing; ready-only input; identity, + aggregate, and runtime-root rejection; borrowed lease assertion with zero implicit release; no + filesystem/SSH/product import or caller; and exact inclusion in both native workflow families. +- Deliberately absent: filesystem enumeration or mutation detection, file opening/streaming, + channels/SFTP/child processes, remote paths/writes/install, progress, cleanup, settings/modes, + fallback, Electron/startup, `ORCA_RELAY_PATH`, tuple enablement, publication, defaults, and + SignPath. Legacy remains the sole production/default path and every tuple stays disabled. + +### E-M6-SOURCE-TREE-CONTRACT-LOCAL-RED-001 — missing source-tree contract and native proof + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0, + pnpm 10.24.0, atop local evidence commit `e76c5f9e8` and the audit/test-only working diff. Exact + Node 24 remains a later native CI gate. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-source-tree.test.ts` — EXPECTED FAIL, exit 1: the + absent `ssh-relay-runtime-source-tree` module prevents collection of the ten audited cases; zero + tests run in 600 ms Vitest / 3.20 seconds wall, 132,104,192-byte maximum RSS and 96,212,248-byte + peak memory footprint. +- Workflow-oracle command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 config/scripts/ssh-relay-runtime-workflow.test.mjs` — EXPECTED FAIL, exit 1: six + workflow cases pass and the native-family inclusion oracle fails because the new purpose suite has + one source occurrence rather than the required three; 544 ms Vitest / 3.18 seconds wall, + 131,956,736-byte maximum RSS and 96,245,280-byte peak memory footprint. +- Scope at RED: `git diff --check` passed before the package; protected resolver files had no diff. + No source-tree module, production import/caller, filesystem enumeration, SSH operation, transfer, + remote write, settings/fallback, tuple, publication, or default behavior exists. + +### E-M6-SOURCE-TREE-CONTRACT-LOCAL-CONTENTION-001 — rejected concurrent broad-gate run + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64. The first broad + relay, release-script, and full-lint commands were launched together while an unrelated local + Vitest process was also active. Process inspection showed all four CPU/I/O-heavy commands + overlapping for more than six minutes; these results are not accepted as implementation evidence. +- Relay command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — INVALID/FAIL, 39 files passed, + three skipped, and one existing cache-entry suite failed seven cases only at their 30-second test + timeout; 515 tests passed and four were skipped in 449.11 seconds Vitest / 453.40 seconds wall. +- Release-script command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 --silent=true config/scripts/ssh-relay-runtime-*.test.mjs` — INVALID/FAIL, 48 files + passed and two existing suites failed nine cases at 30-second timeouts/teardown races; 271 tests + passed with four unhandled cleanup errors in 445.50 seconds Vitest / 449.76 seconds wall. +- Full lint nevertheless passed every command in 448.00 seconds wall. Because the two test failures + were resource-contention artifacts outside this package, both broad test commands were rerun alone + after process inspection confirmed no competing Vitest/lint process; only those isolated reruns + count below. No timeout was increased and no test or production behavior was weakened. + +### E-M6-SOURCE-TREE-CONTRACT-LOCAL-001 — pure authenticated source-tree contract is locally green + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0, + pnpm 10.24.0, atop local evidence commit `e76c5f9e8` plus this isolated package. Exact Node 24 and + every client OS/architecture remain mandatory in native CI. +- Implementation: the 123-line `ssh-relay-runtime-source-tree.ts` accepts only an acquisition-ready + type exported by the existing acquisition module. It rechecks signed tuple/content identity, + verified cache aggregates, and the exact cache `runtime` root; then projects Linux/Darwin/Windows + manifest entries into separately ASCII-sorted, deeply frozen client-native directory/file + descriptors. It preserves signed roles, modes, sizes, and hashes, exposes only a borrowed lease + assertion, and never releases the owner lease. It imports only `node:path`, acquisition types, and + runtime identity types; it performs no I/O and has no product caller. +- Post-format purpose command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-runtime-source-tree.test.ts` — PASS, + one file / ten tests in 1.64 seconds Vitest / 6.64 seconds wall, 132,792,320-byte maximum RSS and + 96,998,776-byte peak memory footprint. It covers signed Linux/Windows tuples, reversed insertion + order, client-native paths, exact descriptors/aggregates, deep freezing, lease borrowing, and + ready/identity/aggregate/runtime-root rejection. +- Workflow-oracle command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, one file / seven tests in + 1.24 seconds Vitest / 6.23 seconds wall, 133,103,616-byte maximum RSS. Both native workflow command + families now execute the purpose suite exactly once. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1` plus the source-tree, acquisition unit/integration, cache-entry, schema, signature, + and workflow-oracle suites — PASS, seven files / 85 tests in 39.79 seconds Vitest. The observed + 188.64-second wrapper wall time came from the same local command scheduling period, but no focused + test failed or timed out. +- Isolated broader relay rerun: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 --silent=true src/main/ssh/ssh-relay-*.test.ts` — PASS, 40 + files passed / three declared full-size skips, 522 tests passed / four declared platform skips in + 54.54 seconds Vitest / 57.40 seconds wall, 222,511,104-byte maximum RSS. Only the established stale + lock diagnostic and local Node 26 `module.register()` deprecation warning appear. +- Isolated release-script rerun: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 --silent=true config/scripts/ssh-relay-runtime-*.test.mjs` — + PASS, 50 files / 280 tests in 35.05 seconds Vitest / 39.91 seconds wall, 190,300,160-byte maximum + RSS. +- Static gates: `/usr/bin/time -l pnpm typecheck` passes in 9.32 seconds wall with + 1,238,499,328-byte maximum RSS. `/usr/bin/time -l pnpm lint` passes every full lint, + switch-exhaustiveness, 41 reliability, 355-entry max-lines, bundled-skill, 9,837 localization- + reference, locale-parity, and zero-candidate localization gate; the 26 warnings remain only in + untouched files. Targeted `oxlint`, targeted `oxfmt --check`, `git diff --check`, protected- + resolver empty diff, no other `src/main` importer, and the 123/226-line implementation/test size + checks pass. No max-lines suppression or vague module is added. +- Boundary/residual gaps: this is a synchronous descriptor contract, not filesystem pre-scan, + mutation detection, streaming, live SSH, SFTP/system-SSH, remote staging/install, or a full-size + transfer. Exact-head all-six Node 24 client proof is required before accepting the item. Every + product/startup/settings/fallback/tuple/publication/default path remains absent; legacy remains the + sole production/default path, every tuple stays disabled, and SignPath stays deferred. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -16138,12 +16263,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Audit only Milestone 6's smallest disconnected transport-neutral verified source-tree contract. -Determine the exact authenticated cache-entry/manifest inputs, immutable file ordering, mode and -size/count limits, ownership/cancellation semantics, and which later transfer families consume it -before adding tests or implementation. Do not open an SSH channel, transfer or install bytes, modify -platform detection/selectors, add an Electron/startup consumer, mode wiring, fallback, tuple -enablement, release publication, or default behavior. +Commit and push the locally green Milestone 6 disconnected transport-neutral verified source-tree +contract, then require exact-head all-six native client proof plus adjacent PR checks before accepting +the item or advancing to the filesystem pre-scan package. +Do not enumerate the filesystem, open an SSH channel, transfer or install bytes, modify platform +detection/selectors, add an Electron/startup consumer, mode wiring, fallback, tuple enablement, +release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. diff --git a/src/main/ssh/ssh-relay-artifact-acquisition.ts b/src/main/ssh/ssh-relay-artifact-acquisition.ts index 4208c53a437..651e2b9bbd6 100644 --- a/src/main/ssh/ssh-relay-artifact-acquisition.ts +++ b/src/main/ssh/ssh-relay-artifact-acquisition.ts @@ -22,16 +22,18 @@ const DEFAULT_OPERATIONS: SshRelayArtifactAcquisitionOperations = Object.freeze( populate: populateSshRelayArtifactCache }) +export type SshRelayArtifactReadyAcquisition = Readonly<{ + kind: 'ready' + source: 'cache' | 'download' + artifact: SshRelaySelectedArtifact + entry: Readonly + lease: SshRelayArtifactCacheInUseLease +}> + export type SshRelayArtifactAcquisition = | Readonly<{ kind: 'unavailable'; reason: 'official-manifest-unavailable' }> | Readonly<{ kind: 'legacy'; reason: SshRelayArtifactLegacyReason }> - | Readonly<{ - kind: 'ready' - source: 'cache' | 'download' - artifact: SshRelaySelectedArtifact - entry: Readonly - lease: SshRelayArtifactCacheInUseLease - }> + | SshRelayArtifactReadyAcquisition function sameArtifactIdentity( expected: SshRelaySelectedArtifact, diff --git a/src/main/ssh/ssh-relay-runtime-source-tree.test.ts b/src/main/ssh/ssh-relay-runtime-source-tree.test.ts new file mode 100644 index 00000000000..cf7f07111b5 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-source-tree.test.ts @@ -0,0 +1,225 @@ +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import nacl from 'tweetnacl' +import { describe, expect, it, vi } from 'vitest' + +import type { SshRelayArtifactReadyAcquisition } from './ssh-relay-artifact-acquisition' +import type { SshRelayArtifactCacheEntry } from './ssh-relay-artifact-cache-entry-verification' +import type { SshRelayArtifactCacheInUseLease } from './ssh-relay-artifact-cache-in-use-lease' +import { + createSshRelayArtifactTestManifest, + createSshRelayWindowsArtifactTestManifest +} from './ssh-relay-artifact-test-manifest' +import { selectSshRelayArtifact } from './ssh-relay-artifact-selector' +import { + signSshRelayArtifactManifest, + sshRelayManifestKeyId, + verifySshRelayArtifactManifest +} from './ssh-relay-manifest-signature' +import { sshRelayRuntimeArchiveName } from './ssh-relay-release-asset' +import { computeSshRelayRuntimeContentId, type SshRelayDigest } from './ssh-relay-runtime-identity' +import { createSshRelayRuntimeSourceTree } from './ssh-relay-runtime-source-tree' + +const keyPair = nacl.sign.keyPair.fromSeed(Uint8Array.from({ length: 32 }, (_, index) => index)) +const cacheRoot = join(tmpdir(), 'orca-relay-runtime-source-tree') + +type Fixture = { + acquisition: SshRelayArtifactReadyAcquisition + assertOwned: ReturnType + release: ReturnType +} + +function readyFixture( + os: 'linux' | 'win32', + { reverseEntries = false }: { reverseEntries?: boolean } = {} +): Fixture { + const manifest = + os === 'win32' + ? createSshRelayWindowsArtifactTestManifest() + : createSshRelayArtifactTestManifest() + const tuple = manifest.tuples[0] + if (reverseEntries) { + tuple.entries.reverse() + tuple.contentId = computeSshRelayRuntimeContentId(tuple) + tuple.archive.name = sshRelayRuntimeArchiveName(tuple.tupleId, tuple.contentId) + } + manifest.signatures = [signSshRelayArtifactManifest(manifest, keyPair.secretKey)] + const verified = verifySshRelayArtifactManifest(manifest, [ + { keyId: sshRelayManifestKeyId(keyPair.publicKey), publicKey: keyPair.publicKey } + ]) + const host = + os === 'win32' + ? ({ + os, + architecture: 'x64', + processTranslated: false, + build: 22631, + openSshVersion: '9.5p1', + powerShellVersion: '5.1', + dotNetFrameworkRelease: 528040 + } as const) + : ({ + os, + architecture: 'x64', + processTranslated: false, + kernelVersion: '6.8', + libc: { family: 'glibc', version: '2.39' }, + libstdcxxVersion: '6.0.33', + glibcxxVersion: '3.4.33' + } as const) + const artifact = selectSshRelayArtifact(verified, host) + if (artifact.kind !== 'selected') { + throw new Error(`Expected selected source-tree fixture, got ${artifact.reason}`) + } + const entryPath = join(cacheRoot, 'entries', artifact.contentId.slice('sha256:'.length)) + const entry: SshRelayArtifactCacheEntry = { + contentId: artifact.contentId, + tupleId: artifact.tupleId, + entryPath, + archivePath: join(entryPath, artifact.archive.name), + runtimeRoot: join(entryPath, 'runtime'), + proofPath: join(entryPath, 'proof.json'), + files: artifact.archive.fileCount, + expandedBytes: artifact.archive.expandedSize + } + const assertOwned = vi.fn(async () => {}) + const release = vi.fn(async () => {}) + const lease: SshRelayArtifactCacheInUseLease = { + leasePath: join(cacheRoot, 'in-use', 'a'.repeat(32)), + token: 'a'.repeat(32), + assertOwned, + release + } + return { + acquisition: { kind: 'ready', source: 'cache', artifact, entry, lease }, + assertOwned, + release + } +} + +function asciiSorted(paths: string[]): string[] { + return [...paths].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0)) +} + +const inconsistentCacheEntries: [string, Partial][] = [ + ['tuple identity', { tupleId: 'win32-x64' }], + ['content identity', { contentId: `sha256:${'f'.repeat(64)}` as SshRelayDigest }], + ['file count', { files: 1 }], + ['expanded byte count', { expandedBytes: 1 }] +] + +describe('SSH relay runtime source-tree contract', () => { + it.each(['linux', 'win32'] as const)( + 'projects an authenticated %s acquisition in immutable ASCII order', + (os) => { + const { acquisition } = readyFixture(os, { reverseEntries: true }) + + const tree = createSshRelayRuntimeSourceTree(acquisition) + + const expectedDirectories = asciiSorted( + acquisition.artifact.tuple.entries + .filter((entry) => entry.type === 'directory') + .map((entry) => entry.path) + ) + const expectedFiles = asciiSorted( + acquisition.artifact.tuple.entries + .filter((entry) => entry.type === 'file') + .map((entry) => entry.path) + ) + expect(tree).toMatchObject({ + tupleId: acquisition.artifact.tupleId, + contentId: acquisition.artifact.contentId, + releaseTag: acquisition.artifact.releaseTag, + os: acquisition.artifact.tuple.os, + architecture: acquisition.artifact.tuple.architecture, + runtimeRoot: acquisition.entry.runtimeRoot, + fileCount: acquisition.entry.files, + expandedBytes: acquisition.entry.expandedBytes + }) + expect(tree.directories.map((entry) => entry.path)).toEqual(expectedDirectories) + expect(tree.files.map((entry) => entry.path)).toEqual(expectedFiles) + for (const descriptor of [...tree.directories, ...tree.files]) { + expect(descriptor.localPath).toBe( + join(acquisition.entry.runtimeRoot, ...descriptor.path.split('/')) + ) + } + expect(tree.files).toEqual( + expectedFiles.map((path) => { + const entry = acquisition.artifact.tuple.entries.find( + (candidate) => candidate.path === path + ) + if (!entry || entry.type !== 'file') { + throw new Error(`Missing expected signed file: ${path}`) + } + return { + path, + localPath: join(acquisition.entry.runtimeRoot, ...path.split('/')), + type: 'file', + role: entry.role, + size: entry.size, + mode: entry.mode, + sha256: entry.sha256 + } + }) + ) + } + ) + + it('deeply freezes descriptors without mutating signed manifest insertion order', () => { + const { acquisition } = readyFixture('linux') + const originalOrder = acquisition.artifact.tuple.entries.map((entry) => entry.path) + + const tree = createSshRelayRuntimeSourceTree(acquisition) + + expect(Object.isFrozen(tree)).toBe(true) + expect(Object.isFrozen(tree.directories)).toBe(true) + expect(Object.isFrozen(tree.files)).toBe(true) + expect(tree.directories.every(Object.isFrozen)).toBe(true) + expect(tree.files.every(Object.isFrozen)).toBe(true) + expect(acquisition.artifact.tuple.entries.map((entry) => entry.path)).toEqual(originalOrder) + }) + + it('borrows lease ownership without asserting or releasing it implicitly', async () => { + const { acquisition, assertOwned, release } = readyFixture('linux') + + const tree = createSshRelayRuntimeSourceTree(acquisition) + + expect(assertOwned).not.toHaveBeenCalled() + expect(release).not.toHaveBeenCalled() + await tree.assertLeaseOwned() + expect(assertOwned).toHaveBeenCalledTimes(1) + expect(release).not.toHaveBeenCalled() + }) + + it('rejects a non-ready acquisition', () => { + expect(() => + createSshRelayRuntimeSourceTree({ + kind: 'legacy', + reason: 'kernel-too-old' + } as unknown as SshRelayArtifactReadyAcquisition) + ).toThrow(/ready/i) + }) + + it.each(inconsistentCacheEntries)('rejects inconsistent cache %s', (_name, entryPatch) => { + const { acquisition } = readyFixture('linux') + + expect(() => + createSshRelayRuntimeSourceTree({ + ...acquisition, + entry: { ...acquisition.entry, ...entryPatch } + }) + ).toThrow(/identity|file count|expanded byte/i) + }) + + it('rejects a noncanonical cache runtime root', () => { + const { acquisition } = readyFixture('linux') + + expect(() => + createSshRelayRuntimeSourceTree({ + ...acquisition, + entry: { ...acquisition.entry, runtimeRoot: join(acquisition.entry.entryPath, 'other') } + }) + ).toThrow(/runtime root/i) + }) +}) diff --git a/src/main/ssh/ssh-relay-runtime-source-tree.ts b/src/main/ssh/ssh-relay-runtime-source-tree.ts new file mode 100644 index 00000000000..c4bf366a91e --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-source-tree.ts @@ -0,0 +1,123 @@ +import { join } from 'node:path' + +import type { SshRelayArtifactReadyAcquisition } from './ssh-relay-artifact-acquisition' +import type { + SshRelayDigest, + SshRelayRuntimeFileRole, + SshRelayRuntimeTupleId +} from './ssh-relay-runtime-identity' + +export type SshRelayRuntimeSourceDirectory = Readonly<{ + path: string + localPath: string + type: 'directory' + mode: 0o755 +}> + +export type SshRelayRuntimeSourceFile = Readonly<{ + path: string + localPath: string + type: 'file' + role: SshRelayRuntimeFileRole + size: number + mode: 0o644 | 0o755 + sha256: SshRelayDigest +}> + +export type SshRelayRuntimeSourceTree = Readonly<{ + tupleId: SshRelayRuntimeTupleId + contentId: SshRelayDigest + releaseTag: string + os: 'linux' | 'darwin' | 'win32' + architecture: 'x64' | 'arm64' + runtimeRoot: string + directories: readonly SshRelayRuntimeSourceDirectory[] + files: readonly SshRelayRuntimeSourceFile[] + fileCount: number + expandedBytes: number + assertLeaseOwned: () => Promise +}> + +function compareAscii(left: { path: string }, right: { path: string }): number { + return left.path < right.path ? -1 : left.path > right.path ? 1 : 0 +} + +export function createSshRelayRuntimeSourceTree( + acquisition: SshRelayArtifactReadyAcquisition +): SshRelayRuntimeSourceTree { + if (!acquisition || acquisition.kind !== 'ready') { + throw new Error('SSH relay runtime source tree requires a ready artifact acquisition') + } + const { artifact, entry, lease } = acquisition + if ( + artifact.tupleId !== artifact.tuple.tupleId || + artifact.contentId !== artifact.tuple.contentId || + entry.tupleId !== artifact.tupleId || + entry.contentId !== artifact.contentId + ) { + throw new Error('SSH relay runtime source tree has inconsistent artifact identity') + } + if (entry.runtimeRoot !== join(entry.entryPath, 'runtime')) { + throw new Error('SSH relay runtime source tree has a noncanonical cache runtime root') + } + + const directories: SshRelayRuntimeSourceDirectory[] = [] + const files: SshRelayRuntimeSourceFile[] = [] + let expandedBytes = 0 + for (const manifestEntry of artifact.tuple.entries) { + const localPath = join(entry.runtimeRoot, ...manifestEntry.path.split('/')) + if (manifestEntry.type === 'directory') { + directories.push( + Object.freeze({ + path: manifestEntry.path, + localPath, + type: manifestEntry.type, + mode: manifestEntry.mode + }) + ) + continue + } + expandedBytes += manifestEntry.size + files.push( + Object.freeze({ + path: manifestEntry.path, + localPath, + type: manifestEntry.type, + role: manifestEntry.role, + size: manifestEntry.size, + mode: manifestEntry.mode, + sha256: manifestEntry.sha256 + }) + ) + } + if (files.length !== artifact.archive.fileCount || entry.files !== artifact.archive.fileCount) { + throw new Error('SSH relay runtime source tree file count disagrees with the signed artifact') + } + if ( + expandedBytes !== artifact.archive.expandedSize || + entry.expandedBytes !== artifact.archive.expandedSize + ) { + throw new Error( + 'SSH relay runtime source tree expanded byte count disagrees with the signed artifact' + ) + } + + directories.sort(compareAscii) + files.sort(compareAscii) + // Why: transports borrow the live cache lease; only their orchestration owner may release it + // after transfer and cleanup have both settled. + const assertLeaseOwned = (): Promise => lease.assertOwned() + return Object.freeze({ + tupleId: artifact.tupleId, + contentId: artifact.contentId, + releaseTag: artifact.releaseTag, + os: artifact.tuple.os, + architecture: artifact.tuple.architecture, + runtimeRoot: entry.runtimeRoot, + directories: Object.freeze(directories), + files: Object.freeze(files), + fileCount: files.length, + expandedBytes, + assertLeaseOwned + }) +} From fea8e4ad1247bb4c895d7928ace255d75f36b02f Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 08:09:18 -0400 Subject: [PATCH 207/267] docs: record source tree contract CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 11 ++- ...github-release-implementation-checklist.md | 83 +++++++++++++++---- 2 files changed, 77 insertions(+), 17 deletions(-) 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 d4dd156931f..e4de050b23f 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 @@ -10,14 +10,21 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected -transport-neutral source-tree contract is locally green; exact-head native proof next, 2026-07-15, -Codex implementation owner.** +transport-neutral source-tree contract CI is closed; audit only the full-tree source pre-scan next, +2026-07-15, Codex implementation owner.** `E-M6-SOURCE-TREE-CONTRACT-AUDIT-001` fixes a pure ready-acquisition → immutable descriptor boundary: exact verified manifest/cache identity and signed limits, deterministic ASCII ordering, client-native paths, borrowed live-lease assertion, and later shared use by pre-scan plus SFTP, POSIX tar/no-tar, and Windows PowerShell/.NET transfer. It performs no I/O and owns no cleanup; filesystem enumeration, streaming, SSH, remote writes/install, product/mode/fallback/default wiring, publication, and SignPath remain absent. +`E-M6-SOURCE-TREE-CONTRACT-CI-001` closes exact implementation commit `2f38700b9`: all six primary +native Node 24 clients, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and +computer-use pass. Windows arm64 retains only the declared hosted build-26200 rejection against +26100 after complete Node/PTY/watcher/tree proof. Every client passes the ten-case source-tree suite; +Windows totals are 87 files / 680 passed / 14 skipped and POSIX totals are 86 files / 689 passed / one +skip. Full-size extraction/cache metrics remain in budget. Filesystem pre-scan/mutation proof, SSH, +remote writes/install, product/mode/fallback/default wiring, publication, and SignPath remain absent. `E-M6-SOURCE-TREE-CONTRACT-LOCAL-RED-001` records the expected missing-module failure for ten purpose cases and the native-workflow oracle's six-pass/one-fail result: the suite occurs only in the oracle, not either runner command. `E-M6-SOURCE-TREE-CONTRACT-LOCAL-CONTENTION-001` rejects an overlapping 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 e577c4bcd58..6fd978b643c 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: exact-head native proof for the locally green disconnected transport-neutral verified source-tree contract**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **SOURCE-TREE CONTRACT LOCAL GREEN / EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-SOURCE-TREE-CONTRACT-LOCAL-001` passes purpose, workflow, focused, broad relay, release-script, typecheck, lint, format, max-lines, diff, protected-resolver, and no-product-caller gates after separately rerunning two contention-invalidated broad commands. Commit and push the isolated package, then require exact-head all-six native proof before checking the Milestone 6 item or advancing to pre-scan. Do not enumerate the filesystem, open SSH channels, transfer/install bytes, add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: audit only the smallest disconnected full-tree source pre-scan after source-tree CI closure**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **SOURCE-TREE CONTRACT CI CLOSED / PRE-SCAN AUDIT NEXT — 2026-07-15, Codex implementation owner** — `E-M6-SOURCE-TREE-CONTRACT-CI-001` closes exact-head all-six native client proof, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 retains only the declared build-26200/26100 runner mismatch after full runtime proof. Audit only a disconnected filesystem pre-scan that consumes the accepted source-tree contract and creates no remote files. Do not open SSH channels, transfer/install bytes, add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -150,10 +150,10 @@ same change as the work it records. - Completed package: disconnected full host-evidence composition is green locally and on every primary native client under E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001 and E-M5-HOST-EVIDENCE-COMPOSITION-CI-001. It has no product caller. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — exact-head native proof - for Milestone 6's locally green pure transport-neutral verified source-tree contract. Do not - enumerate the filesystem, open SSH channels, transfer or install bytes, or add product/settings/ - fallback/tuple/publication/default behavior. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit only Milestone + 6's smallest disconnected full-tree source pre-scan after source-tree CI closure. Do not open SSH + channels, transfer or install bytes, or add product/settings/fallback/tuple/publication/default + behavior. - 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 @@ -1154,13 +1154,12 @@ Current behavior must not be assumed adequate for a much larger runtime: ### Shared transfer contract -- [ ] Define one transport-neutral source-tree contract based on the verified manifest. **In - progress — 2026-07-15, Codex implementation owner; audit complete under - E-M6-SOURCE-TREE-CONTRACT-AUDIT-001; RED and local green proof recorded under - E-M6-SOURCE-TREE-CONTRACT-LOCAL-RED-001 and E-M6-SOURCE-TREE-CONTRACT-LOCAL-001; exact-head - native proof required before completion.** +- [x] Define one transport-neutral source-tree contract based on the verified manifest. Completed by + E-M6-SOURCE-TREE-CONTRACT-AUDIT-001, E-M6-SOURCE-TREE-CONTRACT-LOCAL-RED-001, + E-M6-SOURCE-TREE-CONTRACT-LOCAL-001, and E-M6-SOURCE-TREE-CONTRACT-CI-001. - [ ] Pre-scan and reject local source mutation, symlinks, special files, extra files, and path - collisions before creating remote files. + collisions before creating remote files. **In progress — 2026-07-15, Codex implementation + owner: audit only; no tests or implementation yet.** - [ ] Stream files with bounded buffers; prohibit whole-tree memory materialization. - [ ] Pass one AbortSignal through enumeration, channel creation, reads, writes, permission repair, and remote cleanup. @@ -16205,6 +16204,58 @@ config/vitest.config.ts --maxWorkers=1 --silent=true config/scripts/ssh-relay-ru product/startup/settings/fallback/tuple/publication/default path remains absent; legacy remains the sole production/default path, every tuple stays disabled, and SignPath stays deferred. +### E-M6-SOURCE-TREE-CONTRACT-CI-001 — all six native clients accept the source-tree contract + +- Date/owner/source: 2026-07-15, Codex implementation owner; exact implementation commit + `2f38700b9844a5e8fd1afa6750d7793bcd57e69c` on draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). SSH Relay Runtime Artifacts run + [29496035270](https://github.com/stablyai/orca/actions/runs/29496035270) checks out that exact SHA + and uses Node 24.18.0. +- All six primary native jobs pass: Windows x64 + [87613081678](https://github.com/stablyai/orca/actions/runs/29496035270/job/87613081678), Windows + arm64 [87613081711](https://github.com/stablyai/orca/actions/runs/29496035270/job/87613081711), + Darwin arm64 [87613081693](https://github.com/stablyai/orca/actions/runs/29496035270/job/87613081693), + Darwin x64 [87613081698](https://github.com/stablyai/orca/actions/runs/29496035270/job/87613081698), + Linux arm64 [87613081803](https://github.com/stablyai/orca/actions/runs/29496035270/job/87613081803), + and Linux x64 [87613081783](https://github.com/stablyai/orca/actions/runs/29496035270/job/87613081783). +- Native contract proof: every client passes all ten purpose cases in 104–442 ms. Both Windows jobs + pass 87 files / 680 tests with 14 declared skips; all four POSIX jobs pass 86 files / 689 tests + with one declared native-Windows skip. This executes signed Linux/Windows descriptor projection, + deterministic ordering, native paths, deep freezing, borrowed lease semantics, and all fail-closed + identity/aggregate/root cases on every supported client OS/architecture. +- Both Linux oldest-userland supplements pass: x64 + [87614003979](https://github.com/stablyai/orca/actions/runs/29496035270/job/87614003979) and arm64 + [87614004039](https://github.com/stablyai/orca/actions/runs/29496035270/job/87614004039). The Windows + x64 oldest-baseline job + [87615011157](https://github.com/stablyai/orca/actions/runs/29496035270/job/87615011157) passes. +- The Windows arm64 oldest-baseline job + [87615011206](https://github.com/stablyai/orca/actions/runs/29496035270/job/87615011206) verifies + the 60-entry / 42-file / 85,213,511-expanded-byte tree and content identity, Node v24.18.0 ABI 137, + PTY exit 23 and 37x101 resize, five watcher events, and two-second resource settlement. Smoke takes + 6,063.5099 ms at 48,398,336-byte RSS and full verification takes 8,392.7748 ms. Its only rejection + is observed Windows build 26200 against required 26100; platform/arm64 checks pass and residual + gaps are empty. The aggregate run conclusion is failure only because this declared runner mismatch + remains intentionally fail-closed. +- Full-size extraction remains within budgets at 1,162.28–5,633.09 ms and + 33,939,456–46,018,560 incremental RSS bytes. Cold immutable-cache publication stays at + 1,384.24–7,156.65 ms and 39,288,832–60,940,288 incremental RSS bytes; warm lookup stays at + 167.72–1,193.01 ms and 0–6,414,336 incremental RSS bytes. Retention stays at 10.56–126.99 ms and + eviction at 19.54–100.30 ms. Every measurement passes its existing budget. +- Adjacent exact-head checks pass: PR Checks run + [29496035043](https://github.com/stablyai/orca/actions/runs/29496035043), job + [87613080471](https://github.com/stablyai/orca/actions/runs/29496035043/job/87613080471); Golden E2E + run [29496036617](https://github.com/stablyai/orca/actions/runs/29496036617), macOS job + [87613085499](https://github.com/stablyai/orca/actions/runs/29496036617/job/87613085499) and Linux + job [87613085648](https://github.com/stablyai/orca/actions/runs/29496036617/job/87613085648); + computer-use run [29496035157](https://github.com/stablyai/orca/actions/runs/29496035157), Windows + job [87613080650](https://github.com/stablyai/orca/actions/runs/29496035157/job/87613080650) and + Ubuntu job [87613080659](https://github.com/stablyai/orca/actions/runs/29496035157/job/87613080659). +- Boundary/residual gaps: the accepted source-tree module still has no other `src/main` importer and + performs no I/O. GitHub client runners do not prove filesystem mutation races, live SSH, SFTP or + system-SSH channel behavior, remote staging/install, or full-size transfer. Pre-scan, streaming, + every product/settings/fallback/tuple/publication/default path, and SignPath remain open. Legacy + remains the sole production/default path and every tuple stays disabled. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -16263,9 +16314,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the locally green Milestone 6 disconnected transport-neutral verified source-tree -contract, then require exact-head all-six native client proof plus adjacent PR checks before accepting -the item or advancing to the filesystem pre-scan package. +Audit only Milestone 6's smallest disconnected full-tree source pre-scan. Determine the exact +source-tree and lease inputs; bounded traversal and file-state snapshots; before/after mutation, +symlink/special/extra/missing/collision rejection; cancellation and resource ownership; and the +immutable output consumed by later transfer families before adding tests or implementation. The +pre-scan must create no remote files or open SSH channels. Do not enumerate the filesystem, open an SSH channel, transfer or install bytes, modify platform detection/selectors, add an Electron/startup consumer, mode wiring, fallback, tuple enablement, release publication, or default behavior. From 74104b623511164d7c6b6ae3bd2ee25d6734cb53 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 08:28:29 -0400 Subject: [PATCH 208/267] feat: authenticate SSH relay transfer sources Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 20 +- ...github-release-implementation-checklist.md | 134 ++++++- ...lay-artifact-cache-entry-full-size.test.ts | 45 ++- .../ssh/ssh-relay-runtime-source-scan.test.ts | 358 ++++++++++++++++++ src/main/ssh/ssh-relay-runtime-source-scan.ts | 288 ++++++++++++++ 7 files changed, 831 insertions(+), 17 deletions(-) create mode 100644 src/main/ssh/ssh-relay-runtime-source-scan.test.ts create mode 100644 src/main/ssh/ssh-relay-runtime-source-scan.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index f8a144310e6..397588130e7 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -283,6 +283,7 @@ jobs: src/main/ssh/ssh-relay-artifact-acquisition.test.ts \ src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts \ src/main/ssh/ssh-relay-runtime-source-tree.test.ts \ + src/main/ssh/ssh-relay-runtime-source-scan.test.ts \ src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts \ src/main/ssh/ssh-relay-official-manifest.test.ts \ src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts \ @@ -836,6 +837,7 @@ jobs: src/main/ssh/ssh-relay-artifact-acquisition.test.ts ` src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts ` src/main/ssh/ssh-relay-runtime-source-tree.test.ts ` + src/main/ssh/ssh-relay-runtime-source-scan.test.ts ` src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts ` src/main/ssh/ssh-relay-official-manifest.test.ts ` src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 444ee12cc19..edc5083ceb6 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -141,6 +141,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-artifact-acquisition.test.ts', 'ssh-relay-artifact-acquisition-integration.test.ts', 'ssh-relay-runtime-source-tree.test.ts', + 'ssh-relay-runtime-source-scan.test.ts', 'ssh-relay-compiled-manifest-trust.test.ts', 'ssh-relay-official-manifest.test.ts', 'ssh-relay-manifest-accepted-keys.test.ts', 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 e4de050b23f..0023ad85d87 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 @@ -9,9 +9,23 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected -transport-neutral source-tree contract CI is closed; audit only the full-tree source pre-scan next, -2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected full-tree +source pre-scan is locally green; commit and exact-head native/full-size CI are next, 2026-07-15, +Codex implementation owner.** +`E-M6-SOURCE-PRESCAN-AUDIT-001` fixes the next local-only boundary: consume only the accepted source +tree plus one exact signal; assert its borrowed lease; boundedly enumerate and hash every real entry; +reject mutation, symlink/junction, special, missing/extra/colliding, mode/size/hash/aggregate drift; +return frozen bigint state snapshots for mandatory later transfer-time comparisons; and close every +local handle. Full-size latency/RSS/handle proof is required. No remote resource or product path is +introduced. +`E-M6-SOURCE-PRESCAN-LOCAL-001` passes 16 purpose cases with one Linux-only local skip, 79 adjacent +cases, 538 broad relay cases, 280 release/workflow cases, typecheck, full lint, formatting, diff, +protected-resolver, and no-product-import gates. It proves one-directory/one-file/64-KiB bounds, +cancellation and close failures, complete tree hashing, mutation/integrity rejection, frozen state +snapshots, and both native workflow invocations. The local full-size case correctly skips without a +native artifact; exact-head all-six native metrics remain required before checking the item. +`E-M6-SOURCE-PRESCAN-LOCAL-RED-001` records the earlier expected missing-module failures for purpose +and full-size measurement plus the workflow oracle's six-pass/one-fail result. `E-M6-SOURCE-TREE-CONTRACT-AUDIT-001` fixes a pure ready-acquisition → immutable descriptor boundary: exact verified manifest/cache identity and signed limits, deterministic ASCII ordering, client-native paths, borrowed live-lease assertion, and later shared use by pre-scan plus SFTP, 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 6fd978b643c..ed286d2772b 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: audit only the smallest disconnected full-tree source pre-scan after source-tree CI closure**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **SOURCE-TREE CONTRACT CI CLOSED / PRE-SCAN AUDIT NEXT — 2026-07-15, Codex implementation owner** — `E-M6-SOURCE-TREE-CONTRACT-CI-001` closes exact-head all-six native client proof, both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use; Windows arm64 retains only the declared build-26200/26100 runner mismatch after full runtime proof. Audit only a disconnected filesystem pre-scan that consumes the accepted source-tree contract and creates no remote files. Do not open SSH channels, transfer/install bytes, add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected full-tree source pre-scan is locally green; exact-head native/full-size CI is the next gate**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **SOURCE PRE-SCAN LOCAL GREEN / COMMIT AND CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-SOURCE-PRESCAN-LOCAL-001` proves bounded complete source authentication, immutable snapshots, cancellation/close behavior, adjacent and broad regressions, workflow inclusion, and isolation from product callers. Commit and push the package, then require all-six native purpose suites and exact full-size artifact scan metrics before checking the item. Do not open SSH channels, transfer/install bytes, add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -150,8 +150,8 @@ same change as the work it records. - Completed package: disconnected full host-evidence composition is green locally and on every primary native client under E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001 and E-M5-HOST-EVIDENCE-COMPOSITION-CI-001. It has no product caller. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit only Milestone - 6's smallest disconnected full-tree source pre-scan after source-tree CI closure. Do not open SSH +- Active package: **In progress — 2026-07-15, Codex implementation owner** — add RED proof, then + implement only Milestone 6's audited disconnected full-tree source pre-scan. Do not open SSH channels, transfer or install bytes, or add product/settings/fallback/tuple/publication/default behavior. - Completed Work Package 2 gate: target-native Windows source-signature reports from exact-head @@ -1159,7 +1159,8 @@ Current behavior must not be assumed adequate for a much larger runtime: E-M6-SOURCE-TREE-CONTRACT-LOCAL-001, and E-M6-SOURCE-TREE-CONTRACT-CI-001. - [ ] Pre-scan and reject local source mutation, symlinks, special files, extra files, and path collisions before creating remote files. **In progress — 2026-07-15, Codex implementation - owner: audit only; no tests or implementation yet.** + owner: audit complete under E-M6-SOURCE-PRESCAN-AUDIT-001; RED proof recorded under + E-M6-SOURCE-PRESCAN-LOCAL-RED-001.** - [ ] Stream files with bounded buffers; prohibit whole-tree memory materialization. - [ ] Pass one AbortSignal through enumeration, channel creation, reads, writes, permission repair, and remote cleanup. @@ -16256,6 +16257,121 @@ config/vitest.config.ts --maxWorkers=1 --silent=true config/scripts/ssh-relay-ru every product/settings/fallback/tuple/publication/default path, and SignPath remain open. Legacy remains the sole production/default path and every tuple stays disabled. +### E-M6-SOURCE-PRESCAN-AUDIT-001 — bounded complete local source authentication and snapshot + +- Date/owner: 2026-07-15, Codex implementation owner. +- Exact input/output boundary: accept only the immutable `SshRelayRuntimeSourceTree` closed by + E-M6-SOURCE-TREE-CONTRACT-CI-001 plus one required caller-owned `AbortSignal`. Return a deeply + frozen scanned tree that preserves the same authenticated identity/order/descriptors and adds an + immutable root, directory, and file state snapshot for later transfer-time comparison. Do not + accept a raw path, manifest, cache entry, or file list. +- Complete pre-remote scan: assert the borrowed cache lease before any filesystem operation and + after all final state checks. Require the runtime root and every declared directory/file to remain + exact real entries; reject symlinks/junctions, special files, missing/extra entries, exact and + case-fold collisions, mode drift on POSIX, size/count/aggregate drift, and SHA-256 disagreement. + Hash every file before any future transport creates remote staging. +- Bounded traversal: use one open directory handle at a time, one open regular-file handle at a + time, one reusable 64 KiB buffer, and the authenticated maximum of 5,000 total entries / 350 MiB + expanded bytes / 250 MiB per file. Stop immediately when actual entries exceed the expected set; + do not materialize file contents or an unbounded directory listing. +- Race/snapshot contract: capture bigint device, inode, size, mtime, and ctime state around each file + open/read/hash and compare the path again after close. Capture root/directory state before + traversal and recheck every directory plus every file after all hashes. A replacement or mutation + at any point fails closed. Later transfer must compare these exact snapshots before opening any + remote file, hash the transmitted bytes while reading, compare again after each read, and abort + remote work on drift; pre-scan alone does not claim to close post-scan races. +- Cancellation/ownership: check the exact signal before lease assertion, directory open/read, + metadata operations, every file chunk, final state checks, and final lease assertion. Await every + directory/file close on success, failure, and cancellation; close failures propagate. The scan + borrows but never releases the acquisition lease and owns no remote resource or cleanup. +- Test seam: a narrow domain-named filesystem-operations injection may wrap real `lstat`, directory + read/close, and file stat/read/close so mutation, cancellation, and close ordering are deterministic + in purpose tests. Production uses fixed defaults; injection cannot change signed expectations. +- Required RED/green proof: real signed Linux and Windows cache trees; deterministic immutable + snapshots; exact signal and lease ordering; root/directory/file mutation; symlink/junction, + special, extra, missing, collision, mode, size, hash, and aggregate rejection; mid-enumeration and + mid-read cancellation; handle-close success/failure ordering; 64 KiB maximum reads; zero whole-file + materialization; exact full-size native artifact latency/RSS/open-handle measurements; no SSH/ + product caller; and inclusion in both native workflow families. +- Deliberately absent: remote path construction, directory/file creation, SSH/SFTP/child process, + transfer framing/writes/progress, permission repair, cleanup, install/launch, settings/modes, + fallback, Electron/startup, `ORCA_RELAY_PATH`, tuple enablement, publication, defaults, and + SignPath. Legacy remains the sole production/default path and every tuple stays disabled. + +### E-M6-SOURCE-PRESCAN-LOCAL-RED-001 — missing pre-scan and native/full-size proof + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0, + pnpm 10.24.0, atop local evidence commit `fea8e4ad1` plus the audit/test-only diff. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-source-scan.test.ts` — EXPECTED FAIL, exit 1: the + absent module prevents collection; zero tests in 808 ms Vitest / 2.55 seconds wall, + 137,756,672-byte maximum RSS. +- Full-size command: the same command with + `src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts` — EXPECTED FAIL, exit 1: its new + exact-artifact scan measurement cannot import the absent module; zero tests in 690 ms Vitest / + 2.43 seconds wall, 132,317,184-byte maximum RSS. +- Workflow-oracle command: the same command with + `config/scripts/ssh-relay-runtime-workflow.test.mjs` — EXPECTED FAIL, exit 1: six cases pass and + native inclusion fails because the purpose suite has one source occurrence rather than three; + 512 ms Vitest / 2.25 seconds wall, 133,005,312-byte maximum RSS. +- Scope at RED: the protected resolver files have no diff. No scan module, SSH/product caller, + remote resource, transfer/install, settings/fallback, tuple, publication, or default behavior + exists. + +### E-M6-SOURCE-PRESCAN-LOCAL-001 — disconnected bounded source pre-scan is locally green + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0, + pnpm 10.24.0, atop evidence commit `fea8e4ad1` plus the implementation diff. Native Node 24 and + exact artifact-size measurements remain the next CI gate. +- Implemented boundary: `ssh-relay-runtime-source-scan.ts` consumes only the accepted source tree, + one exact signal, and its narrow test operations seam. It asserts but never releases the borrowed + lease; enumerates with at most one directory handle; hashes every file through at most one file + handle and one reusable 64 KiB buffer; rejects linked/special/missing/extra/colliding/mode/size/ + hash/state drift; rechecks root, every directory, every file, and the lease; and returns frozen + bigint device/inode/size/mtime/ctime snapshots for later transfer-time comparison. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-source-scan.test.ts` — PASS, exit 0: 16 passed and one + Linux-only case-fold test skipped on macOS; 4.49 seconds Vitest / 8.27 seconds wall and + 134,807,552-byte maximum RSS. The cases cover signed Linux/Windows cache trees, deep freezing, + exact lease ordering, pre/mid-enumeration/mid-read cancellation, no post-cancel child metadata + lookup, directory/file close failure propagation, symlink/special/missing/extra/hash/mode/ + collision rejection, post-hash mutation, and handle/buffer bounds. Instrumentation observes peak + one directory plus one file handle, zero remaining handles, and reads no larger than 65,536 bytes. +- Workflow oracle: the exact command above with + `config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, exit 0: 7/7 in 869 ms Vitest / 2.66 + seconds wall and 132,349,952-byte maximum RSS. Both POSIX and Windows native commands include the + source pre-scan purpose suite. +- Focused adjacent command: the exact Vitest prefix above with source-scan, source-tree, cache-entry, + cache-population, cache-population-integration, cache-resolution, acquisition, + acquisition-integration, and workflow-oracle tests — PASS, exit 0: 9 files, 79 passed and one + platform skip; 10.59 seconds Vitest / 12.47 seconds wall and 140,689,408-byte maximum RSS. +- Broad relay command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts` — PASS, exit 0: 41 files passed, three skipped; + 538 tests passed and five skipped in 34.97 seconds Vitest / 37.78 seconds wall with + 262,488,064-byte maximum RSS. Existing stale-lock diagnostics and the Node 26 `module.register()` + deprecation warning are unchanged. +- Release/workflow command: the same prefix with + `config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, exit 0: 50 files and 280 tests in 29.32 + seconds Vitest / 31.23 seconds wall with 190,332,928-byte maximum RSS. +- Static gates: `pnpm typecheck`, targeted `oxlint`, and full `pnpm lint` all pass. Full lint retains + only pre-existing warnings and passes 41 reliability gates, the 355-suppression max-lines ratchet, + bundled skill guides, 9,837 localization references, locale parity, and zero allowlisted coverage + candidates. `git diff --check` and changed-file `oxfmt --check` pass. +- Isolation gates: protected `ssh-remote-node-resolution.ts` and its test have an empty diff. Searches + for `scanSshRelayRuntimeSourceTree`, its module, and its scanned type find no non-test importer in + `src/main`; therefore this package opens no SSH channel or remote resource and changes no product, + settings, fallback, tuple, publication, or default path. +- Full-size local command: the exact Vitest prefix with + `ssh-relay-artifact-cache-entry-full-size.test.ts` exits 0 with its one measurement case skipped + because no exact native artifact input is present; 1.11 seconds Vitest / 3.23 seconds wall and + 132,169,728-byte maximum RSS. This is not full-size proof. +- Residual gate: do not check the Milestone 6 pre-scan item until the committed exact head passes the + purpose suite on all six native clients and reports scan latency/RSS over every exact full-size + artifact, both Linux supplements, Windows x64 baseline, the declared Windows arm64 build-only + outcome, and adjacent PR/Golden/computer-use checks. GitHub runners still do not prove live SSH, + SFTP/system-SSH transfer, remote staging/install, or post-scan transfer races. Streaming and every + product/mode/fallback/tuple/publication/default path remain absent; legacy remains the sole default. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -16314,11 +16430,9 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Audit only Milestone 6's smallest disconnected full-tree source pre-scan. Determine the exact -source-tree and lease inputs; bounded traversal and file-state snapshots; before/after mutation, -symlink/special/extra/missing/collision rejection; cancellation and resource ownership; and the -immutable output consumed by later transfer families before adding tests or implementation. The -pre-scan must create no remote files or open SSH channels. +Implement only the RED-proven Milestone 6 disconnected full-tree source pre-scan and wire its purpose +suite into both native workflow families, then run local/full-size evidence gates. The pre-scan must +create no remote files or open SSH channels. Do not enumerate the filesystem, open an SSH channel, transfer or install bytes, modify platform detection/selectors, add an Electron/startup consumer, mode wiring, fallback, tuple enablement, release publication, or default behavior. diff --git a/src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts b/src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts index f50f6e88b53..7800b561762 100644 --- a/src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts +++ b/src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts @@ -17,6 +17,11 @@ import { SSH_RELAY_ARTIFACT_CACHE_IN_USE_LIMITS } from './ssh-relay-artifact-cache-in-use-lease' import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector' +import { + scanSshRelayRuntimeSourceTree, + SSH_RELAY_RUNTIME_SOURCE_SCAN_LIMITS +} from './ssh-relay-runtime-source-scan' +import { createSshRelayRuntimeSourceTree } from './ssh-relay-runtime-source-tree' type MeasurementIdentity = { tupleId: SshRelaySelectedArtifact['tupleId'] @@ -116,13 +121,31 @@ describe.skipIf(!hasMeasurementInput)('SSH relay full-size immutable artifact ca if (warm.result.kind !== 'hit') { throw new Error('Full-size cache entry unexpectedly missed after cold publication') } + const warmEntry = warm.result.entry const lease = await acquireSshRelayArtifactCacheInUseLease({ cacheRoot: cacheRoot as string, - entry: warm.result.entry + entry: warmEntry }) - const retained = await measure(() => - evictSshRelayArtifactCache({ cacheRoot: cacheRoot as string, maximumBytes: 0 }) - ).finally(() => lease.release()) + const { scan, retained } = await (async () => { + try { + const sourceTree = createSshRelayRuntimeSourceTree({ + kind: 'ready', + source: 'cache', + artifact, + entry: warmEntry, + lease + }) + const scan = await measure(() => + scanSshRelayRuntimeSourceTree(sourceTree, new AbortController().signal) + ) + const retained = await measure(() => + evictSshRelayArtifactCache({ cacheRoot: cacheRoot as string, maximumBytes: 0 }) + ) + return { scan, retained } + } finally { + await lease.release() + } + })() const eviction = await measure(() => evictSshRelayArtifactCache({ cacheRoot: cacheRoot as string, maximumBytes: 0 }) ) @@ -136,6 +159,8 @@ describe.skipIf(!hasMeasurementInput)('SSH relay full-size immutable artifact ca coldIncrementalRssBytes: cold.incrementalRssBytes, warmElapsedMs: warm.elapsedMs, warmIncrementalRssBytes: warm.incrementalRssBytes, + scanElapsedMs: scan.elapsedMs, + scanIncrementalRssBytes: scan.incrementalRssBytes, retainedElapsedMs: retained.elapsedMs, retainedIncrementalRssBytes: retained.incrementalRssBytes, evictionElapsedMs: eviction.elapsedMs, @@ -151,6 +176,12 @@ describe.skipIf(!hasMeasurementInput)('SSH relay full-size immutable artifact ca expandedBytes: identity.archive.expandedSize }) expect(warm.result).toEqual({ kind: 'hit', entry: cold.result }) + expect(scan.result).toMatchObject({ + tupleId: identity.tupleId, + contentId: identity.contentId, + fileCount: identity.archive.fileCount, + expandedBytes: identity.archive.expandedSize + }) expect(retained.result).toMatchObject({ initialBytes: expect.any(Number), finalBytes: expect.any(Number), @@ -178,6 +209,12 @@ describe.skipIf(!hasMeasurementInput)('SSH relay full-size immutable artifact ca SSH_RELAY_ARTIFACT_CACHE_ENTRY_LIMITS.maximumIncrementalMemoryBytes ) } + expect(scan.elapsedMs).toBeLessThanOrEqual( + SSH_RELAY_RUNTIME_SOURCE_SCAN_LIMITS.measurementTimeoutMs + ) + expect(scan.incrementalRssBytes).toBeLessThanOrEqual( + SSH_RELAY_RUNTIME_SOURCE_SCAN_LIMITS.maximumIncrementalMemoryBytes + ) for (const measurement of [retained, eviction]) { expect(measurement.elapsedMs).toBeLessThanOrEqual( SSH_RELAY_ARTIFACT_CACHE_EVICTION_LIMITS.transactionTimeoutMs diff --git a/src/main/ssh/ssh-relay-runtime-source-scan.test.ts b/src/main/ssh/ssh-relay-runtime-source-scan.test.ts new file mode 100644 index 00000000000..701eeb6bb26 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-source-scan.test.ts @@ -0,0 +1,358 @@ +import { execFile } from 'node:child_process' +import { + chmod, + lstat, + mkdir, + mkdtemp, + open, + opendir, + readFile, + rm, + symlink, + writeFile +} from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { promisify } from 'node:util' + +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { publishSshRelayArtifactCacheEntry } from './ssh-relay-artifact-cache-entry' +import { createSshRelayArtifactCacheEntryFixture } from './ssh-relay-artifact-cache-entry-fixture' +import { + acquireSshRelayArtifactCacheInUseLease, + type SshRelayArtifactCacheInUseLease +} from './ssh-relay-artifact-cache-in-use-lease' +import { createSshRelayRuntimeSourceTree } from './ssh-relay-runtime-source-tree' +import { + scanSshRelayRuntimeSourceTree, + type SshRelayRuntimeSourceScanOperations +} from './ssh-relay-runtime-source-scan' + +const execFileAsync = promisify(execFile) +const cleanupRoots = new Set() +const cleanupLeases = new Set() + +async function sourceFixture(os: 'linux' | 'win32' = 'linux') { + const root = await mkdtemp(join(tmpdir(), 'orca-relay-source-scan-')) + cleanupRoots.add(root) + const inputRoot = join(root, 'input') + await mkdir(inputRoot) + const fixture = await createSshRelayArtifactCacheEntryFixture({ root: inputRoot, os }) + const cacheRoot = join(root, 'cache') + const entry = await publishSshRelayArtifactCacheEntry({ + cacheRoot, + artifact: fixture.artifact, + archivePath: fixture.archivePath + }) + const lease = await acquireSshRelayArtifactCacheInUseLease({ cacheRoot, entry }) + cleanupLeases.add(lease) + const tree = createSshRelayRuntimeSourceTree({ + kind: 'ready', + source: 'cache', + artifact: fixture.artifact, + entry, + lease + }) + return { root, tree } +} + +afterEach(async () => { + await Promise.all([...cleanupLeases].map((lease) => lease.release().catch(() => {}))) + cleanupLeases.clear() + await Promise.all([...cleanupRoots].map((root) => rm(root, { recursive: true, force: true }))) + cleanupRoots.clear() +}) + +describe('SSH relay runtime source pre-scan', () => { + it.each(['linux', 'win32'] as const)( + 'authenticates and freezes a complete signed %s cache tree', + async (os) => { + const { tree } = await sourceFixture(os) + + const scanned = await scanSshRelayRuntimeSourceTree(tree, new AbortController().signal) + + expect(scanned).toMatchObject({ + tupleId: tree.tupleId, + contentId: tree.contentId, + runtimeRoot: tree.runtimeRoot, + fileCount: tree.fileCount, + expandedBytes: tree.expandedBytes + }) + expect(scanned.directories.map((entry) => entry.path)).toEqual( + tree.directories.map((entry) => entry.path) + ) + expect(scanned.files.map((entry) => entry.path)).toEqual( + tree.files.map((entry) => entry.path) + ) + expect(Object.isFrozen(scanned)).toBe(true) + expect(Object.isFrozen(scanned.runtimeRootState)).toBe(true) + expect(scanned.directories.every((entry) => Object.isFrozen(entry.state))).toBe(true) + expect(scanned.files.every((entry) => Object.isFrozen(entry.state))).toBe(true) + expect(typeof scanned.files[0].state.mtimeNs).toBe('bigint') + } + ) + + it('asserts the borrowed lease before and after the complete scan without releasing it', async () => { + const { tree } = await sourceFixture() + const assertLeaseOwned = vi.fn(tree.assertLeaseOwned) + + await scanSshRelayRuntimeSourceTree({ ...tree, assertLeaseOwned }, new AbortController().signal) + + expect(assertLeaseOwned).toHaveBeenCalledTimes(2) + }) + + it('settles pre-cancellation before lease or filesystem work', async () => { + const { tree } = await sourceFixture() + const assertLeaseOwned = vi.fn(tree.assertLeaseOwned) + const controller = new AbortController() + controller.abort(new Error('cancel source scan')) + + await expect( + scanSshRelayRuntimeSourceTree({ ...tree, assertLeaseOwned }, controller.signal) + ).rejects.toThrow(/cancel source scan/i) + expect(assertLeaseOwned).not.toHaveBeenCalled() + }) + + it.each([ + [ + 'extra file', + async (tree: Awaited>['tree']) => { + await writeFile(join(tree.runtimeRoot, 'extra.bin'), 'extra') + } + ], + [ + 'missing file', + async (tree: Awaited>['tree']) => { + await rm(tree.files[0].localPath) + } + ], + [ + 'same-size hash drift', + async (tree: Awaited>['tree']) => { + const bytes = await readFile(tree.files[0].localPath) + bytes[0] ^= 0xff + await writeFile(tree.files[0].localPath, bytes) + } + ] + ] as const)('rejects %s before returning a snapshot', async (_name, mutate) => { + const { tree } = await sourceFixture() + await mutate(tree) + + await expect(scanSshRelayRuntimeSourceTree(tree, new AbortController().signal)).rejects.toThrow( + /extra|undeclared|missing|integrity|hash/i + ) + }) + + it('rejects a linked directory before following it', async () => { + const { root, tree } = await sourceFixture() + const bin = join(tree.runtimeRoot, 'bin') + const external = join(root, 'external-bin') + await rm(bin, { recursive: true }) + await mkdir(external) + await symlink(external, bin, process.platform === 'win32' ? 'junction' : 'dir') + + await expect(scanSshRelayRuntimeSourceTree(tree, new AbortController().signal)).rejects.toThrow( + /link|type|directory/i + ) + }) + + it.skipIf(process.platform === 'win32')('rejects a special file', async () => { + const { tree } = await sourceFixture() + await rm(tree.files[0].localPath) + await execFileAsync('mkfifo', [tree.files[0].localPath]) + + await expect(scanSshRelayRuntimeSourceTree(tree, new AbortController().signal)).rejects.toThrow( + /special|type|regular/i + ) + }) + + it.skipIf(process.platform !== 'linux')('rejects case-fold collisions', async () => { + const { tree } = await sourceFixture() + await writeFile(join(tree.runtimeRoot, 'RELAY.JS'), 'collision') + + await expect(scanSshRelayRuntimeSourceTree(tree, new AbortController().signal)).rejects.toThrow( + /collision/i + ) + }) + + it.skipIf(process.platform === 'win32')('rejects executable mode drift', async () => { + const { tree } = await sourceFixture() + const executable = tree.files.find((file) => file.mode === 0o755)! + await chmod(executable.localPath, 0o644) + + await expect(scanSshRelayRuntimeSourceTree(tree, new AbortController().signal)).rejects.toThrow( + /mode/i + ) + }) + + it('closes an open file and propagates mid-read cancellation', async () => { + const { tree } = await sourceFixture() + const controller = new AbortController() + const close = vi.fn(async () => {}) + const operations: Partial = { + openFile: async (path) => { + const handle = await open(path, 'r') + return { + stat: () => handle.stat({ bigint: true }), + read: async (...args) => { + const result = await handle.read(...args) + controller.abort(new Error('cancel during source read')) + return result + }, + close: async () => { + await handle.close() + await close() + } + } + } + } + + await expect( + scanSshRelayRuntimeSourceTree(tree, controller.signal, operations) + ).rejects.toThrow(/cancel during source read/i) + expect(close).toHaveBeenCalledTimes(1) + }) + + it('closes an open directory and propagates mid-enumeration cancellation', async () => { + const { tree } = await sourceFixture() + const controller = new AbortController() + const close = vi.fn(async () => {}) + const scanLstat = vi.fn((path: string) => lstat(path, { bigint: true })) + const operations: Partial = { + lstat: scanLstat, + openDirectory: async (path) => { + const handle = await opendir(path) + return { + read: async () => { + const entry = await handle.read() + controller.abort(new Error('cancel during source enumeration')) + return entry + }, + close: async () => { + await handle.close() + await close() + } + } + } + } + + await expect( + scanSshRelayRuntimeSourceTree(tree, controller.signal, operations) + ).rejects.toThrow(/cancel during source enumeration/i) + expect(close).toHaveBeenCalledTimes(1) + expect(scanLstat).toHaveBeenCalledTimes(1) + }) + + it.each(['directory', 'file'] as const)('propagates an open %s close failure', async (kind) => { + const { tree } = await sourceFixture() + const operations: Partial = + kind === 'directory' + ? { + openDirectory: async (path) => { + const handle = await opendir(path) + return { + read: () => handle.read(), + close: async () => { + await handle.close() + throw new Error('source directory close failed') + } + } + } + } + : { + openFile: async (path) => { + const handle = await open(path, 'r') + return { + stat: () => handle.stat({ bigint: true }), + read: (buffer, offset, length, position) => + handle.read(buffer, offset, length, position), + close: async () => { + await handle.close() + throw new Error('source file close failed') + } + } + } + } + + await expect( + scanSshRelayRuntimeSourceTree(tree, new AbortController().signal, operations) + ).rejects.toThrow(new RegExp(`source ${kind} close failed`, 'i')) + }) + + it('never keeps more than one directory or file handle open', async () => { + const { tree } = await sourceFixture() + let openDirectories = 0 + let openFiles = 0 + let peakDirectories = 0 + let peakFiles = 0 + const operations: Partial = { + openDirectory: async (path) => { + const handle = await opendir(path) + openDirectories += 1 + peakDirectories = Math.max(peakDirectories, openDirectories) + return { + read: () => handle.read(), + close: async () => { + await handle.close() + openDirectories -= 1 + } + } + }, + openFile: async (path) => { + const handle = await open(path, 'r') + openFiles += 1 + peakFiles = Math.max(peakFiles, openFiles) + return { + stat: () => handle.stat({ bigint: true }), + read: (buffer, offset, length, position) => handle.read(buffer, offset, length, position), + close: async () => { + await handle.close() + openFiles -= 1 + } + } + } + } + + await scanSshRelayRuntimeSourceTree(tree, new AbortController().signal, operations) + + expect({ openDirectories, openFiles, peakDirectories, peakFiles }).toEqual({ + openDirectories: 0, + openFiles: 0, + peakDirectories: 1, + peakFiles: 1 + }) + }) + + it('uses at most one 64 KiB read buffer and rejects a mutation after hashing', async () => { + const { tree } = await sourceFixture() + const readLengths: number[] = [] + let mutated = false + const operations: Partial = { + openFile: async (path) => { + const handle = await open(path, 'r') + return { + stat: () => handle.stat({ bigint: true }), + read: async (buffer, offset, length, position) => { + readLengths.push(length) + return handle.read(buffer, offset, length, position) + }, + close: async () => { + await handle.close() + if (!mutated) { + mutated = true + const bytes = await readFile(path) + bytes[0] ^= 0xff + await writeFile(path, bytes) + } + } + } + } + } + + await expect( + scanSshRelayRuntimeSourceTree(tree, new AbortController().signal, operations) + ).rejects.toThrow(/changed|mutation|state/i) + expect(Math.max(...readLengths)).toBeLessThanOrEqual(64 * 1024) + }) +}) diff --git a/src/main/ssh/ssh-relay-runtime-source-scan.ts b/src/main/ssh/ssh-relay-runtime-source-scan.ts new file mode 100644 index 00000000000..151ba95acfb --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-source-scan.ts @@ -0,0 +1,288 @@ +import { createHash } from 'node:crypto' +import { lstat, open, opendir } from 'node:fs/promises' +import { join } from 'node:path' + +import type { + SshRelayRuntimeSourceDirectory, + SshRelayRuntimeSourceFile, + SshRelayRuntimeSourceTree +} from './ssh-relay-runtime-source-tree' + +const CHUNK_BYTES = 64 * 1024 +const MEASUREMENT_TIMEOUT_MS = 2 * 60_000 +const MAXIMUM_INCREMENTAL_MEMORY_BYTES = 80 * 1024 * 1024 + +type SourceMetadata = { + dev: bigint + ino: bigint + size: bigint + mtimeNs: bigint + ctimeNs: bigint + mode: bigint + isFile: () => boolean + isDirectory: () => boolean + isSymbolicLink: () => boolean +} + +type SourceDirectoryHandle = { + read: () => Promise<{ name: string } | null> + close: () => Promise +} + +type SourceFileHandle = { + stat: () => Promise + read: ( + buffer: Buffer, + offset: number, + length: number, + position: number + ) => Promise<{ bytesRead: number }> + close: () => Promise +} + +export type SshRelayRuntimeSourceScanOperations = Readonly<{ + lstat: (path: string) => Promise + openDirectory: (path: string) => Promise + openFile: (path: string) => Promise +}> + +export type SshRelayRuntimeSourceState = Readonly<{ + dev: bigint + ino: bigint + size: bigint + mtimeNs: bigint + ctimeNs: bigint +}> + +export type SshRelayRuntimeScannedSourceTree = Readonly< + Omit & { + runtimeRootState: SshRelayRuntimeSourceState + directories: readonly Readonly< + SshRelayRuntimeSourceDirectory & { state: SshRelayRuntimeSourceState } + >[] + files: readonly Readonly[] + } +> + +const DEFAULT_OPERATIONS: SshRelayRuntimeSourceScanOperations = Object.freeze({ + lstat: (path) => lstat(path, { bigint: true }), + openDirectory: (path) => opendir(path), + openFile: async (path) => { + const handle = await open(path, 'r') + return { + stat: () => handle.stat({ bigint: true }), + read: (buffer, offset, length, position) => handle.read(buffer, offset, length, position), + close: () => handle.close() + } + } +}) + +function sourceState(metadata: SourceMetadata): SshRelayRuntimeSourceState { + return Object.freeze({ + dev: metadata.dev, + ino: metadata.ino, + size: metadata.size, + mtimeNs: metadata.mtimeNs, + ctimeNs: metadata.ctimeNs + }) +} + +function sameState(left: SshRelayRuntimeSourceState, right: SourceMetadata): boolean { + return ( + left.dev === right.dev && + left.ino === right.ino && + left.size === right.size && + left.mtimeNs === right.mtimeNs && + left.ctimeNs === right.ctimeNs + ) +} + +function assertExactType( + metadata: SourceMetadata, + expected: SshRelayRuntimeSourceDirectory | SshRelayRuntimeSourceFile, + path: string +): void { + if (metadata.isSymbolicLink()) { + throw new Error(`SSH relay runtime source contains a linked entry: ${path}`) + } + const matches = expected.type === 'directory' ? metadata.isDirectory() : metadata.isFile() + if (!matches) { + throw new Error(`SSH relay runtime source has a special or mismatched entry type: ${path}`) + } + if (process.platform !== 'win32' && Number(metadata.mode & 0o777n) !== expected.mode) { + throw new Error(`SSH relay runtime source mode changed: ${path}`) + } + if (expected.type === 'file' && metadata.size !== BigInt(expected.size)) { + throw new Error(`SSH relay runtime source file size changed: ${path}`) + } +} + +async function hashFile( + file: SshRelayRuntimeSourceFile, + expectedState: SshRelayRuntimeSourceState, + buffer: Buffer, + signal: AbortSignal, + operations: SshRelayRuntimeSourceScanOperations +): Promise { + signal.throwIfAborted() + const before = await operations.lstat(file.localPath) + assertExactType(before, file, file.path) + if (!sameState(expectedState, before)) { + throw new Error(`SSH relay runtime source file changed before hashing: ${file.path}`) + } + signal.throwIfAborted() + const handle = await operations.openFile(file.localPath) + const digest = createHash('sha256') + let bytes = 0 + try { + signal.throwIfAborted() + const opened = await handle.stat() + if (!opened.isFile() || !sameState(expectedState, opened)) { + throw new Error(`SSH relay runtime source file changed while opening: ${file.path}`) + } + while (bytes < file.size) { + signal.throwIfAborted() + const { bytesRead } = await handle.read( + buffer, + 0, + Math.min(buffer.length, file.size - bytes), + bytes + ) + if (bytesRead <= 0) { + break + } + bytes += bytesRead + digest.update(buffer.subarray(0, bytesRead)) + } + signal.throwIfAborted() + const after = await handle.stat() + if (!sameState(expectedState, after)) { + throw new Error(`SSH relay runtime source file changed while hashing: ${file.path}`) + } + } finally { + await handle.close() + } + signal.throwIfAborted() + const afterClose = await operations.lstat(file.localPath) + if (!sameState(expectedState, afterClose) || bytes !== file.size) { + throw new Error(`SSH relay runtime source file changed while hashing: ${file.path}`) + } + if (`sha256:${digest.digest('hex')}` !== file.sha256) { + throw new Error(`SSH relay runtime source file integrity changed: ${file.path}`) + } +} + +export async function scanSshRelayRuntimeSourceTree( + tree: SshRelayRuntimeSourceTree, + signal: AbortSignal, + overrides: Partial = {} +): Promise { + const operations = { ...DEFAULT_OPERATIONS, ...overrides } + signal.throwIfAborted() + await tree.assertLeaseOwned() + signal.throwIfAborted() + + const expected = new Map() + const expectedFolded = new Set() + for (const entry of [...tree.directories, ...tree.files]) { + if (entry.localPath !== join(tree.runtimeRoot, ...entry.path.split('/'))) { + throw new Error(`SSH relay runtime source descriptor path is inconsistent: ${entry.path}`) + } + const folded = entry.path.toLowerCase() + if (expected.has(entry.path) || expectedFolded.has(folded)) { + throw new Error(`SSH relay runtime source descriptor has a path collision: ${entry.path}`) + } + expected.set(entry.path, entry) + expectedFolded.add(folded) + } + + const rootMetadata = await operations.lstat(tree.runtimeRoot) + if (!rootMetadata.isDirectory() || rootMetadata.isSymbolicLink()) { + throw new Error('SSH relay runtime source root must be a real directory') + } + const runtimeRootState = sourceState(rootMetadata) + const states = new Map() + const seen = new Set() + const seenFolded = new Set() + const pending = [{ path: '', localPath: tree.runtimeRoot }] + while (pending.length > 0) { + signal.throwIfAborted() + const directory = pending.pop()! + const handle = await operations.openDirectory(directory.localPath) + try { + while (true) { + signal.throwIfAborted() + const child = await handle.read() + if (!child) { + break + } + const path = directory.path ? `${directory.path}/${child.name}` : child.name + const folded = path.toLowerCase() + if (seen.has(path) || seenFolded.has(folded)) { + throw new Error(`SSH relay runtime source has a path collision: ${path}`) + } + seen.add(path) + seenFolded.add(folded) + if (seen.size > expected.size) { + throw new Error(`SSH relay runtime source has an extra entry: ${path}`) + } + const declared = expected.get(path) + if (!declared) { + throw new Error(`SSH relay runtime source has an undeclared entry: ${path}`) + } + signal.throwIfAborted() + const metadata = await operations.lstat(declared.localPath) + assertExactType(metadata, declared, path) + states.set(path, sourceState(metadata)) + if (declared.type === 'directory') { + pending.push({ path, localPath: declared.localPath }) + } + } + } finally { + await handle.close() + } + } + for (const path of expected.keys()) { + if (!seen.has(path)) { + throw new Error(`SSH relay runtime source is missing a declared entry: ${path}`) + } + } + + const buffer = Buffer.allocUnsafe(CHUNK_BYTES) + for (const file of tree.files) { + await hashFile(file, states.get(file.path)!, buffer, signal, operations) + } + signal.throwIfAborted() + const rootAfter = await operations.lstat(tree.runtimeRoot) + if (!rootAfter.isDirectory() || !sameState(runtimeRootState, rootAfter)) { + throw new Error('SSH relay runtime source root changed during pre-scan') + } + for (const entry of [...tree.directories, ...tree.files]) { + signal.throwIfAborted() + const after = await operations.lstat(entry.localPath) + assertExactType(after, entry, entry.path) + if (!sameState(states.get(entry.path)!, after)) { + throw new Error(`SSH relay runtime source entry changed during pre-scan: ${entry.path}`) + } + } + signal.throwIfAborted() + await tree.assertLeaseOwned() + signal.throwIfAborted() + + return Object.freeze({ + ...tree, + runtimeRootState, + directories: Object.freeze( + tree.directories.map((entry) => Object.freeze({ ...entry, state: states.get(entry.path)! })) + ), + files: Object.freeze( + tree.files.map((entry) => Object.freeze({ ...entry, state: states.get(entry.path)! })) + ) + }) +} + +export const SSH_RELAY_RUNTIME_SOURCE_SCAN_LIMITS = Object.freeze({ + chunkBytes: CHUNK_BYTES, + measurementTimeoutMs: MEASUREMENT_TIMEOUT_MS, + maximumIncrementalMemoryBytes: MAXIMUM_INCREMENTAL_MEMORY_BYTES +}) From 724ed6295551a63cd710b4da3749548ac787a0ad Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 08:35:15 -0400 Subject: [PATCH 209/267] fix: classify SSH relay source collisions Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 14 +++-- ...github-release-implementation-checklist.md | 51 ++++++++++++++++++- .../ssh/ssh-relay-runtime-source-scan.test.ts | 26 ++++++++++ src/main/ssh/ssh-relay-runtime-source-scan.ts | 3 ++ 4 files changed, 89 insertions(+), 5 deletions(-) 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 0023ad85d87..14223be527f 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 @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected full-tree -source pre-scan is locally green; commit and exact-head native/full-size CI are next, 2026-07-15, -Codex implementation owner.** +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — order-independent +case-collision correction is locally green; commit and fresh exact-head all-six proof are next, +2026-07-15, Codex implementation owner.** `E-M6-SOURCE-PRESCAN-AUDIT-001` fixes the next local-only boundary: consume only the accepted source tree plus one exact signal; assert its borrowed lease; boundedly enumerate and hash every real entry; reject mutation, symlink/junction, special, missing/extra/colliding, mode/size/hash/aggregate drift; @@ -26,6 +26,14 @@ snapshots, and both native workflow invocations. The local full-size case correc native artifact; exact-head all-six native metrics remain required before checking the item. `E-M6-SOURCE-PRESCAN-LOCAL-RED-001` records the earlier expected missing-module failures for purpose and full-size measurement plus the workflow oracle's six-pass/one-fail result. +`E-M6-SOURCE-PRESCAN-CI-RED-001` records the Linux arm64 Node 24 runner finding: `RELAY.JS` +enumerated before signed `relay.js` fails closed as undeclared rather than collision. No SSH or +execution occurs. The remaining artifact run was cancelled after capture; collision classification +must become order-independent and receive fresh all-six native/full-size proof. +`E-M6-SOURCE-PRESCAN-COLLISION-FIX-LOCAL-001` makes the classification independent of enumeration +order and adds a platform-neutral case proving rejection before child metadata access. It passes 17 +purpose cases with one Linux-only local skip, 80 focused cases, 539 broad relay cases, typecheck, +full lint, and diff gates. Fresh exact-head Linux and all-six full-size proof remain required. `E-M6-SOURCE-TREE-CONTRACT-AUDIT-001` fixes a pure ready-acquisition → immutable descriptor boundary: exact verified manifest/cache identity and signed limits, deterministic ASCII ordering, client-native paths, borrowed live-lease assertion, and later shared use by pre-scan plus SFTP, 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 ed286d2772b..d7c675b74e3 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected full-tree source pre-scan is locally green; exact-head native/full-size CI is the next gate**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **SOURCE PRE-SCAN LOCAL GREEN / COMMIT AND CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-SOURCE-PRESCAN-LOCAL-001` proves bounded complete source authentication, immutable snapshots, cancellation/close behavior, adjacent and broad regressions, workflow inclusion, and isolation from product callers. Commit and push the package, then require all-six native purpose suites and exact full-size artifact scan metrics before checking the item. Do not open SSH channels, transfer/install bytes, add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: order-independent collision correction is locally green; fresh exact-head native/full-size CI is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **SOURCE PRE-SCAN COLLISION FIX LOCAL GREEN / COMMIT AND CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-SOURCE-PRESCAN-COLLISION-FIX-LOCAL-001` proves the Linux finding is corrected independently of filesystem enumeration order and without child metadata access. Commit and push the narrow correction, then require a fresh exact-head all-six native/full-size run. Do not open SSH channels, transfer/install bytes, add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -16372,6 +16372,53 @@ config/vitest.config.ts --maxWorkers=1 --silent=true config/scripts/ssh-relay-ru SFTP/system-SSH transfer, remote staging/install, or post-scan transfer races. Streaming and every product/mode/fallback/tuple/publication/default path remain absent; legacy remains the sole default. +### E-M6-SOURCE-PRESCAN-CI-RED-001 — Linux exposes order-dependent collision classification + +- Date/owner/head: 2026-07-15, Codex implementation owner; exact implementation commit + `74104b623511164d7c6b6ae3bd2ee25d6734cb53` on draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). +- Workflow/job: SSH Relay Runtime Artifacts run + [29498296906](https://github.com/stablyai/orca/actions/runs/29498296906), native Linux arm64 job + [87620486364](https://github.com/stablyai/orca/actions/runs/29498296906/job/87620486364), Node + 24.18.0. The artifact contract step fails after about one minute. +- Exact failure: the real case-sensitive filesystem enumerates added `RELAY.JS` before signed + `relay.js`; the scan rejects it as `SSH relay runtime source has an undeclared entry: RELAY.JS`, + while the purpose case requires `/collision/i`. Integrity still fails closed before hashing, + transfer, remote staging, or execution; the defect is classification order, not an acceptance or + fallback bypass. +- Action: the implementation owner cancelled the remaining already-doomed artifact run after + capturing the failure. Make an undeclared spelling whose case fold matches the authenticated set + a collision even when its signed peer has not yet been enumerated, and add a platform-neutral + enumeration-order test before requesting fresh exact-head proof. No box is checked from this run. + +### E-M6-SOURCE-PRESCAN-COLLISION-FIX-LOCAL-001 — classification is order-independent locally + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0, + pnpm 10.24.0, atop failed exact-head commit `74104b623` plus the narrow correction diff. +- Correction: when an enumerated spelling is absent from the exact authenticated map but present in + its case-folded set, reject it as a collision before any child metadata operation. A new + platform-neutral operations-seam case presents `RELAY.JS` before signed `relay.js`, requires the + collision classification, observes only the root `lstat`, and proves the directory handle closes. + The real Linux filesystem case remains enabled on Linux. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-source-scan.test.ts` — PASS, exit 0: 17 passed and the + real-filesystem Linux case skipped locally; 10.65 seconds Vitest / 18.64 seconds wall and + 128,745,472-byte maximum RSS. +- Focused adjacent command: the E-M6-SOURCE-PRESCAN-LOCAL-001 nine-file command — PASS, exit 0: 80 + passed and one platform skip in 11.14 seconds Vitest. The native-workflow oracle remains included. +- Broad relay command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-*.test.ts` — PASS, exit 0: 41 files passed, three skipped; 539 tests passed and + five skipped in 33.88 seconds Vitest. Existing stale-lock and Node 26 deprecation diagnostics are + unchanged. +- Static gates: `pnpm typecheck`, targeted `oxlint`, `git diff --check`, and full `pnpm lint` pass; + full lint retains only the pre-existing warnings and again passes reliability, max-lines, bundled + guide, and localization gates. Protected resolver files remain untouched and no production + importer exists. +- Residual gate: local macOS cannot execute the real case-sensitive filesystem case. Commit and push + the correction, then require that case plus the full purpose suite on both Linux native jobs and + every other native client, and require the complete exact-artifact scan metrics before closing the + package. The failed/cancelled run supplies no green cell. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/src/main/ssh/ssh-relay-runtime-source-scan.test.ts b/src/main/ssh/ssh-relay-runtime-source-scan.test.ts index 701eeb6bb26..b8441bc1dae 100644 --- a/src/main/ssh/ssh-relay-runtime-source-scan.test.ts +++ b/src/main/ssh/ssh-relay-runtime-source-scan.test.ts @@ -176,6 +176,32 @@ describe('SSH relay runtime source pre-scan', () => { ) }) + it('classifies a case-fold variant before its signed peer is enumerated', async () => { + const { tree } = await sourceFixture() + const close = vi.fn(async () => {}) + const scanLstat = vi.fn((path: string) => lstat(path, { bigint: true })) + let returnedVariant = false + const operations: Partial = { + lstat: scanLstat, + openDirectory: async () => ({ + read: async () => { + if (returnedVariant) { + return null + } + returnedVariant = true + return { name: 'RELAY.JS' } + }, + close + }) + } + + await expect( + scanSshRelayRuntimeSourceTree(tree, new AbortController().signal, operations) + ).rejects.toThrow(/collision/i) + expect(scanLstat).toHaveBeenCalledTimes(1) + expect(close).toHaveBeenCalledTimes(1) + }) + it.skipIf(process.platform === 'win32')('rejects executable mode drift', async () => { const { tree } = await sourceFixture() const executable = tree.files.find((file) => file.mode === 0o755)! diff --git a/src/main/ssh/ssh-relay-runtime-source-scan.ts b/src/main/ssh/ssh-relay-runtime-source-scan.ts index 151ba95acfb..b37cde31aa1 100644 --- a/src/main/ssh/ssh-relay-runtime-source-scan.ts +++ b/src/main/ssh/ssh-relay-runtime-source-scan.ts @@ -228,6 +228,9 @@ export async function scanSshRelayRuntimeSourceTree( } const declared = expected.get(path) if (!declared) { + if (expectedFolded.has(folded)) { + throw new Error(`SSH relay runtime source has a path collision: ${path}`) + } throw new Error(`SSH relay runtime source has an undeclared entry: ${path}`) } signal.throwIfAborted() From 16a58a23450eebcd99ed0f77ea4bf4be52f4b062 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 08:53:44 -0400 Subject: [PATCH 210/267] docs: record SSH relay source pre-scan CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 14 +++- ...github-release-implementation-checklist.md | 77 +++++++++++++++++-- 2 files changed, 82 insertions(+), 9 deletions(-) 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 14223be527f..44b1bb3d340 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 @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — order-independent -case-collision correction is locally green; commit and fresh exact-head all-six proof are next, -2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected source +pre-scan CI is closed; audit only the bounded client streaming boundary next, 2026-07-15, Codex +implementation owner.** `E-M6-SOURCE-PRESCAN-AUDIT-001` fixes the next local-only boundary: consume only the accepted source tree plus one exact signal; assert its borrowed lease; boundedly enumerate and hash every real entry; reject mutation, symlink/junction, special, missing/extra/colliding, mode/size/hash/aggregate drift; @@ -34,6 +34,14 @@ must become order-independent and receive fresh all-six native/full-size proof. order and adds a platform-neutral case proving rejection before child metadata access. It passes 17 purpose cases with one Linux-only local skip, 80 focused cases, 539 broad relay cases, typecheck, full lint, and diff gates. Fresh exact-head Linux and all-six full-size proof remain required. +`E-M6-SOURCE-PRESCAN-CI-001` closes exact correction commit `724ed6295`: all six primary native jobs, +both Linux supplements, Windows x64 baseline, PR Checks, Golden E2E, and computer-use pass. Every +Linux client passes all 18 pre-scan cases; macOS/Windows retain only declared platform skips. Exact +full-size scans cover 34–42 files and 85,213,511–124,846,430 bytes in 145.534–724.753 ms with +81,920–3,239,936 incremental RSS bytes, all within budget. Windows arm64 retains only the declared +hosted build-26200 rejection against 26100 after full Node/PTY/watcher/tree proof. Live SSH, +streaming, remote staging/install, product/mode/fallback/default wiring, publication, and SignPath +remain absent. `E-M6-SOURCE-TREE-CONTRACT-AUDIT-001` fixes a pure ready-acquisition → immutable descriptor boundary: exact verified manifest/cache identity and signed limits, deterministic ASCII ordering, client-native paths, borrowed live-lease assertion, and later shared use by pre-scan plus SFTP, 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 d7c675b74e3..e6335bd71f6 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: order-independent collision correction is locally green; fresh exact-head native/full-size CI is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **SOURCE PRE-SCAN COLLISION FIX LOCAL GREEN / COMMIT AND CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-SOURCE-PRESCAN-COLLISION-FIX-LOCAL-001` proves the Linux finding is corrected independently of filesystem enumeration order and without child metadata access. Commit and push the narrow correction, then require a fresh exact-head all-six native/full-size run. Do not open SSH channels, transfer/install bytes, add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected source pre-scan CI is closed; audit only the bounded client streaming boundary next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **SOURCE PRE-SCAN CLOSED / BOUNDED STREAM AUDIT NEXT — 2026-07-15, Codex implementation owner** — `E-M6-SOURCE-PRESCAN-CI-001` proves the corrected complete local scan on all six native clients and every exact full-size artifact. Audit the next transport-neutral bounded read/hash/progress boundary and record RED before implementation; do not open SSH channels or create remote staging in that package. Do not add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -1157,10 +1157,11 @@ Current behavior must not be assumed adequate for a much larger runtime: - [x] Define one transport-neutral source-tree contract based on the verified manifest. Completed by E-M6-SOURCE-TREE-CONTRACT-AUDIT-001, E-M6-SOURCE-TREE-CONTRACT-LOCAL-RED-001, E-M6-SOURCE-TREE-CONTRACT-LOCAL-001, and E-M6-SOURCE-TREE-CONTRACT-CI-001. -- [ ] Pre-scan and reject local source mutation, symlinks, special files, extra files, and path - collisions before creating remote files. **In progress — 2026-07-15, Codex implementation - owner: audit complete under E-M6-SOURCE-PRESCAN-AUDIT-001; RED proof recorded under - E-M6-SOURCE-PRESCAN-LOCAL-RED-001.** +- [x] Pre-scan and reject local source mutation, symlinks, special files, extra files, and path + collisions before creating remote files. Completed by E-M6-SOURCE-PRESCAN-AUDIT-001, + E-M6-SOURCE-PRESCAN-LOCAL-RED-001, E-M6-SOURCE-PRESCAN-LOCAL-001, + E-M6-SOURCE-PRESCAN-CI-RED-001, E-M6-SOURCE-PRESCAN-COLLISION-FIX-LOCAL-001, and + E-M6-SOURCE-PRESCAN-CI-001. - [ ] Stream files with bounded buffers; prohibit whole-tree memory materialization. - [ ] Pass one AbortSignal through enumeration, channel creation, reads, writes, permission repair, and remote cleanup. @@ -16419,6 +16420,70 @@ src/main/ssh/ssh-relay-*.test.ts` — PASS, exit 0: 41 files passed, three skipp every other native client, and require the complete exact-artifact scan metrics before closing the package. The failed/cancelled run supplies no green cell. +### E-M6-SOURCE-PRESCAN-CI-001 — all-six native and exact full-size pre-scan gates pass + +- Date/owner/head: 2026-07-15, Codex implementation owner; exact correction commit + `724ed6295551a63cd710b4da3749548ac787a0ad` on draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). +- Native workflow: SSH Relay Runtime Artifacts run + [29498740510](https://github.com/stablyai/orca/actions/runs/29498740510). All six primary Node + 24.18.0 jobs pass their contract, deterministic-build/smoke, full-size extraction/cache/pre-scan, + and evidence-upload steps: win32 x64 + [87621972614](https://github.com/stablyai/orca/actions/runs/29498740510/job/87621972614), + win32 arm64 + [87621972692](https://github.com/stablyai/orca/actions/runs/29498740510/job/87621972692), + Darwin x64 + [87621972687](https://github.com/stablyai/orca/actions/runs/29498740510/job/87621972687), + Darwin arm64 + [87621972703](https://github.com/stablyai/orca/actions/runs/29498740510/job/87621972703), + Linux x64 + [87621972727](https://github.com/stablyai/orca/actions/runs/29498740510/job/87621972727), and + Linux arm64 + [87621972772](https://github.com/stablyai/orca/actions/runs/29498740510/job/87621972772). +- Purpose suite: both Linux jobs pass all 18 source pre-scan cases, including the real + case-sensitive collision; each macOS job passes 17 with the Linux-only case skipped; each Windows + job passes 15 with three declared POSIX/Linux-only skips. Overall native contract totals are + 707 passed / one skipped on Linux, 706 passed / two skipped on macOS, and 695 passed / 17 skipped + on Windows. The suite takes 1.415–7.117 seconds across the six clients. +- Exact full-size metrics, reported by the cache measurement JSON after hashing the complete signed + tree while its lease is held: + - Linux x64: 34 files / 124,846,430 bytes; scan 145.534 ms / 131,072 incremental RSS bytes. + - Linux arm64: 34 files / 122,865,324 bytes; scan 155.329 ms / 262,144 incremental RSS bytes. + - Darwin x64: 35 files / 124,316,655 bytes; scan 724.753 ms / 139,264 incremental RSS bytes. + - Darwin arm64: 35 files / 122,027,869 bytes; scan 161.363 ms / 81,920 incremental RSS bytes. + - Windows x64: 42 files / 96,527,161 bytes; scan 194.877 ms / 3,239,936 incremental RSS bytes. + - Windows arm64: 42 files / 85,213,511 bytes; scan 375.761 ms / 2,289,664 incremental RSS bytes. + Every cell is below the 120-second and 80-MiB scan budgets; observed ranges are 145.534–724.753 + ms and 81,920–3,239,936 incremental RSS bytes. Cold/warm cache, extraction, retention, and eviction + also remain within their existing budgets. +- Baselines: Linux x64 supplement + [87623008534](https://github.com/stablyai/orca/actions/runs/29498740510/job/87623008534) and Linux + arm64 supplement + [87623008618](https://github.com/stablyai/orca/actions/runs/29498740510/job/87623008618) pass. + Windows x64 oldest baseline + [87624256480](https://github.com/stablyai/orca/actions/runs/29498740510/job/87624256480) passes. + Windows arm64 oldest baseline + [87624256574](https://github.com/stablyai/orca/actions/runs/29498740510/job/87624256574) has only + the declared hosted-image build rejection: observed 26200 versus required 26100. Before that + rejection it verifies the complete 60-entry / 42-file / 85,213,511-byte tree, Node 24.18.0 ABI + 137, PTY exit 23 and resize 37x101, five watcher events, two-second resource settlement, and smoke + 5,898.6823 ms / 49,664,000 RSS; complete verification is 8,024.2681 ms. The workflow is therefore + red only by the already-declared runner-floor evidence gap, not the source pre-scan package. +- Adjacent exact-head workflows pass: PR Checks run + [29498740486](https://github.com/stablyai/orca/actions/runs/29498740486), verify job + [87621971980](https://github.com/stablyai/orca/actions/runs/29498740486/job/87621971980); Golden E2E + run [29498740519](https://github.com/stablyai/orca/actions/runs/29498740519), Linux job + [87621971899](https://github.com/stablyai/orca/actions/runs/29498740519/job/87621971899) and macOS job + [87621971841](https://github.com/stablyai/orca/actions/runs/29498740519/job/87621971841); + computer-use run [29498740483](https://github.com/stablyai/orca/actions/runs/29498740483), Windows + job [87621971772](https://github.com/stablyai/orca/actions/runs/29498740483/job/87621971772) and + Ubuntu job [87621971790](https://github.com/stablyai/orca/actions/runs/29498740483/job/87621971790). +- Boundary/residual gaps: GitHub clients prove native filesystem and artifact contracts, not live + SSH, SFTP or system-SSH channel behavior, remote staging/install, or post-scan transfer races. The + scanned-tree type still has no non-test `src/main` importer. Bounded client streaming, every remote + transport, product/settings/fallback/tuple/publication/default path, and SignPath remain open. + Legacy remains the sole production/default path and every tuple remains disabled. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 59997b1383ef51d390d811b17e235917b51e4a69 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 09:23:03 -0400 Subject: [PATCH 211/267] feat: bound SSH relay runtime source streaming Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 22 +- ...github-release-implementation-checklist.md | 143 +++- .../ssh-relay-artifact-cache-entry-fixture.ts | 8 +- ...lay-artifact-cache-entry-full-size.test.ts | 36 +- .../ssh/ssh-relay-runtime-source-snapshot.ts | 158 ++++ .../ssh-relay-runtime-source-stream.test.ts | 774 ++++++++++++++++++ .../ssh/ssh-relay-runtime-source-stream.ts | 298 +++++++ 9 files changed, 1423 insertions(+), 19 deletions(-) create mode 100644 src/main/ssh/ssh-relay-runtime-source-snapshot.ts create mode 100644 src/main/ssh/ssh-relay-runtime-source-stream.test.ts create mode 100644 src/main/ssh/ssh-relay-runtime-source-stream.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 397588130e7..3c2b0e75914 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -284,6 +284,7 @@ jobs: src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts \ src/main/ssh/ssh-relay-runtime-source-tree.test.ts \ src/main/ssh/ssh-relay-runtime-source-scan.test.ts \ + src/main/ssh/ssh-relay-runtime-source-stream.test.ts \ src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts \ src/main/ssh/ssh-relay-official-manifest.test.ts \ src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts \ @@ -838,6 +839,7 @@ jobs: src/main/ssh/ssh-relay-artifact-acquisition-integration.test.ts ` src/main/ssh/ssh-relay-runtime-source-tree.test.ts ` src/main/ssh/ssh-relay-runtime-source-scan.test.ts ` + src/main/ssh/ssh-relay-runtime-source-stream.test.ts ` src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts ` src/main/ssh/ssh-relay-official-manifest.test.ts ` src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index edc5083ceb6..3b99c61f563 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -142,6 +142,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-artifact-acquisition-integration.test.ts', 'ssh-relay-runtime-source-tree.test.ts', 'ssh-relay-runtime-source-scan.test.ts', + 'ssh-relay-runtime-source-stream.test.ts', 'ssh-relay-compiled-manifest-trust.test.ts', 'ssh-relay-official-manifest.test.ts', 'ssh-relay-manifest-accepted-keys.test.ts', 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 44b1bb3d340..1fe407f584c 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 @@ -9,9 +9,25 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected source -pre-scan CI is closed; audit only the bounded client streaming boundary next, 2026-07-15, Codex -implementation owner.** +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected bounded +client source stream is locally green; commit/push and exact-head native/full-size CI are next, +2026-07-15, Codex implementation owner.** +`E-M6-SOURCE-STREAM-AUDIT-001` fixes the next artifact-only boundary: exact scanned tree plus one +signal, maximum four 64-KiB readers, local snapshot proof before destination open, hash/size and +post-read snapshot proof before destination finalization, joined abort/close cleanup, and path-free +aggregate progress. It deliberately contains no SSH, remote path/staging, product caller, setting, +fallback, tuple, publication, or default behavior. +`E-M6-SOURCE-STREAM-LOCAL-RED-001` records the expected missing-module failures for the purpose and +full-size suites plus the workflow oracle's six-pass/one-fail proof that neither native command ran +the new suite. +`E-M6-SOURCE-STREAM-LOCAL-001` passes 27/27 purpose cases, 107 focused cases with one declared skip, +566 broad relay cases with five declared skips, 280/280 release/workflow cases, typecheck, full lint, +targeted format, max-lines, diff, protected-resolver, and no-product-import gates. It proves bounded +one-to-four-worker/64-KiB reads, complete pre-destination snapshots, exact digest/size checks, +hostile metadata and mutation rejection, cancellation, destination uniqueness/completeness, joined +cleanup, path-free progress, lease ownership, and concurrent settlement. The local full-size case +correctly skips without an exact native artifact; all-six native metrics and adjacent CI remain +required before checking the package. No SSH/product/default caller exists. `E-M6-SOURCE-PRESCAN-AUDIT-001` fixes the next local-only boundary: consume only the accepted source tree plus one exact signal; assert its borrowed lease; boundedly enumerate and hash every real entry; reject mutation, symlink/junction, special, missing/extra/colliding, mode/size/hash/aggregate drift; 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 e6335bd71f6..e3c90b4ad57 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected source pre-scan CI is closed; audit only the bounded client streaming boundary next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **SOURCE PRE-SCAN CLOSED / BOUNDED STREAM AUDIT NEXT — 2026-07-15, Codex implementation owner** — `E-M6-SOURCE-PRESCAN-CI-001` proves the corrected complete local scan on all six native clients and every exact full-size artifact. Audit the next transport-neutral bounded read/hash/progress boundary and record RED before implementation; do not open SSH channels or create remote staging in that package. Do not add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected bounded client source stream is locally green; exact-head native/full-size CI is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **BOUNDED SOURCE STREAM LOCAL GREEN / EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-SOURCE-STREAM-LOCAL-001` proves the filesystem-snapshot split, bounded reader/destination lifecycle, failure settlement, purpose/full-size wiring, adjacent suites, static gates, and isolation locally. Commit and push this package, then require all-six native artifact/full-size jobs plus adjacent CI before checking the package or advancing to an SSH transport adapter. Do not add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -150,10 +150,12 @@ same change as the work it records. - Completed package: disconnected full host-evidence composition is green locally and on every primary native client under E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001 and E-M5-HOST-EVIDENCE-COMPOSITION-CI-001. It has no product caller. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — add RED proof, then - implement only Milestone 6's audited disconnected full-tree source pre-scan. Do not open SSH - channels, transfer or install bytes, or add product/settings/fallback/tuple/publication/default - behavior. +- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit, RED, and local + GREEN complete under E-M6-SOURCE-STREAM-AUDIT-001, E-M6-SOURCE-STREAM-LOCAL-RED-001, and + E-M6-SOURCE-STREAM-LOCAL-001. Commit and push the disconnected bounded client source stream, then + require exact-head all-six native/full-size and adjacent CI. Do not import SSH, open channels, + construct remote paths, create remote staging, or add product/settings/fallback/tuple/publication/ + default behavior. - 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 @@ -16484,6 +16486,122 @@ src/main/ssh/ssh-relay-*.test.ts` — PASS, exit 0: 41 files passed, three skipp transport, product/settings/fallback/tuple/publication/default path, and SignPath remain open. Legacy remains the sole production/default path and every tuple remains disabled. +### E-M6-SOURCE-STREAM-AUDIT-001 — bounded transport-neutral source read and destination lifecycle + +- Date/owner: 2026-07-15, Codex implementation owner. +- Exact boundary: accept only one `SshRelayRuntimeScannedSourceTree`, its one required caller-owned + `AbortSignal`, a destination factory, a concurrency value from one through four, and an optional + synchronous progress observer. The factory receives only the authenticated file descriptor and + exact signal and returns a domain-shaped destination with awaited `write`, `close`, and `abort` + operations. No raw source path/list/manifest/cache entry or independently replaceable signal is + accepted. +- Pre-write sequence: assert the borrowed lease, then for each file recheck the scanned root, every + signed parent directory, and file path/type/mode/state. Open the local file read-only with + `O_NOFOLLOW` where supported and require the opened handle to match the exact scanned state. Only + after those checks may the destination factory run. This lets later transports create an + exclusive staged remote file without doing so for a rejected local source. +- Bounded transfer: use a fixed worker pool of at most four. Each worker owns one reusable 64 KiB + buffer and at most one local file handle plus one destination; never materialize a whole file or + tree. Await each destination write before reusing its buffer. Hash the exact bytes presented to + successful writes and require exact signed size and SHA-256 before completing the file. +- Post-write sequence: after the final chunk, recheck the open handle, close it, and recheck the file, + signed parents, and root against the scanned bigint snapshots. Only then close/finalize the + destination. A local mutation, replacement, short/long read, digest mismatch, lease loss, or + cancellation aborts the destination; it never reports the file complete. +- Lifecycle/error contract: check the exact signal before lease/filesystem/destination operations and + every chunk. Await local close and destination abort/close on success, failure, cancellation, and + callback failure. Preserve the primary failure and join cleanup failures in `AggregateError`; the + source stream borrows but never releases the cache lease. A caller timeout is expressed only by + aborting the same signal; the module creates no hidden timer. +- Progress/result: emit deeply frozen aggregate snapshots only after a destination accepts bytes and + after a file finalizes. Include tuple/content IDs, total/completed file counts, total/transferred + bytes, and active file count; never include local or signed paths. Return the final frozen proof + and do not log. Concurrent updates must be monotonic and exact. +- Required RED/green proof: signed Linux/Windows scanned trees; zero-byte, multi-chunk, and concurrent + reads; maximum four handles/destinations and 64 KiB chunks; ordering that forbids destination open + before local proof; mutation/symlink/type/mode/state/size/hash rejection before and during read; + exact transmitted-byte hash; duplicate/missing destination settlement; pre/mid-read/mid-write + cancellation; local/destination open/write/close/abort failures and joined cleanup; monotonic + path-free progress; lease ordering/ownership; exact full-size all-six latency/RSS/handle metrics; + both native workflow families; and no SSH/product importer. +- Measurement budgets: 20-minute caller-owned transfer ceiling, 80 MiB incremental desktop RSS, + four concurrent files/destinations, eight total process handles, and 64 KiB per worker buffer. The + exact-artifact harness may use an in-process hashing/discard destination solely to measure client + source streaming; it is not SSH or remote-transfer proof. +- Deliberately absent: SSH target/connection/channel/SFTP/child process, remote platform/path/command, + framing protocol, remote write/permission/cleanup/staging/install/launch, settings/modes/fallback, + Electron/startup, `ORCA_RELAY_PATH`, tuple enablement, publication, defaults, and SignPath. Later + POSIX, Windows, and SFTP packages must adapt this boundary and independently prove their remote + resource settlement and full-size live behavior. + +### E-M6-SOURCE-STREAM-LOCAL-RED-001 — bounded stream and native/full-size proof are absent + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0, + pnpm 10.24.0, atop evidence commit `16a58a234` plus the audit/test-only diff. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-source-stream.test.ts` — EXPECTED FAIL, exit 1: the + absent source-stream module prevents collection; zero tests in 1.36 seconds Vitest / 4.00 seconds + wall and 135,544,832-byte maximum RSS. +- Full-size command: the same prefix with + `src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts` — EXPECTED FAIL, exit 1: its exact + artifact stream measurement cannot import the absent module; zero tests in 1.22 seconds Vitest / + 3.84 seconds wall and 131,989,504-byte maximum RSS. +- Workflow-oracle command: the same prefix with + `config/scripts/ssh-relay-runtime-workflow.test.mjs` — EXPECTED FAIL, exit 1: six cases pass and + native inclusion fails because the purpose suite occurs only in the oracle, not the POSIX or + Windows runner command; 881 ms Vitest / 3.52 seconds wall and 132,841,472-byte maximum RSS. +- Scope at RED: test scaffolding adds signed zero/multi-chunk fixture bytes, destination lifecycle/ + mutation/cancellation/progress cases, and full-size measurement expectations only. The protected + resolver files have no diff. No source-stream module, SSH/product importer, remote resource/path/ + staging, settings/fallback, tuple, publication, or default behavior exists. + +### E-M6-SOURCE-STREAM-LOCAL-001 — disconnected bounded client source stream is locally green + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0, + pnpm 10.24.0, atop evidence commit `16a58a234` plus the uncommitted implementation/checklist diff. +- Implementation/result: `ssh-relay-runtime-source-snapshot.ts` owns exact root/parent/file/handle + snapshot checks and the client-native `O_NOFOLLOW` open operation; the 298-physical-line stream + module owns the one-to-four-worker lifecycle, one reusable 64 KiB buffer per worker, exact written- + byte hash/size checks, destination validation/uniqueness, frozen path-free progress, final full- + tree/lease proof, and joined cleanup. It accepts only an exact scanned tree, caller signal, + concurrency, destination factory, and optional observer. It has no SSH/product/default caller. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-source-stream.test.ts` — PASS, 27/27 in 2.43 seconds + Vitest / 3.45 seconds wall, 145,375,232-byte maximum RSS, zero swaps. It proves signed Linux and + Windows trees; complete tree/path/handle proof before a destination; zero/multi-chunk/concurrent + reads; four-handle/destination and 64 KiB limits; linked/type/mode/state/digest/mutation rejection; + incomplete/reused destination rejection; pre/mid-read/mid-write cancellation; local open/read/ + close and destination open/write/close/abort failures; joined three-error cleanup; path-free + monotonic progress; lease borrowing; immediate peer-stop before slow cleanup; and all-worker + settlement before rejection. +- Full-size harness command: the same prefix with + `src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts` — PASS with one expected local + skip because no exact native artifact environment was supplied; 404 ms Vitest / 1.40 seconds wall + and 132,022,272-byte maximum RSS. The harness now measures `streamElapsedMs` and + `streamIncrementalRssBytes`, checks the exact final aggregate, and retains the 20-minute/80 MiB + budgets; this local skip is not full-size proof. +- Native workflow oracle: the same prefix with + `config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 7/7 in 295 ms Vitest / 1.38 seconds + wall and 132,464,640-byte maximum RSS. Both POSIX and Windows native commands contain the purpose + suite, and the shared full-size case executes the stream measurement against each exact artifact. +- Adjacent/broad commands: the ten-file focused source/cache/acquisition/workflow command passes + 107 tests with one declared platform skip in 8.26 seconds; `src/main/ssh/ssh-relay-*.test.ts` + passes 566 with five declared platform/full-size skips in 24.06 seconds; and + `config/scripts/ssh-relay-runtime-*.test.mjs` passes 280/280 in 18.92 seconds. +- Static/isolation commands: `pnpm typecheck`, targeted `pnpm exec oxlint ...`, `pnpm lint`, + `pnpm run check:max-lines-ratchet`, targeted `pnpm exec oxfmt --check ...`, `git diff --check`, the + protected-resolver `git diff --exit-code -- ...`, and the non-test importer `rg` gate all pass. + Full `pnpm exec oxfmt --check .` remains red on 31 pre-existing unrelated files; all nine changed + files pass the targeted formatter check and none of those baseline files is modified. +- Hardening note: the first four-case hostile-metadata oracle used object spread on Node `Stats`, + which dropped prototype methods and correctly failed four tests plus typecheck; the fixture was + corrected to preserve the prototype. No production relaxation was made. The corrected suite then + passed 24 cases before the final local-open, digest, and concurrent-settlement cases raised it to 27. +- Residual gaps: exact-head all-six full-size latency/RSS/handle evidence and adjacent Actions jobs + remain required before this package is checked. The hashing/discard destination is client-only + measurement, not SFTP/system-SSH or remote proof. Remote channels, paths, staging/install, + product/Beta/fallback/tuple/publication/default wiring, and SignPath remain absent. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -16542,12 +16660,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Implement only the RED-proven Milestone 6 disconnected full-tree source pre-scan and wire its purpose -suite into both native workflow families, then run local/full-size evidence gates. The pre-scan must -create no remote files or open SSH channels. -Do not enumerate the filesystem, open an SSH channel, transfer or install bytes, modify platform -detection/selectors, add an Electron/startup consumer, mode wiring, fallback, tuple enablement, -release publication, or default behavior. +Commit and push only the locally green Milestone 6 disconnected bounded client source stream and its +purpose/full-size native workflow wiring, then require all-six exact-head native/full-size and +adjacent CI before checking the package or advancing. The stream must create no remote files or open +SSH channels. +Do not open an SSH channel, transfer to or install on a remote, modify platform detection/selectors, +add an Electron/startup consumer, mode wiring, fallback, tuple enablement, release publication, or +default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. diff --git a/src/main/ssh/ssh-relay-artifact-cache-entry-fixture.ts b/src/main/ssh/ssh-relay-artifact-cache-entry-fixture.ts index 8a49083ab45..915e390f322 100644 --- a/src/main/ssh/ssh-relay-artifact-cache-entry-fixture.ts +++ b/src/main/ssh/ssh-relay-artifact-cache-entry-fixture.ts @@ -110,10 +110,12 @@ export type SshRelayArtifactCacheEntryFixture = { export async function createSshRelayArtifactCacheEntryFixture({ root, - os + os, + fileBytes: fileByteOverrides = new Map() }: { root: string os: 'linux' | 'win32' + fileBytes?: ReadonlyMap }): Promise { const manifest: SshRelayArtifactManifest = os === 'win32' @@ -125,7 +127,9 @@ export async function createSshRelayArtifactCacheEntryFixture({ if (entry.type !== 'file') { continue } - const bytes = Buffer.from(`cache entry fixture:${tuple.tupleId}:${entry.path}`) + const bytes = + fileByteOverrides.get(entry.path) ?? + Buffer.from(`cache entry fixture:${tuple.tupleId}:${entry.path}`) fileBytes.set(entry.path, bytes) entry.size = bytes.length entry.sha256 = sha256(bytes) diff --git a/src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts b/src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts index 7800b561762..f34c1b2e945 100644 --- a/src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts +++ b/src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts @@ -21,6 +21,10 @@ import { scanSshRelayRuntimeSourceTree, SSH_RELAY_RUNTIME_SOURCE_SCAN_LIMITS } from './ssh-relay-runtime-source-scan' +import { + streamSshRelayRuntimeSourceTree, + SSH_RELAY_RUNTIME_SOURCE_STREAM_LIMITS +} from './ssh-relay-runtime-source-stream' import { createSshRelayRuntimeSourceTree } from './ssh-relay-runtime-source-tree' type MeasurementIdentity = { @@ -126,7 +130,7 @@ describe.skipIf(!hasMeasurementInput)('SSH relay full-size immutable artifact ca cacheRoot: cacheRoot as string, entry: warmEntry }) - const { scan, retained } = await (async () => { + const { scan, stream, retained } = await (async () => { try { const sourceTree = createSshRelayRuntimeSourceTree({ kind: 'ready', @@ -138,10 +142,22 @@ describe.skipIf(!hasMeasurementInput)('SSH relay full-size immutable artifact ca const scan = await measure(() => scanSshRelayRuntimeSourceTree(sourceTree, new AbortController().signal) ) + const stream = await measure(() => + streamSshRelayRuntimeSourceTree({ + tree: scan.result, + signal: new AbortController().signal, + maximumConcurrency: 4, + openDestination: async () => ({ + write: async () => {}, + close: async () => {}, + abort: async () => {} + }) + }) + ) const retained = await measure(() => evictSshRelayArtifactCache({ cacheRoot: cacheRoot as string, maximumBytes: 0 }) ) - return { scan, retained } + return { scan, stream, retained } } finally { await lease.release() } @@ -161,6 +177,8 @@ describe.skipIf(!hasMeasurementInput)('SSH relay full-size immutable artifact ca warmIncrementalRssBytes: warm.incrementalRssBytes, scanElapsedMs: scan.elapsedMs, scanIncrementalRssBytes: scan.incrementalRssBytes, + streamElapsedMs: stream.elapsedMs, + streamIncrementalRssBytes: stream.incrementalRssBytes, retainedElapsedMs: retained.elapsedMs, retainedIncrementalRssBytes: retained.incrementalRssBytes, evictionElapsedMs: eviction.elapsedMs, @@ -182,6 +200,14 @@ describe.skipIf(!hasMeasurementInput)('SSH relay full-size immutable artifact ca fileCount: identity.archive.fileCount, expandedBytes: identity.archive.expandedSize }) + expect(stream.result).toEqual({ + tupleId: identity.tupleId, + contentId: identity.contentId, + filesCompleted: identity.archive.fileCount, + totalFiles: identity.archive.fileCount, + bytesTransferred: identity.archive.expandedSize, + totalBytes: identity.archive.expandedSize + }) expect(retained.result).toMatchObject({ initialBytes: expect.any(Number), finalBytes: expect.any(Number), @@ -215,6 +241,12 @@ describe.skipIf(!hasMeasurementInput)('SSH relay full-size immutable artifact ca expect(scan.incrementalRssBytes).toBeLessThanOrEqual( SSH_RELAY_RUNTIME_SOURCE_SCAN_LIMITS.maximumIncrementalMemoryBytes ) + expect(stream.elapsedMs).toBeLessThanOrEqual( + SSH_RELAY_RUNTIME_SOURCE_STREAM_LIMITS.measurementTimeoutMs + ) + expect(stream.incrementalRssBytes).toBeLessThanOrEqual( + SSH_RELAY_RUNTIME_SOURCE_STREAM_LIMITS.maximumIncrementalMemoryBytes + ) for (const measurement of [retained, eviction]) { expect(measurement.elapsedMs).toBeLessThanOrEqual( SSH_RELAY_ARTIFACT_CACHE_EVICTION_LIMITS.transactionTimeoutMs diff --git a/src/main/ssh/ssh-relay-runtime-source-snapshot.ts b/src/main/ssh/ssh-relay-runtime-source-snapshot.ts new file mode 100644 index 00000000000..992338a335f --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-source-snapshot.ts @@ -0,0 +1,158 @@ +import { constants } from 'node:fs' +import { lstat, open } from 'node:fs/promises' + +import type { + SshRelayRuntimeScannedSourceTree, + SshRelayRuntimeSourceState +} from './ssh-relay-runtime-source-scan' + +export type SshRelayRuntimeSourceMetadata = { + dev: bigint + ino: bigint + size: bigint + mtimeNs: bigint + ctimeNs: bigint + mode: bigint + isFile: () => boolean + isDirectory: () => boolean + isSymbolicLink: () => boolean +} + +export type SshRelayRuntimeSourceFileHandle = { + stat: () => Promise + read: ( + buffer: Buffer, + offset: number, + length: number, + position: number + ) => Promise<{ bytesRead: number }> + close: () => Promise +} + +export type SshRelayRuntimeSourceStreamOperations = Readonly<{ + lstat: (path: string) => Promise + openFile: (path: string) => Promise +}> + +export type SshRelayRuntimeScannedSourceFile = SshRelayRuntimeScannedSourceTree['files'][number] + +export const SSH_RELAY_RUNTIME_SOURCE_STREAM_OPERATIONS: SshRelayRuntimeSourceStreamOperations = + Object.freeze({ + lstat: (path) => lstat(path, { bigint: true }), + openFile: async (path) => { + const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)) + return { + stat: () => handle.stat({ bigint: true }), + read: (buffer, offset, length, position) => handle.read(buffer, offset, length, position), + close: () => handle.close() + } + } + }) + +export function matchesSshRelayRuntimeSourceState( + left: SshRelayRuntimeSourceState, + right: SshRelayRuntimeSourceMetadata +): boolean { + return ( + left.dev === right.dev && + left.ino === right.ino && + left.size === right.size && + left.mtimeNs === right.mtimeNs && + left.ctimeNs === right.ctimeNs + ) +} + +function assertRootSnapshot( + tree: SshRelayRuntimeScannedSourceTree, + metadata: SshRelayRuntimeSourceMetadata +): void { + if ( + metadata.isSymbolicLink() || + !metadata.isDirectory() || + !matchesSshRelayRuntimeSourceState(tree.runtimeRootState, metadata) + ) { + throw new Error('SSH relay runtime source root changed before or during streaming') + } +} + +function assertEntrySnapshot( + entry: SshRelayRuntimeScannedSourceTree['directories'][number] | SshRelayRuntimeScannedSourceFile, + metadata: SshRelayRuntimeSourceMetadata +): void { + const correctType = entry.type === 'directory' ? metadata.isDirectory() : metadata.isFile() + if ( + metadata.isSymbolicLink() || + !correctType || + !matchesSshRelayRuntimeSourceState(entry.state, metadata) + ) { + throw new Error( + `SSH relay runtime source entry changed before or during streaming: ${entry.path}` + ) + } + if (process.platform !== 'win32' && Number(metadata.mode & 0o777n) !== entry.mode) { + throw new Error( + `SSH relay runtime source mode changed before or during streaming: ${entry.path}` + ) + } +} + +export function sshRelayRuntimeSourceParentDirectories( + tree: SshRelayRuntimeScannedSourceTree, + file: SshRelayRuntimeScannedSourceFile +): readonly SshRelayRuntimeScannedSourceTree['directories'][number][] { + const directoryByPath = new Map(tree.directories.map((directory) => [directory.path, directory])) + const parts = file.path.split('/') + const parents: SshRelayRuntimeScannedSourceTree['directories'][number][] = [] + for (let index = 1; index < parts.length; index += 1) { + const path = parts.slice(0, index).join('/') + const directory = directoryByPath.get(path) + if (!directory) { + throw new Error(`SSH relay runtime source file has an undeclared parent: ${file.path}`) + } + parents.push(directory) + } + return parents +} + +export async function assertSshRelayRuntimeSourcePathSnapshot( + tree: SshRelayRuntimeScannedSourceTree, + file: SshRelayRuntimeScannedSourceFile, + parents: readonly SshRelayRuntimeScannedSourceTree['directories'][number][], + signal: AbortSignal, + operations: SshRelayRuntimeSourceStreamOperations +): Promise { + signal.throwIfAborted() + assertRootSnapshot(tree, await operations.lstat(tree.runtimeRoot)) + for (const directory of parents) { + signal.throwIfAborted() + assertEntrySnapshot(directory, await operations.lstat(directory.localPath)) + } + signal.throwIfAborted() + assertEntrySnapshot(file, await operations.lstat(file.localPath)) +} + +export async function assertSshRelayRuntimeSourceTreeSnapshot( + tree: SshRelayRuntimeScannedSourceTree, + signal: AbortSignal, + operations: SshRelayRuntimeSourceStreamOperations +): Promise { + signal.throwIfAborted() + assertRootSnapshot(tree, await operations.lstat(tree.runtimeRoot)) + for (const entry of [...tree.directories, ...tree.files]) { + signal.throwIfAborted() + assertEntrySnapshot(entry, await operations.lstat(entry.localPath)) + } +} + +export function assertSshRelayRuntimeOpenedFileSnapshot( + file: SshRelayRuntimeScannedSourceFile, + metadata: SshRelayRuntimeSourceMetadata, + phase: 'opening' | 'streaming' +): void { + if (!metadata.isFile() || !matchesSshRelayRuntimeSourceState(file.state, metadata)) { + throw new Error(`SSH relay runtime source file changed while ${phase}: ${file.path}`) + } + if (process.platform !== 'win32' && Number(metadata.mode & 0o777n) !== file.mode) { + throw new Error(`SSH relay runtime source mode changed while ${phase}: ${file.path}`) + } +} diff --git a/src/main/ssh/ssh-relay-runtime-source-stream.test.ts b/src/main/ssh/ssh-relay-runtime-source-stream.test.ts new file mode 100644 index 00000000000..8a854cba380 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-source-stream.test.ts @@ -0,0 +1,774 @@ +import { createHash } from 'node:crypto' +import { constants } from 'node:fs' +import { lstat, mkdir, mkdtemp, open, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { publishSshRelayArtifactCacheEntry } from './ssh-relay-artifact-cache-entry' +import { createSshRelayArtifactCacheEntryFixture } from './ssh-relay-artifact-cache-entry-fixture' +import { + acquireSshRelayArtifactCacheInUseLease, + type SshRelayArtifactCacheInUseLease +} from './ssh-relay-artifact-cache-in-use-lease' +import { scanSshRelayRuntimeSourceTree } from './ssh-relay-runtime-source-scan' +import type { SshRelayRuntimeSourceMetadata } from './ssh-relay-runtime-source-snapshot' +import { + streamSshRelayRuntimeSourceTree, + type SshRelayRuntimeSourceDestination, + type SshRelayRuntimeSourceStreamOperations, + type SshRelayRuntimeSourceStreamProgress +} from './ssh-relay-runtime-source-stream' +import { createSshRelayRuntimeSourceTree } from './ssh-relay-runtime-source-tree' + +const cleanupRoots = new Set() +const cleanupLeases = new Set() + +async function sourceStreamFixture({ + os = 'linux', + fileBytes +}: { + os?: 'linux' | 'win32' + fileBytes?: ReadonlyMap +} = {}) { + const root = await mkdtemp(join(tmpdir(), 'orca-relay-source-stream-')) + cleanupRoots.add(root) + const inputRoot = join(root, 'input') + await mkdir(inputRoot) + const fixture = await createSshRelayArtifactCacheEntryFixture({ + root: inputRoot, + os, + fileBytes + }) + const cacheRoot = join(root, 'cache') + const entry = await publishSshRelayArtifactCacheEntry({ + cacheRoot, + artifact: fixture.artifact, + archivePath: fixture.archivePath + }) + const lease = await acquireSshRelayArtifactCacheInUseLease({ cacheRoot, entry }) + cleanupLeases.add(lease) + const sourceTree = createSshRelayRuntimeSourceTree({ + kind: 'ready', + source: 'cache', + artifact: fixture.artifact, + entry, + lease + }) + const tree = await scanSshRelayRuntimeSourceTree(sourceTree, new AbortController().signal) + return { root, tree } +} + +function digestDestination( + digests: Map, + events: string[] = [] +): (file: { path: string }) => Promise { + return async (file) => { + events.push(`open:${file.path}`) + const digest = createHash('sha256') + return { + write: async (chunk) => { + events.push(`write:${file.path}:${chunk.length}`) + digest.update(chunk) + }, + close: async () => { + events.push(`close:${file.path}`) + digests.set(file.path, `sha256:${digest.digest('hex')}`) + }, + abort: async () => { + events.push(`abort:${file.path}`) + } + } + } +} + +afterEach(async () => { + await Promise.all([...cleanupLeases].map((lease) => lease.release().catch(() => {}))) + cleanupLeases.clear() + await Promise.all([...cleanupRoots].map((root) => rm(root, { recursive: true, force: true }))) + cleanupRoots.clear() +}) + +describe('SSH relay runtime bounded source stream', () => { + it.each(['linux', 'win32'] as const)( + 'streams the complete signed %s tree with frozen path-free progress', + async (os) => { + const { tree } = await sourceStreamFixture({ os }) + const digests = new Map() + const progress: SshRelayRuntimeSourceStreamProgress[] = [] + + const result = await streamSshRelayRuntimeSourceTree({ + tree, + signal: new AbortController().signal, + maximumConcurrency: 2, + openDestination: digestDestination(digests), + onProgress: (value) => progress.push(value) + }) + + expect(result).toEqual({ + tupleId: tree.tupleId, + contentId: tree.contentId, + filesCompleted: tree.fileCount, + totalFiles: tree.fileCount, + bytesTransferred: tree.expandedBytes, + totalBytes: tree.expandedBytes + }) + expect(Object.isFrozen(result)).toBe(true) + expect(digests).toEqual(new Map(tree.files.map((file) => [file.path, file.sha256]))) + expect(progress.length).toBeGreaterThan(tree.fileCount) + expect(progress.every(Object.isFrozen)).toBe(true) + expect(progress.at(-1)).toMatchObject(result) + expect(progress.map((value) => value.bytesTransferred)).toEqual( + progress.map((value) => value.bytesTransferred).sort((left, right) => left - right) + ) + const progressJson = JSON.stringify(progress) + expect(progressJson).not.toContain(tree.runtimeRoot) + expect(tree.files.every((file) => !progressJson.includes(file.path))).toBe(true) + } + ) + + it('bounds zero-byte and multi-chunk files to one 64 KiB buffer per worker', async () => { + const largeBytes = Buffer.alloc(2 * 64 * 1024 + 17, 0xa5) + const { tree } = await sourceStreamFixture({ + fileBytes: new Map([ + ['relay.js', largeBytes], + ['THIRD_PARTY_LICENSES.txt', Buffer.alloc(0)] + ]) + }) + const writeLengths = new Map() + const closed: string[] = [] + + await streamSshRelayRuntimeSourceTree({ + tree, + signal: new AbortController().signal, + maximumConcurrency: 1, + openDestination: async (file) => ({ + write: async (chunk) => { + const lengths = writeLengths.get(file.path) ?? [] + lengths.push(chunk.length) + writeLengths.set(file.path, lengths) + }, + close: async () => { + closed.push(file.path) + }, + abort: async () => {} + }) + }) + + expect(writeLengths.get('relay.js')).toEqual([64 * 1024, 64 * 1024, 17]) + expect(writeLengths.get('THIRD_PARTY_LICENSES.txt')).toBeUndefined() + expect(closed).toContain('THIRD_PARTY_LICENSES.txt') + }) + + it('never opens more than four local files or destinations', async () => { + const { tree } = await sourceStreamFixture() + let openFiles = 0 + let openDestinations = 0 + let peakFiles = 0 + let peakDestinations = 0 + let releaseWrites: (() => void) | undefined + const writeGate = new Promise((resolve) => { + releaseWrites = resolve + }) + const operations: Partial = { + openFile: async (path) => { + const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)) + openFiles += 1 + peakFiles = Math.max(peakFiles, openFiles) + return { + stat: () => handle.stat({ bigint: true }), + read: (buffer, offset, length, position) => handle.read(buffer, offset, length, position), + close: async () => { + await handle.close() + openFiles -= 1 + } + } + } + } + + const transfer = streamSshRelayRuntimeSourceTree( + { + tree, + signal: new AbortController().signal, + maximumConcurrency: 4, + openDestination: async () => { + openDestinations += 1 + peakDestinations = Math.max(peakDestinations, openDestinations) + if (openDestinations === 4) { + releaseWrites?.() + } + return { + write: async () => writeGate, + close: async () => { + openDestinations -= 1 + }, + abort: async () => { + openDestinations -= 1 + } + } + } + }, + operations + ) + + await transfer + expect({ openFiles, openDestinations, peakFiles, peakDestinations }).toEqual({ + openFiles: 0, + openDestinations: 0, + peakFiles: 4, + peakDestinations: 4 + }) + }) + + it.each([0, 5, 1.5])( + 'rejects invalid concurrency %s before lease or filesystem work', + async (value) => { + const { tree } = await sourceStreamFixture() + const assertLeaseOwned = vi.fn(tree.assertLeaseOwned) + const openDestination = vi.fn() + + await expect( + streamSshRelayRuntimeSourceTree({ + tree: { ...tree, assertLeaseOwned }, + signal: new AbortController().signal, + maximumConcurrency: value, + openDestination + }) + ).rejects.toThrow(/concurrency/i) + expect(assertLeaseOwned).not.toHaveBeenCalled() + expect(openDestination).not.toHaveBeenCalled() + } + ) + + it('settles pre-cancellation before lease, filesystem, or destination work', async () => { + const { tree } = await sourceStreamFixture() + const assertLeaseOwned = vi.fn(tree.assertLeaseOwned) + const openDestination = vi.fn() + const controller = new AbortController() + controller.abort(new Error('cancel source stream')) + + await expect( + streamSshRelayRuntimeSourceTree({ + tree: { ...tree, assertLeaseOwned }, + signal: controller.signal, + openDestination + }) + ).rejects.toThrow(/cancel source stream/i) + expect(assertLeaseOwned).not.toHaveBeenCalled() + expect(openDestination).not.toHaveBeenCalled() + }) + + it('opens a destination only after exact path and handle checks', async () => { + const { tree } = await sourceStreamFixture() + const events: string[] = [] + const operations: Partial = { + lstat: async (path) => { + events.push(`lstat:${path}`) + return lstat(path, { bigint: true }) + }, + openFile: async (path) => { + events.push(`open-file:${path}`) + const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)) + return { + stat: async () => { + events.push(`handle-stat:${path}`) + return handle.stat({ bigint: true }) + }, + read: (buffer, offset, length, position) => handle.read(buffer, offset, length, position), + close: () => handle.close() + } + } + } + + await streamSshRelayRuntimeSourceTree( + { + tree, + signal: new AbortController().signal, + maximumConcurrency: 1, + openDestination: async (file) => { + events.push(`open-destination:${file.path}`) + return { write: async () => {}, close: async () => {}, abort: async () => {} } + } + }, + operations + ) + + const firstDestination = events.findIndex((event) => event.startsWith('open-destination:')) + const beforeFirstDestination = events.slice(0, firstDestination) + for (const path of [ + tree.runtimeRoot, + ...tree.directories.map((directory) => directory.localPath), + ...tree.files.map((file) => file.localPath) + ]) { + expect(beforeFirstDestination).toContain(`lstat:${path}`) + } + expect(beforeFirstDestination).toEqual( + expect.arrayContaining([ + expect.stringMatching(/^open-file:/), + expect.stringMatching(/^handle-stat:/) + ]) + ) + }) + + it.each< + readonly [string, (metadata: SshRelayRuntimeSourceMetadata) => SshRelayRuntimeSourceMetadata] + >([ + [ + 'linked', + (metadata) => Object.assign(Object.create(metadata), { isSymbolicLink: () => true }) + ], + ['wrong type', (metadata) => Object.assign(Object.create(metadata), { isFile: () => false })], + [ + 'wrong mode', + (metadata) => Object.assign(Object.create(metadata), { mode: metadata.mode ^ 1n }) + ], + [ + 'state drift', + (metadata) => Object.assign(Object.create(metadata), { mtimeNs: metadata.mtimeNs + 1n }) + ] + ])('rejects %s source metadata before destination creation', async (_label, mutate) => { + const { tree } = await sourceStreamFixture() + const target = tree.files[0].localPath + const openDestination = vi.fn() + await expect( + streamSshRelayRuntimeSourceTree( + { tree, signal: new AbortController().signal, openDestination }, + { + lstat: async (path) => { + const metadata = (await lstat(path, { + bigint: true + })) as SshRelayRuntimeSourceMetadata + return path === target ? mutate(metadata) : metadata + } + } + ) + ).rejects.toThrow(/changed|mode/i) + expect(openDestination).not.toHaveBeenCalled() + }) + + it('rejects source mutation before destination creation', async () => { + const { tree } = await sourceStreamFixture() + const openDestination = vi.fn() + const bytes = await readFile(tree.files[0].localPath) + bytes[0] ^= 0xff + await writeFile(tree.files[0].localPath, bytes) + + await expect( + streamSshRelayRuntimeSourceTree({ + tree, + signal: new AbortController().signal, + openDestination + }) + ).rejects.toThrow(/changed|mutation|state/i) + expect(openDestination).not.toHaveBeenCalled() + }) + + it('closes over a local-open failure without creating a destination', async () => { + const { tree } = await sourceStreamFixture() + const openDestination = vi.fn() + await expect( + streamSshRelayRuntimeSourceTree( + { tree, signal: new AbortController().signal, openDestination }, + { + openFile: async () => { + throw new Error('local open failure') + } + } + ) + ).rejects.toThrow(/local open failure/i) + expect(openDestination).not.toHaveBeenCalled() + }) + + it('aborts when bytes returned by the local reader fail the signed digest', async () => { + const { tree } = await sourceStreamFixture() + const abort = vi.fn(async () => {}) + const operations: Partial = { + openFile: async (path) => { + const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)) + return { + stat: () => handle.stat({ bigint: true }), + read: async (buffer, offset, length, position) => { + const result = await handle.read(buffer, offset, length, position) + if (result.bytesRead > 0) { + buffer[offset] ^= 0xff + } + return result + }, + close: () => handle.close() + } + } + } + + await expect( + streamSshRelayRuntimeSourceTree( + { + tree, + signal: new AbortController().signal, + openDestination: async () => ({ write: async () => {}, close: async () => {}, abort }) + }, + operations + ) + ).rejects.toThrow(/integrity changed/i) + expect(abort).toHaveBeenCalledTimes(1) + }) + + it('aborts instead of finalizing when the source mutates during a write', async () => { + const { tree } = await sourceStreamFixture() + const close = vi.fn(async () => {}) + const abort = vi.fn(async () => {}) + let mutated = false + + await expect( + streamSshRelayRuntimeSourceTree({ + tree, + signal: new AbortController().signal, + maximumConcurrency: 1, + openDestination: async (file) => ({ + write: async () => { + if (!mutated) { + mutated = true + const bytes = await readFile(file.localPath) + bytes[0] ^= 0xff + await writeFile(file.localPath, bytes) + } + }, + close, + abort + }) + }) + ).rejects.toThrow(/changed|mutation|state/i) + expect(close).not.toHaveBeenCalled() + expect(abort).toHaveBeenCalledTimes(1) + }) + + it('closes the local file and aborts once on destination write failure', async () => { + const { tree } = await sourceStreamFixture() + const localClose = vi.fn(async () => {}) + const abort = vi.fn(async () => {}) + const operations: Partial = { + openFile: async (path) => { + const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)) + return { + stat: () => handle.stat({ bigint: true }), + read: (buffer, offset, length, position) => handle.read(buffer, offset, length, position), + close: async () => { + await handle.close() + await localClose() + } + } + } + } + + await expect( + streamSshRelayRuntimeSourceTree( + { + tree, + signal: new AbortController().signal, + maximumConcurrency: 1, + openDestination: async () => ({ + write: async () => { + throw new Error('destination write failed') + }, + close: async () => {}, + abort + }) + }, + operations + ) + ).rejects.toThrow(/destination write failed/i) + expect(localClose).toHaveBeenCalledTimes(1) + expect(abort).toHaveBeenCalledTimes(1) + }) + + it('propagates mid-write cancellation and performs no later writes', async () => { + const { tree } = await sourceStreamFixture({ + fileBytes: new Map([['relay.js', Buffer.alloc(3 * 64 * 1024, 0x5a)]]) + }) + const controller = new AbortController() + const write = vi.fn(async () => { + controller.abort(new Error('cancel during destination write')) + }) + const abort = vi.fn(async () => {}) + + await expect( + streamSshRelayRuntimeSourceTree({ + tree, + signal: controller.signal, + maximumConcurrency: 1, + openDestination: async () => ({ write, close: async () => {}, abort }) + }) + ).rejects.toThrow(/cancel during destination write/i) + expect(write).toHaveBeenCalledTimes(1) + expect(abort).toHaveBeenCalledTimes(1) + }) + + it('settles mid-read cancellation before any destination write', async () => { + const { tree } = await sourceStreamFixture() + const controller = new AbortController() + const write = vi.fn(async () => {}) + const abort = vi.fn(async () => {}) + const operations: Partial = { + openFile: async (path) => { + const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)) + return { + stat: () => handle.stat({ bigint: true }), + read: async (buffer, offset, length, position) => { + const result = await handle.read(buffer, offset, length, position) + controller.abort(new Error('cancel during local read')) + return result + }, + close: () => handle.close() + } + } + } + + await expect( + streamSshRelayRuntimeSourceTree( + { + tree, + signal: controller.signal, + openDestination: async () => ({ write, close: async () => {}, abort }) + }, + operations + ) + ).rejects.toThrow(/cancel during local read/i) + expect(write).not.toHaveBeenCalled() + expect(abort).toHaveBeenCalledTimes(1) + }) + + it('joins local read, local close, and destination abort failures', async () => { + const { tree } = await sourceStreamFixture() + const operations: Partial = { + openFile: async (path) => { + const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)) + return { + stat: () => handle.stat({ bigint: true }), + read: async () => { + throw new Error('local read failure') + }, + close: async () => { + await handle.close() + throw new Error('local close failure') + } + } + } + } + + const error = await streamSshRelayRuntimeSourceTree( + { + tree, + signal: new AbortController().signal, + openDestination: async () => ({ + write: async () => {}, + close: async () => {}, + abort: async () => { + throw new Error('destination abort failure') + } + }) + }, + operations + ).catch((value: unknown) => value) + + expect(error).toBeInstanceOf(AggregateError) + expect((error as AggregateError).errors).toEqual([ + expect.objectContaining({ message: 'local read failure' }), + expect.objectContaining({ message: 'local close failure' }), + expect.objectContaining({ message: 'destination abort failure' }) + ]) + }) + + it('waits for every concurrent destination to settle before rejecting', async () => { + const { tree } = await sourceStreamFixture() + let releaseSecondWrite: (() => void) | undefined + let markSecondWriteStarted: (() => void) | undefined + let markFirstAbort: (() => void) | undefined + const secondWriteGate = new Promise((resolve) => { + releaseSecondWrite = resolve + }) + const secondWriteStarted = new Promise((resolve) => { + markSecondWriteStarted = resolve + }) + const firstAborted = new Promise((resolve) => { + markFirstAbort = resolve + }) + let settled = false + const secondClose = vi.fn(async () => {}) + const secondAbort = vi.fn(async () => {}) + + const transfer = streamSshRelayRuntimeSourceTree({ + tree, + signal: new AbortController().signal, + maximumConcurrency: 2, + openDestination: async (file) => ({ + write: + file === tree.files[0] + ? async () => { + await secondWriteStarted + throw new Error('first concurrent write failed') + } + : async () => { + markSecondWriteStarted?.() + await secondWriteGate + }, + close: file === tree.files[0] ? async () => {} : secondClose, + abort: async () => { + if (file === tree.files[0]) { + markFirstAbort?.() + } else { + await secondAbort() + } + } + }) + }).finally(() => { + settled = true + }) + + await firstAborted + expect(settled).toBe(false) + releaseSecondWrite?.() + await expect(transfer).rejects.toThrow(/first concurrent write failed/i) + expect(settled).toBe(true) + expect(secondClose).not.toHaveBeenCalled() + expect(secondAbort).toHaveBeenCalledTimes(1) + }) + + it('joins a primary write failure with destination abort failure', async () => { + const { tree } = await sourceStreamFixture() + + const error = await streamSshRelayRuntimeSourceTree({ + tree, + signal: new AbortController().signal, + maximumConcurrency: 1, + openDestination: async () => ({ + write: async () => { + throw new Error('primary write failure') + }, + close: async () => {}, + abort: async () => { + throw new Error('destination abort failure') + } + }) + }).catch((value: unknown) => value) + + expect(error).toBeInstanceOf(AggregateError) + expect((error as AggregateError).errors).toEqual([ + expect.objectContaining({ message: 'primary write failure' }), + expect.objectContaining({ message: 'destination abort failure' }) + ]) + }) + + it('joins a destination-open failure with local-close failure', async () => { + const { tree } = await sourceStreamFixture() + const operations: Partial = { + openFile: async (path) => { + const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)) + return { + stat: () => handle.stat({ bigint: true }), + read: (buffer, offset, length, position) => handle.read(buffer, offset, length, position), + close: async () => { + await handle.close() + throw new Error('local close failure') + } + } + } + } + + const error = await streamSshRelayRuntimeSourceTree( + { + tree, + signal: new AbortController().signal, + openDestination: async () => { + throw new Error('destination open failure') + } + }, + operations + ).catch((value: unknown) => value) + + expect(error).toBeInstanceOf(AggregateError) + expect((error as AggregateError).errors).toEqual([ + expect.objectContaining({ message: 'destination open failure' }), + expect.objectContaining({ message: 'local close failure' }) + ]) + }) + + it('rejects incomplete and reused destinations without taking duplicate ownership', async () => { + const { tree } = await sourceStreamFixture() + await expect( + streamSshRelayRuntimeSourceTree({ + tree, + signal: new AbortController().signal, + openDestination: async () => undefined as unknown as SshRelayRuntimeSourceDestination + }) + ).rejects.toThrow(/destination is incomplete/i) + + const close = vi.fn(async () => {}) + const abort = vi.fn(async () => {}) + const destination = { write: async () => {}, close, abort } + await expect( + streamSshRelayRuntimeSourceTree({ + tree, + signal: new AbortController().signal, + maximumConcurrency: 1, + openDestination: async () => destination + }) + ).rejects.toThrow(/destination was reused/i) + expect(close).toHaveBeenCalledTimes(1) + expect(abort).not.toHaveBeenCalled() + }) + + it('aborts when destination close or the progress observer fails', async () => { + const { tree } = await sourceStreamFixture() + const closeAbort = vi.fn(async () => {}) + await expect( + streamSshRelayRuntimeSourceTree({ + tree, + signal: new AbortController().signal, + maximumConcurrency: 1, + openDestination: async () => ({ + write: async () => {}, + close: async () => { + throw new Error('destination close failed') + }, + abort: closeAbort + }) + }) + ).rejects.toThrow(/destination close failed/i) + expect(closeAbort).toHaveBeenCalledTimes(1) + + const progressAbort = vi.fn(async () => {}) + await expect( + streamSshRelayRuntimeSourceTree({ + tree, + signal: new AbortController().signal, + maximumConcurrency: 1, + openDestination: async () => ({ + write: async () => {}, + close: async () => {}, + abort: progressAbort + }), + onProgress: () => { + throw new Error('progress observer failed') + } + }) + ).rejects.toThrow(/progress observer failed/i) + expect(progressAbort).toHaveBeenCalledTimes(1) + }) + + it('asserts but never releases the borrowed lease before and after streaming', async () => { + const { tree } = await sourceStreamFixture() + const assertLeaseOwned = vi.fn(tree.assertLeaseOwned) + + await streamSshRelayRuntimeSourceTree({ + tree: { ...tree, assertLeaseOwned }, + signal: new AbortController().signal, + openDestination: async () => ({ + write: async () => {}, + close: async () => {}, + abort: async () => {} + }) + }) + + expect(assertLeaseOwned).toHaveBeenCalledTimes(2) + }) +}) diff --git a/src/main/ssh/ssh-relay-runtime-source-stream.ts b/src/main/ssh/ssh-relay-runtime-source-stream.ts new file mode 100644 index 00000000000..ebad3401f70 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-source-stream.ts @@ -0,0 +1,298 @@ +import { createHash } from 'node:crypto' + +import type { SshRelayRuntimeScannedSourceTree } from './ssh-relay-runtime-source-scan' +import { + assertSshRelayRuntimeOpenedFileSnapshot, + assertSshRelayRuntimeSourcePathSnapshot, + assertSshRelayRuntimeSourceTreeSnapshot, + SSH_RELAY_RUNTIME_SOURCE_STREAM_OPERATIONS, + sshRelayRuntimeSourceParentDirectories, + type SshRelayRuntimeScannedSourceFile, + type SshRelayRuntimeSourceFileHandle, + type SshRelayRuntimeSourceStreamOperations +} from './ssh-relay-runtime-source-snapshot' + +export type { + SshRelayRuntimeScannedSourceFile, + SshRelayRuntimeSourceStreamOperations +} from './ssh-relay-runtime-source-snapshot' + +const CHUNK_BYTES = 64 * 1024 +const MAXIMUM_CONCURRENT_FILES = 4 +const MEASUREMENT_TIMEOUT_MS = 20 * 60_000 +const MAXIMUM_INCREMENTAL_MEMORY_BYTES = 80 * 1024 * 1024 + +export type SshRelayRuntimeSourceDestination = Readonly<{ + write: (chunk: Buffer) => Promise + close: () => Promise + abort: (reason: unknown) => Promise +}> + +export type SshRelayRuntimeSourceStreamProgress = Readonly<{ + tupleId: SshRelayRuntimeScannedSourceTree['tupleId'] + contentId: SshRelayRuntimeScannedSourceTree['contentId'] + filesCompleted: number + totalFiles: number + bytesTransferred: number + totalBytes: number + activeFiles: number +}> + +export type SshRelayRuntimeSourceStreamResult = Readonly< + Omit +> + +export type SshRelayRuntimeSourceStreamOptions = Readonly<{ + tree: SshRelayRuntimeScannedSourceTree + signal: AbortSignal + maximumConcurrency?: number + openDestination: ( + file: SshRelayRuntimeScannedSourceFile, + signal: AbortSignal + ) => Promise + onProgress?: (progress: SshRelayRuntimeSourceStreamProgress) => void +}> + +function joinedFailure(primary: unknown, cleanup: readonly unknown[]): unknown { + return cleanup.length === 0 + ? primary + : new AggregateError([primary, ...cleanup], 'SSH relay runtime source stream cleanup failed') +} + +function assertSourceDestination( + destination: unknown +): asserts destination is SshRelayRuntimeSourceDestination { + if ( + typeof destination !== 'object' || + destination === null || + !('write' in destination) || + typeof destination.write !== 'function' || + !('close' in destination) || + typeof destination.close !== 'function' || + !('abort' in destination) || + typeof destination.abort !== 'function' + ) { + throw new Error('SSH relay runtime source stream destination is incomplete') + } +} + +export async function streamSshRelayRuntimeSourceTree( + options: SshRelayRuntimeSourceStreamOptions, + overrides: Partial = {} +): Promise { + const { tree, signal, openDestination, onProgress } = options + const maximumConcurrency = options.maximumConcurrency ?? 1 + if ( + !Number.isInteger(maximumConcurrency) || + maximumConcurrency < 1 || + maximumConcurrency > MAXIMUM_CONCURRENT_FILES + ) { + throw new Error( + `SSH relay runtime source stream concurrency must be between 1 and ${MAXIMUM_CONCURRENT_FILES}` + ) + } + const operations = { ...SSH_RELAY_RUNTIME_SOURCE_STREAM_OPERATIONS, ...overrides } + signal.throwIfAborted() + await tree.assertLeaseOwned() + await assertSshRelayRuntimeSourceTreeSnapshot(tree, signal, operations) + + let nextFileIndex = 0 + let filesCompleted = 0 + let bytesTransferred = 0 + let activeFiles = 0 + let stopReason: unknown + const failures: unknown[] = [] + const seenDestinations = new WeakSet() + + const emitProgress = (): void => { + onProgress?.( + Object.freeze({ + tupleId: tree.tupleId, + contentId: tree.contentId, + filesCompleted, + totalFiles: tree.fileCount, + bytesTransferred, + totalBytes: tree.expandedBytes, + activeFiles + }) + ) + } + + const assertRunning = (): void => { + signal.throwIfAborted() + if (stopReason !== undefined) { + throw stopReason + } + } + + const requestStop = (error: unknown): void => { + if (stopReason === undefined) { + stopReason = error + } + } + + const recordFailure = (error: unknown): void => { + const isCleanupFailure = + error instanceof AggregateError && error.errors.length > 0 && error.errors[0] === stopReason + if (isCleanupFailure) { + const unjoinedPrimaryIndex = failures.indexOf(stopReason) + if (unjoinedPrimaryIndex >= 0) { + failures.splice(unjoinedPrimaryIndex, 1) + } + } else if ( + error === stopReason && + failures.some( + (failure) => + failure instanceof AggregateError && + failure.errors.length > 0 && + failure.errors[0] === stopReason + ) + ) { + return + } + if (!failures.includes(error)) { + failures.push(error) + } + } + + const streamFile = async ( + file: SshRelayRuntimeScannedSourceFile, + buffer: Buffer + ): Promise => { + const parents = sshRelayRuntimeSourceParentDirectories(tree, file) + let handle: SshRelayRuntimeSourceFileHandle | undefined + let destination: SshRelayRuntimeSourceDestination | undefined + let destinationActive = false + try { + assertRunning() + await assertSshRelayRuntimeSourcePathSnapshot(tree, file, parents, signal, operations) + assertRunning() + handle = await operations.openFile(file.localPath) + assertRunning() + const opened = await handle.stat() + assertSshRelayRuntimeOpenedFileSnapshot(file, opened, 'opening') + + assertRunning() + const openedDestination: unknown = await openDestination(file, signal) + assertSourceDestination(openedDestination) + destination = openedDestination + if (seenDestinations.has(destination)) { + throw new Error('SSH relay runtime source stream destination was reused') + } + seenDestinations.add(destination) + destinationActive = true + activeFiles += 1 + + const digest = createHash('sha256') + let bytes = 0 + while (bytes < file.size) { + assertRunning() + const requested = Math.min(buffer.length, file.size - bytes) + const { bytesRead } = await handle.read(buffer, 0, requested, bytes) + if (bytesRead <= 0 || bytesRead > requested) { + break + } + assertRunning() + const chunk = buffer.subarray(0, bytesRead) + // Why: the awaited destination write is the buffer-lifetime boundary; transports must not + // retain the view after resolving, so the worker can reuse one bounded buffer. + await destination.write(chunk) + assertRunning() + digest.update(chunk) + bytes += bytesRead + bytesTransferred += bytesRead + emitProgress() + } + + assertRunning() + const afterRead = await handle.stat() + assertSshRelayRuntimeOpenedFileSnapshot(file, afterRead, 'streaming') + const closingHandle = handle + handle = undefined + await closingHandle.close() + await assertSshRelayRuntimeSourcePathSnapshot(tree, file, parents, signal, operations) + if (bytes !== file.size || `sha256:${digest.digest('hex')}` !== file.sha256) { + throw new Error(`SSH relay runtime source file size or integrity changed: ${file.path}`) + } + + assertRunning() + await destination.close() + destinationActive = false + destination = undefined + activeFiles -= 1 + filesCompleted += 1 + emitProgress() + } catch (error) { + // Why: peer workers must stop before potentially slow cleanup settles on the failing worker. + requestStop(error) + const cleanupFailures: unknown[] = [] + if (handle) { + const closingHandle = handle + handle = undefined + await closingHandle.close().catch((closeError: unknown) => cleanupFailures.push(closeError)) + } + if (destination && destinationActive) { + destinationActive = false + activeFiles -= 1 + await destination + .abort(error) + .catch((abortError: unknown) => cleanupFailures.push(abortError)) + } + throw joinedFailure(error, cleanupFailures) + } + } + + const worker = async (): Promise => { + const buffer = Buffer.allocUnsafe(CHUNK_BYTES) + while (stopReason === undefined) { + signal.throwIfAborted() + const index = nextFileIndex + if (index >= tree.files.length) { + return + } + nextFileIndex += 1 + try { + await streamFile(tree.files[index], buffer) + } catch (error) { + recordFailure(error) + return + } + } + } + + const workers = Array.from( + { length: Math.min(maximumConcurrency, Math.max(1, tree.files.length)) }, + () => worker() + ) + await Promise.allSettled(workers) + if (failures.length === 1) { + throw failures[0] + } + if (failures.length > 1) { + throw new AggregateError(failures, 'SSH relay runtime source stream failed') + } + signal.throwIfAborted() + await assertSshRelayRuntimeSourceTreeSnapshot(tree, signal, operations) + signal.throwIfAborted() + await tree.assertLeaseOwned() + signal.throwIfAborted() + + if (filesCompleted !== tree.fileCount || bytesTransferred !== tree.expandedBytes) { + throw new Error('SSH relay runtime source stream aggregate count or size is inconsistent') + } + return Object.freeze({ + tupleId: tree.tupleId, + contentId: tree.contentId, + filesCompleted, + totalFiles: tree.fileCount, + bytesTransferred, + totalBytes: tree.expandedBytes + }) +} + +export const SSH_RELAY_RUNTIME_SOURCE_STREAM_LIMITS = Object.freeze({ + chunkBytes: CHUNK_BYTES, + maximumConcurrentFiles: MAXIMUM_CONCURRENT_FILES, + measurementTimeoutMs: MEASUREMENT_TIMEOUT_MS, + maximumIncrementalMemoryBytes: MAXIMUM_INCREMENTAL_MEMORY_BYTES +}) From df39c287d63ae5f2cc8a3f4e2023558d8c4fefe3 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 09:30:05 -0400 Subject: [PATCH 212/267] test: scope SSH relay mode oracle to POSIX Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 14 +++- ...github-release-implementation-checklist.md | 66 ++++++++++++++++--- .../ssh-relay-runtime-source-stream.test.ts | 29 ++++++-- 3 files changed, 93 insertions(+), 16 deletions(-) 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 1fe407f584c..b3a742a4901 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 @@ -9,9 +9,19 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected bounded -client source stream is locally green; commit/push and exact-head native/full-size CI are next, +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — the Windows-only POSIX- +mode test correction is locally green; commit/push and fresh all-six native/full-size proof are next, 2026-07-15, Codex implementation owner.** +`E-M6-SOURCE-STREAM-CI-RED-001` records run `29501986883` / Windows x64 job `87632962424`: the +hostile wrong-mode case incorrectly expected POSIX mode enforcement on Windows, then production +correctly rejected the test's incomplete destination. Linux x64/arm64 and Darwin x64/arm64 purpose +commands passed before cancellation. Make only that case POSIX-only; no production behavior is +relaxed and no cell is credited from the superseded run. +`E-M6-SOURCE-STREAM-WINDOWS-ORACLE-CORRECTION-LOCAL-001` passes 27/27 purpose cases on macOS, 107 +focused cases with one declared skip, 566 broad relay cases with five declared skips, 280/280 +workflow cases, typecheck, full lint, format, max-lines, diff, protected-resolver, and isolation +gates. Only the POSIX-mode case changed; fresh Windows and all-six native/full-size CI remain +mandatory. `E-M6-SOURCE-STREAM-AUDIT-001` fixes the next artifact-only boundary: exact scanned tree plus one signal, maximum four 64-KiB readers, local snapshot proof before destination open, hash/size and post-read snapshot proof before destination finalization, joined abort/close cleanup, and path-free 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 e3c90b4ad57..df506afaf49 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected bounded client source stream is locally green; exact-head native/full-size CI is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **BOUNDED SOURCE STREAM LOCAL GREEN / EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-SOURCE-STREAM-LOCAL-001` proves the filesystem-snapshot split, bounded reader/destination lifecycle, failure settlement, purpose/full-size wiring, adjacent suites, static gates, and isolation locally. Commit and push this package, then require all-six native artifact/full-size jobs plus adjacent CI before checking the package or advancing to an SSH transport adapter. Do not add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: Windows source-stream oracle correction is locally green; fresh exact-head native/full-size CI is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **BOUNDED SOURCE STREAM WINDOWS ORACLE CORRECTION LOCAL GREEN — 2026-07-15, Codex implementation owner** — `E-M6-SOURCE-STREAM-CI-RED-001` records the Windows-only POSIX-mode oracle mismatch and `E-M6-SOURCE-STREAM-WINDOWS-ORACLE-CORRECTION-LOCAL-001` proves the test-only correction plus focused/broad/static gates. Commit/push the correction and require fresh all-six native/full-size and adjacent CI. Do not add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -152,10 +152,12 @@ same change as the work it records. E-M5-HOST-EVIDENCE-COMPOSITION-CI-001. It has no product caller. - Active package: **In progress — 2026-07-15, Codex implementation owner** — audit, RED, and local GREEN complete under E-M6-SOURCE-STREAM-AUDIT-001, E-M6-SOURCE-STREAM-LOCAL-RED-001, and - E-M6-SOURCE-STREAM-LOCAL-001. Commit and push the disconnected bounded client source stream, then - require exact-head all-six native/full-size and adjacent CI. Do not import SSH, open channels, - construct remote paths, create remote staging, or add product/settings/fallback/tuple/publication/ - default behavior. + E-M6-SOURCE-STREAM-LOCAL-001. E-M6-SOURCE-STREAM-CI-RED-001 exposes a Windows-only test-oracle + mismatch; its POSIX-only correction is locally green under + E-M6-SOURCE-STREAM-WINDOWS-ORACLE-CORRECTION-LOCAL-001. Commit/push it, then require fresh + exact-head all-six native/full-size and adjacent CI. Do not import SSH, open channels, construct + remote paths, create remote staging, or add product/settings/fallback/tuple/publication/default + behavior. - 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 @@ -16602,6 +16604,51 @@ src/main/ssh/ssh-relay-*.test.ts` — PASS, exit 0: 41 files passed, three skipp measurement, not SFTP/system-SSH or remote proof. Remote channels, paths, staging/install, product/Beta/fallback/tuple/publication/default wiring, and SignPath remain absent. +### E-M6-SOURCE-STREAM-CI-RED-001 — Windows exposes a POSIX-only mode-test oracle + +- Date/owner/source: 2026-07-15, Codex implementation owner; exact commit `59997b1383ef51d390d811b17e235917b51e4a69`, + artifact run [29501986883](https://github.com/stablyai/orca/actions/runs/29501986883), Windows x64 + job [87632962424](https://github.com/stablyai/orca/actions/runs/29501986883/job/87632962424). +- Result: expected CI RED in the newly added cross-platform purpose suite. Windows x64 reached the + `rejects wrong mode source metadata before destination creation` case and failed its + `/changed|mode/i` assertion because Windows deliberately does not compare POSIX mode bits. The + test then reached the mock factory, whose undefined result was correctly rejected as `SSH relay +runtime source stream destination is incomplete`. The contract step failed after 31 seconds and + the job after 1 minute 42 seconds; artifact download/build/full-size measurement did not run. +- Cross-run observation: Linux x64, Linux arm64, Darwin arm64, and Darwin x64 purpose commands + passed before cancellation. Darwin arm64 also completed its build/full-size measurement, but this + superseded run is not closure evidence. Windows arm64 had not reached the purpose command. +- Classification/correction: production snapshot behavior is correct and unchanged: POSIX compares + signed mode, while Windows relies on type and identity/state. Make only the wrong-mode oracle + `skipIf(process.platform === 'win32')`; linked/type/state/digest/mutation checks remain mandatory + on Windows. The run was cancelled after evidence capture to conserve native runner time. +- Safety/residual: the failure occurred in disconnected tests with no SSH/product importer, remote + resource, publication, tuple, setting, fallback, or default behavior. Fresh exact-head all-six + purpose/full-size and adjacent evidence is required; no cell is credited from this run. + +### E-M6-SOURCE-STREAM-WINDOWS-ORACLE-CORRECTION-LOCAL-001 — POSIX mode case is locally corrected + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0, + pnpm 10.24.0, atop exact implementation commit `59997b138` plus the test/checklist correction diff. +- Correction: remove only `wrong mode` from the cross-platform hostile-metadata table and add the + same assertion as `skipIf(process.platform === 'win32')`. POSIX still requires signed mode before + destination creation. Windows still requires linked/type/state/digest/mutation rejection and now + declares exactly one purpose-case skip. No production source changed. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-source-stream.test.ts` — PASS, 27/27 in 2.61 seconds + Vitest / 3.80 seconds wall, 141,737,984-byte maximum RSS, zero swaps. +- Adjacent/broad commands: the ten-file focused source/cache/acquisition/workflow command passes + 107 tests with one declared platform skip in 8.29 seconds; `src/main/ssh/ssh-relay-*.test.ts` + passes 566 with five declared platform/full-size skips in 25.38 seconds; and + `config/scripts/ssh-relay-runtime-*.test.mjs` passes 280/280 in 17.13 seconds. +- Static/isolation commands: `pnpm typecheck`, targeted `pnpm exec oxlint`, `pnpm lint`, + `pnpm run check:max-lines-ratchet`, targeted `pnpm exec oxfmt --check`, `git diff --check`, the + protected-resolver diff gate, and the non-test importer `rg` gate pass. Existing unrelated lint + warnings remain warnings; no max-lines bypass was added. +- Residual: a native Windows runner must prove 26 passes plus exactly the one POSIX-mode skip. Fresh + all-six full-size metrics and adjacent Actions remain mandatory; no evidence cell is closed by + local macOS proof. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -16660,10 +16707,9 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the locally green Milestone 6 disconnected bounded client source stream and its -purpose/full-size native workflow wiring, then require all-six exact-head native/full-size and -adjacent CI before checking the package or advancing. The stream must create no remote files or open -SSH channels. +Commit/push the locally green Windows test-oracle correction, then require fresh all-six exact-head +native/full-size and adjacent CI before checking the package or advancing. The stream must create no +remote files or open SSH channels. Do not open an SSH channel, transfer to or install on a remote, modify platform detection/selectors, add an Electron/startup consumer, mode wiring, fallback, tuple enablement, release publication, or default behavior. diff --git a/src/main/ssh/ssh-relay-runtime-source-stream.test.ts b/src/main/ssh/ssh-relay-runtime-source-stream.test.ts index 8a854cba380..e6855e55334 100644 --- a/src/main/ssh/ssh-relay-runtime-source-stream.test.ts +++ b/src/main/ssh/ssh-relay-runtime-source-stream.test.ts @@ -319,10 +319,6 @@ describe('SSH relay runtime bounded source stream', () => { (metadata) => Object.assign(Object.create(metadata), { isSymbolicLink: () => true }) ], ['wrong type', (metadata) => Object.assign(Object.create(metadata), { isFile: () => false })], - [ - 'wrong mode', - (metadata) => Object.assign(Object.create(metadata), { mode: metadata.mode ^ 1n }) - ], [ 'state drift', (metadata) => Object.assign(Object.create(metadata), { mtimeNs: metadata.mtimeNs + 1n }) @@ -347,6 +343,31 @@ describe('SSH relay runtime bounded source stream', () => { expect(openDestination).not.toHaveBeenCalled() }) + it.skipIf(process.platform === 'win32')( + 'rejects POSIX mode metadata before destination creation', + async () => { + const { tree } = await sourceStreamFixture() + const target = tree.files[0].localPath + const openDestination = vi.fn() + await expect( + streamSshRelayRuntimeSourceTree( + { tree, signal: new AbortController().signal, openDestination }, + { + lstat: async (path) => { + const metadata = (await lstat(path, { + bigint: true + })) as SshRelayRuntimeSourceMetadata + return path === target + ? Object.assign(Object.create(metadata), { mode: metadata.mode ^ 1n }) + : metadata + } + } + ) + ).rejects.toThrow(/mode/i) + expect(openDestination).not.toHaveBeenCalled() + } + ) + it('rejects source mutation before destination creation', async () => { const { tree } = await sourceStreamFixture() const openDestination = vi.fn() From 705bca2dba6547047555c036f130bb991c5535a6 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 09:56:17 -0400 Subject: [PATCH 213/267] docs: record SSH relay source stream CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 15 ++- ...github-release-implementation-checklist.md | 95 ++++++++++++++++++- 2 files changed, 104 insertions(+), 6 deletions(-) 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 b3a742a4901..3bb3dd0b586 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 @@ -9,9 +9,18 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — the Windows-only POSIX- -mode test correction is locally green; commit/push and fresh all-six native/full-size proof are next, -2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — bounded client source +streaming is exact-head green; the smallest disconnected SFTP adapter audit is next, 2026-07-15, +Codex implementation owner.** +`E-M6-SOURCE-STREAM-CI-001` closes exact commit `df39c287d`: all six primary native clients pass the +27-case source-stream suite (Windows has only the declared POSIX-mode skip), exact 85–125 MB trees +stream in 103.663–530.937 ms with 0–5,492,736 incremental RSS bytes, both Linux supplements and the +Windows x64 floor pass, and PR Checks/Golden E2E pass. The first computer-use Windows attempt hit two +unrelated unchanged five-second launcher timeouts under `E-M6-SOURCE-STREAM-ADJACENT-CI-RED-001`; +its same-SHA failed-job rerun passes the tests, packaged build, daemon smoke, and Windows E2E without +code or timeout changes. Windows arm64 retains only the declared hosted build-26200 rejection against +26100 after complete runtime proof. SFTP/system-SSH channels, remote staging/install/mode repair, +product/Beta/fallback/default wiring, publication, and SignPath remain absent. `E-M6-SOURCE-STREAM-CI-RED-001` records run `29501986883` / Windows x64 job `87632962424`: the hostile wrong-mode case incorrectly expected POSIX mode enforcement on Windows, then production correctly rejected the test's incomplete destination. Linux x64/arm64 and Darwin x64/arm64 purpose 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 df506afaf49..ace0e2e78a2 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: Windows source-stream oracle correction is locally green; fresh exact-head native/full-size CI is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **BOUNDED SOURCE STREAM WINDOWS ORACLE CORRECTION LOCAL GREEN — 2026-07-15, Codex implementation owner** — `E-M6-SOURCE-STREAM-CI-RED-001` records the Windows-only POSIX-mode oracle mismatch and `E-M6-SOURCE-STREAM-WINDOWS-ORACLE-CORRECTION-LOCAL-001` proves the test-only correction plus focused/broad/static gates. Commit/push the correction and require fresh all-six native/full-size and adjacent CI. Do not add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: bounded client source streaming is exact-head green; the smallest disconnected SFTP adapter audit is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **BOUNDED SOURCE STREAM EXACT-HEAD GREEN / SFTP ADAPTER AUDIT NEXT — 2026-07-15, Codex implementation owner** — `E-M6-SOURCE-STREAM-CI-001` closes all-six native purpose and exact full-size client-stream proof. The first adjacent computer-use Windows attempt exposed only two unrelated five-second launcher timeouts under `E-M6-SOURCE-STREAM-ADJACENT-CI-RED-001`; its same-SHA failed-job rerun is fully green without changing timeouts or code. Audit the smallest SFTP destination/session boundary before writing its RED tests. Do not add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -1166,7 +1166,11 @@ Current behavior must not be assumed adequate for a much larger runtime: E-M6-SOURCE-PRESCAN-LOCAL-RED-001, E-M6-SOURCE-PRESCAN-LOCAL-001, E-M6-SOURCE-PRESCAN-CI-RED-001, E-M6-SOURCE-PRESCAN-COLLISION-FIX-LOCAL-001, and E-M6-SOURCE-PRESCAN-CI-001. -- [ ] Stream files with bounded buffers; prohibit whole-tree memory materialization. +- [x] Stream files with bounded buffers; prohibit whole-tree memory materialization. Completed by + E-M6-SOURCE-STREAM-AUDIT-001, E-M6-SOURCE-STREAM-LOCAL-RED-001, + E-M6-SOURCE-STREAM-LOCAL-001, E-M6-SOURCE-STREAM-CI-RED-001, + E-M6-SOURCE-STREAM-WINDOWS-ORACLE-CORRECTION-LOCAL-001, and + E-M6-SOURCE-STREAM-CI-001. - [ ] Pass one AbortSignal through enumeration, channel creation, reads, writes, permission repair, and remote cleanup. - [ ] Close and await every local stream, SSH channel, child process, SFTP session, and cleanup task @@ -16649,6 +16653,91 @@ runtime source stream destination is incomplete`. The contract step failed after all-six full-size metrics and adjacent Actions remain mandatory; no evidence cell is closed by local macOS proof. +### E-M6-SOURCE-STREAM-ADJACENT-CI-RED-001 — unrelated Windows launcher timeouts require rerun + +- Date/owner/source: 2026-07-15, Codex implementation owner; exact commit + `df39c287d63ae5f2cc8a3f4e2023558d8c4fefe3`, computer-use run + [29502462430](https://github.com/stablyai/orca/actions/runs/29502462430), first-attempt Windows job + [87634610647](https://github.com/stablyai/orca/actions/runs/29502462430/job/87634610647). +- Result: the broad computer-use job's initial Windows attempt failed because the pre-existing + `config/scripts/build-windows-cli-launcher.test.mjs:29` and + `src/main/ssh/ssh-remote-cli-launcher.test.ts:55` cases each exceeded their unchanged five-second + timeout. Neither file imports the new source-tree, pre-scan, or source-stream modules. The exact + source-stream Windows contract job separately passed its 26 applicable cases with one declared + POSIX-mode skip and completed its exact full-size proof. +- Disposition: do not raise timeouts and do not credit the adjacent workflow from this attempt. + Rerun only the failed Windows job on the same SHA; a repeat would require independent diagnosis + before accepting the source-stream checkpoint. + +### E-M6-SOURCE-STREAM-CI-001 — all-six native and exact full-size source streaming passes + +- Date/owner/head: 2026-07-15, Codex implementation owner; exact correction commit + `df39c287d63ae5f2cc8a3f4e2023558d8c4fefe3` on draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). +- Native workflow: SSH Relay Runtime Artifacts run + [29502462515](https://github.com/stablyai/orca/actions/runs/29502462515). All six primary Node + 24.18.0 jobs pass the contract, deterministic-build/smoke, exact full-size extraction/cache/scan/ + stream, and evidence-upload steps: Darwin arm64 + [87634610869](https://github.com/stablyai/orca/actions/runs/29502462515/job/87634610869), Windows x64 + [87634610874](https://github.com/stablyai/orca/actions/runs/29502462515/job/87634610874), Windows arm64 + [87634610969](https://github.com/stablyai/orca/actions/runs/29502462515/job/87634610969), Linux arm64 + [87634610973](https://github.com/stablyai/orca/actions/runs/29502462515/job/87634610973), Linux x64 + [87634611056](https://github.com/stablyai/orca/actions/runs/29502462515/job/87634611056), and Darwin x64 + [87634611171](https://github.com/stablyai/orca/actions/runs/29502462515/job/87634611171). +- Runner identities: Darwin arm64 uses `macos-15` / `macos15` image `20260706.0213.1`; Darwin x64 + uses `macos-15-intel` / `macos15` image `20260715.0340.1`; Linux arm64 uses `ubuntu-24.04-arm` / + `ubuntu24-arm64` image `20260714.61.1`; Linux x64 uses `ubuntu-24.04` / `ubuntu24` image + `20260705.232.1`; Windows arm64 uses `windows-11-arm` / `win11-arm64` image `20260706.102.1`; + Windows x64 uses `windows-2022` / `win22` image `20260714.244.1`. Every job reports + `github-hosted`, its matching native architecture, and the exact source SHA above. +- Purpose results: every POSIX client passes all 27 cases; each Windows client passes 26 with only + the declared POSIX-mode case skipped. Suite times are 2.077 seconds Darwin arm64, 5.956 seconds + Darwin x64, 3.703 seconds Linux arm64, 2.412 seconds Linux x64, 13.744 seconds Windows x64, and + 9.814 seconds Windows arm64. Overall native contract totals are 733 passed / two skipped on each + Darwin client, 734 passed / one skipped on each Linux client, and 721 passed / 18 skipped on each + Windows client. +- Exact full-size stream metrics, after complete-tree pre-scan and while the verified cache lease is + held: + - Linux x64: 34 files / 124,846,430 bytes; stream 177.507 ms / 131,072 incremental RSS bytes. + - Linux arm64: 34 files / 122,865,324 bytes; stream 147.701 ms / 393,216 incremental RSS bytes. + - Darwin x64: 35 files / 124,316,655 bytes; stream 530.937 ms / 266,240 incremental RSS bytes. + - Darwin arm64: 35 files / 122,027,869 bytes; stream 103.663 ms / 163,840 incremental RSS bytes. + - Windows x64: 42 files / 96,527,161 bytes; stream 180.443 ms / 5,492,736 incremental RSS bytes. + - Windows arm64: 42 files / 85,213,511 bytes; stream 353.213 ms / zero incremental RSS bytes. + Every cell is below the 120-second and 80-MiB stream budgets; observed ranges are + 103.663–530.937 ms and 0–5,492,736 incremental RSS bytes. The source-stream contract separately + proves one reusable 64-KiB buffer per one-to-four workers, exact file/destination counts, and + joined local-handle/destination settlement on success, failure, and cancellation. +- Baselines: Linux arm64 supplement + [87635652453](https://github.com/stablyai/orca/actions/runs/29502462515/job/87635652453) and Linux + x64 supplement + [87635652461](https://github.com/stablyai/orca/actions/runs/29502462515/job/87635652461) pass. + Windows x64 oldest baseline + [87637747935](https://github.com/stablyai/orca/actions/runs/29502462515/job/87637747935) passes. + Windows arm64 oldest baseline + [87637747923](https://github.com/stablyai/orca/actions/runs/29502462515/job/87637747923) retains only + the declared hosted-image floor rejection: observed build 26200 versus required 26100. Before + that rejection it verifies the 60-entry / 42-file / 85,213,511-byte tree, Node/PTY/watcher smoke, + two-second resource settlement, and 5,768.3331-ms smoke at 48,476,160 RSS; complete verification + is 7,684.2782 ms. The aggregate workflow is therefore red only by the known runner-floor evidence + gap, not the source-stream package. +- Adjacent exact-head evidence: PR Checks run + [29502462631](https://github.com/stablyai/orca/actions/runs/29502462631), verify job + [87634610912](https://github.com/stablyai/orca/actions/runs/29502462631/job/87634610912), and Golden + E2E run [29502462423](https://github.com/stablyai/orca/actions/runs/29502462423), Linux job + [87634610095](https://github.com/stablyai/orca/actions/runs/29502462423/job/87634610095) and macOS job + [87634610096](https://github.com/stablyai/orca/actions/runs/29502462423/job/87634610096), pass. + Computer-use run [29502462430](https://github.com/stablyai/orca/actions/runs/29502462430) passes + after the evidence-gated same-SHA failed-job rerun: Windows job + [87639449051](https://github.com/stablyai/orca/actions/runs/29502462430/job/87639449051) passes the + previously timed-out tests plus packaged build/daemon/E2E; Ubuntu job + [87639450403](https://github.com/stablyai/orca/actions/runs/29502462430/job/87639450403) passes. +- Boundary/residual gaps: this hashes into a client-only discard destination; it creates no SSH + connection, SFTP session, system-SSH channel, remote path, staging/install state, mode repair, or + remote cleanup. It is not live transfer or remote proof. Every transport adapter, remote hash/ + install/launch step, product/Beta/fallback/tuple/publication/default path, and SignPath remain + open. Legacy remains the sole production/default path and every tuple remains disabled. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 72d482201e99975bb0133d1934906a95d2033934 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 10:06:52 -0400 Subject: [PATCH 214/267] feat: add bounded SSH relay SFTP file destination Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 18 +- ...github-release-implementation-checklist.md | 89 +++++- ...elay-runtime-sftp-file-destination.test.ts | 289 ++++++++++++++++++ ...ssh-relay-runtime-sftp-file-destination.ts | 210 +++++++++++++ 6 files changed, 604 insertions(+), 5 deletions(-) create mode 100644 src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts create mode 100644 src/main/ssh/ssh-relay-runtime-sftp-file-destination.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 3c2b0e75914..7c18c15a716 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -285,6 +285,7 @@ jobs: src/main/ssh/ssh-relay-runtime-source-tree.test.ts \ src/main/ssh/ssh-relay-runtime-source-scan.test.ts \ src/main/ssh/ssh-relay-runtime-source-stream.test.ts \ + src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts \ src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts \ src/main/ssh/ssh-relay-official-manifest.test.ts \ src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts \ @@ -840,6 +841,7 @@ jobs: src/main/ssh/ssh-relay-runtime-source-tree.test.ts ` src/main/ssh/ssh-relay-runtime-source-scan.test.ts ` src/main/ssh/ssh-relay-runtime-source-stream.test.ts ` + src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts ` src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts ` src/main/ssh/ssh-relay-official-manifest.test.ts ` src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 3b99c61f563..53ff1bf7bfb 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -143,6 +143,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-runtime-source-tree.test.ts', 'ssh-relay-runtime-source-scan.test.ts', 'ssh-relay-runtime-source-stream.test.ts', + 'ssh-relay-runtime-sftp-file-destination.test.ts', 'ssh-relay-compiled-manifest-trust.test.ts', 'ssh-relay-official-manifest.test.ts', 'ssh-relay-manifest-accepted-keys.test.ts', 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 3bb3dd0b586..e7240664173 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 @@ -9,9 +9,21 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — bounded client source -streaming is exact-head green; the smallest disconnected SFTP adapter audit is next, 2026-07-15, -Codex implementation owner.** +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected SFTP +per-file destination is locally green; exact-head native proof is next, 2026-07-15, Codex +implementation owner.** `E-M6-SFTP-FILE-DESTINATION-AUDIT-001` fixes exclusive `wx` open, callback-bounded +positional writes, explicit POSIX mode repair/handle-stat proof, and joined close-before-unlink +cleanup. It deliberately borrows but does not acquire/end an SFTP session and does not create +directories, clean a whole staging tree, or publish/launch anything. Session-wide cancellation, +full-size live SFTP, product/default wiring, and SignPath remain later gates. +`E-M6-SFTP-FILE-DESTINATION-LOCAL-RED-001` records the expected missing-module failure for the +12-case purpose contract; the 7/7 native workflow oracle already pins it once in both command +families. Protected resolvers remain untouched and no production importer exists. +`E-M6-SFTP-FILE-DESTINATION-LOCAL-001` passes the hardened 13-case purpose suite, 103 focused cases +with one declared skip, 579 broad relay cases with five declared skips, 280/280 workflow cases, +typecheck, full lint, targeted format, max-lines, diff, protected-resolver, and no-product-import +gates. It proves the exact per-file lifecycle only; SFTP session/tree orchestration, live/full-size +remote proof, and every product/default path remain open. `E-M6-SOURCE-STREAM-CI-001` closes exact commit `df39c287d`: all six primary native clients pass the 27-case source-stream suite (Windows has only the declared POSIX-mode skip), exact 85–125 MB trees stream in 103.663–530.937 ms with 0–5,492,736 incremental RSS bytes, both Linux supplements and the 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 ace0e2e78a2..7b27094bb36 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: bounded client source streaming is exact-head green; the smallest disconnected SFTP adapter audit is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **BOUNDED SOURCE STREAM EXACT-HEAD GREEN / SFTP ADAPTER AUDIT NEXT — 2026-07-15, Codex implementation owner** — `E-M6-SOURCE-STREAM-CI-001` closes all-six native purpose and exact full-size client-stream proof. The first adjacent computer-use Windows attempt exposed only two unrelated five-second launcher timeouts under `E-M6-SOURCE-STREAM-ADJACENT-CI-RED-001`; its same-SHA failed-job rerun is fully green without changing timeouts or code. Audit the smallest SFTP destination/session boundary before writing its RED tests. Do not add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected SFTP per-file destination is locally green; exact-head native proof is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **SFTP PER-FILE DESTINATION LOCAL GREEN — 2026-07-15, Codex implementation owner** — `E-M6-SFTP-FILE-DESTINATION-LOCAL-001` passes the hardened 13-case purpose contract, focused/broad/workflow tests, and all static/isolation gates. It proves exclusive remote open, exact callback-bounded offsets, in-flight cancellation joining, explicit POSIX mode repair/handle-stat proof, Windows mode skip, close-before-unlink cleanup, and idempotent aggregation. Commit/push this disconnected package and require exact-head all-six native/adjacent CI before advancing to SFTP session/tree orchestration. The adapter borrows but does not acquire/end an SFTP session and does not create directories or publish/launch a tree; session-wide cancellation and whole-staging cleanup remain later gates. Do not add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -16738,6 +16738,91 @@ runtime source stream destination is incomplete`. The contract step failed after install/launch step, product/Beta/fallback/tuple/publication/default path, and SignPath remain open. Legacy remains the sole production/default path and every tuple remains disabled. +### E-M6-SFTP-FILE-DESTINATION-AUDIT-001 — exclusive bounded SFTP file lifecycle boundary + +- Date/owner: 2026-07-15, Codex implementation owner. +- Exact input boundary: accept only a caller-owned SFTP file primitive set, one already validated + exact remote staging-file path, the signed `0o644` or `0o755` mode, an explicit POSIX-mode flag, + and the same required caller-owned `AbortSignal` used by the source streamer. Do not derive any + remote path from a client-native local path and do not acquire or end the SFTP session here. +- Open/write contract: check cancellation before open; use only exclusive `wx` open; once the handle + exists this adapter owns that new file. Each `write(chunk)` issues exactly one positional SFTP + write and resolves only from its callback, making that callback the source stream's reusable + 64-KiB buffer-lifetime boundary. Writes are sequential, offsets are exact, empty/concurrent/late + writes are rejected, and no chunk/tree concatenation or base64 representation exists. +- Completion contract: for POSIX remotes, apply the signed mode through the owned handle, re-read + handle attributes, and require the exact permission bits before closing. Windows/non-POSIX + remotes skip POSIX mode operations explicitly. A close is successful only after the remote handle + callback settles and a final signal check passes. +- Failure/cancellation contract: an open failure never unlinks because no ownership was obtained. + Any later write, mode, verification, close, or cancellation failure leaves the destination + abortable; abort attempts owned-handle close before unlink, joins cleanup failures, and is + idempotent. Cancellation during an in-flight SFTP callback remains joined until that callback is + forced to settle by the later session owner, preventing reuse of a possibly retained source + buffer. +- Required purpose RED: missing destination module; exclusive open and exact offsets; deferred + callback/buffer lifetime; pre-open and post-open cancellation; write/mode/stat/close failures; + POSIX exact-mode verification; explicit Windows mode skip; no deletion on failed exclusive open; + close-before-unlink order; joined/idempotent cleanup; illegal concurrent/empty/late operations; + and path-free adapter-authored diagnostics. Add the suite exactly once to both native artifact + command families before implementation. +- Explicit residual boundary: this is a per-file primitive, not an SFTP transfer. SFTP session + acquisition/end/error ownership, bounded tree concurrency, remote directory creation, remote-path + policy, session-wide timeout/cancellation, full-staging cleanup, real server behavior, full-size + transfer, mode behavior across servers, and later remote tree verification/install/publish/launch + all remain open. No production importer, setting, fallback, tuple, release, or default path may be + added by this package; legacy remains the sole production path. + +### E-M6-SFTP-FILE-DESTINATION-LOCAL-RED-001 — destination lifecycle is absent + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0, + pnpm 10.24.0, atop exact checkpoint commit `705bca2db` plus the purpose/workflow/checklist RED diff. +- Purpose command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts` — expected RED, exit 1: one failed + suite, zero collected tests, because `./ssh-relay-runtime-sftp-file-destination` does not exist. + The 260-line source contract declares 12 cases including two parameterized failure cells. +- Native workflow oracle: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 7/7 in 356 ms. The workflow source and + oracle already require the purpose suite exactly once in both POSIX and PowerShell native command + families, so implementation cannot become locally green while native coverage remains omitted. +- Static precondition: `git diff --check` passes and the test remains below the max-lines ratchet at + 260 lines. Protected resolver files remain untouched. No production importer, SFTP session owner, + remote directory/tree orchestrator, product caller, setting, fallback, tuple, publication, or + default behavior exists in the RED diff. + +### E-M6-SFTP-FILE-DESTINATION-LOCAL-001 — per-file SFTP lifecycle is locally green + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0, + pnpm 10.24.0, atop exact checkpoint commit `705bca2db` plus the implementation/checklist diff. +- Implementation: the 210-line `ssh-relay-runtime-sftp-file-destination.ts` accepts only the audited + primitive boundary. It opens with `wx` and signed mode, awaits one SFTP positional-write callback + per source chunk before advancing the exact offset, checks the same signal around every callback, + repairs and verifies POSIX permission bits before close, and skips those operations only from the + explicit non-POSIX flag. Adapter-authored state/input errors contain no remote path. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts` — PASS, 13/13 in + 431 ms Vitest / 2.36 seconds wall, 133,005,312-byte maximum RSS, zero swaps. The additional + hardening case proves cancellation during a pending write cannot settle or begin cleanup until the + SFTP callback releases the borrowed source buffer; it then closes before unlinking. +- Focused command: the destination, source stream/scan/tree, acquisition/cache population and + resolution, integration, and workflow-oracle ten-file command — PASS, 103 tests with one declared + platform skip in 8.28 seconds. +- Broad commands: `src/main/ssh/ssh-relay-*.test.ts` — PASS, 579 tests with five declared platform/ + full-size skips in 27.25 seconds; `config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 280/280 in + 20.82 seconds. The first quoted-glob invocations matched no files and were discarded as command + syntax mistakes; the unquoted exact checklist commands above are the credited results. +- Static/isolation commands: `pnpm typecheck`, targeted `pnpm exec oxlint`, `pnpm lint`, targeted + `pnpm exec oxfmt --check`, `pnpm run check:max-lines-ratchet`, `git diff --check`, the protected- + resolver diff gate, and the no-non-test-importer `rg` gate pass. Full lint includes switch + exhaustiveness, styled-scrollbar, 41 reliability, bundled-skill, max-lines, and localization gates; + only pre-existing unrelated warnings remain. The module/test are 210/289 lines with no bypass. +- Residual: this is callback-contract proof with mocked primitives, not a live SFTP server, session, + SSH connection, remote directory/staging tree, bounded multi-file transfer, session-wide abort/ + timeout, whole-tree cleanup, or full-size remote measurement. It has no non-test importer. Exact- + head all-six Node 24 and adjacent Actions evidence is required before auditing the session/tree + adapter. Product/Beta/fallback/tuple/publication/default behavior and SignPath remain absent; + legacy remains the sole production path. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts b/src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts new file mode 100644 index 00000000000..61b39bde8e0 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts @@ -0,0 +1,289 @@ +import { describe, expect, it, vi } from 'vitest' + +import { + openSshRelayRuntimeSftpFileDestination, + type SshRelayRuntimeSftpFileOperations +} from './ssh-relay-runtime-sftp-file-destination' + +type Callback = (error?: Error) => void + +function createOperations(options: { mode?: number } = {}): { + operations: SshRelayRuntimeSftpFileOperations + events: string[] + handle: Buffer +} { + const handle = Buffer.from('owned-handle') + const events: string[] = [] + const operations = { + open: vi.fn( + ( + _path: string, + _flags: string, + _attributes: { mode: number }, + callback: (error: Error | undefined, value: Buffer) => void + ) => { + events.push('open') + callback(undefined, handle) + } + ), + write: vi.fn( + ( + _handle: Buffer, + _buffer: Buffer, + _offset: number, + _length: number, + _position: number, + callback: Callback + ) => { + events.push('write') + callback() + } + ), + fchmod: vi.fn((_handle: Buffer, _mode: number, callback: Callback) => { + events.push('fchmod') + callback() + }), + fstat: vi.fn( + (_handle: Buffer, callback: (error: Error | undefined, value: { mode: number }) => void) => { + events.push('fstat') + callback(undefined, { mode: options.mode ?? 0o100755 }) + } + ), + close: vi.fn((_handle: Buffer, callback: Callback) => { + events.push('close') + callback() + }), + unlink: vi.fn((_path: string, callback: Callback) => { + events.push('unlink') + callback() + }) + } as unknown as SshRelayRuntimeSftpFileOperations + return { operations, events, handle } +} + +function openDestination( + operations: SshRelayRuntimeSftpFileOperations, + options: { signal?: AbortSignal; enforcePosixMode?: boolean; remotePath?: string } = {} +) { + return openSshRelayRuntimeSftpFileDestination({ + operations, + remotePath: options.remotePath ?? '/owned-staging/bin/node', + mode: 0o755, + enforcePosixMode: options.enforcePosixMode ?? true, + signal: options.signal ?? new AbortController().signal + }) +} + +describe('SSH relay runtime SFTP file destination', () => { + it('opens exclusively and awaits each exact positional write callback', async () => { + const { operations, handle } = createOperations() + let firstCallback: Callback | undefined + vi.mocked(operations.write).mockImplementationOnce( + (_handle, _buffer, _offset, _length, _position, callback) => { + firstCallback = callback + } + ) + + const destination = await openDestination(operations) + const firstChunk = Buffer.from('abc') + let firstSettled = false + const firstWrite = destination.write(firstChunk).then(() => { + firstSettled = true + }) + await Promise.resolve() + expect(firstSettled).toBe(false) + expect(operations.open).toHaveBeenCalledWith( + '/owned-staging/bin/node', + 'wx', + { mode: 0o755 }, + expect.any(Function) + ) + expect(operations.write).toHaveBeenNthCalledWith( + 1, + handle, + firstChunk, + 0, + 3, + 0, + expect.any(Function) + ) + + firstCallback?.() + await firstWrite + const secondChunk = Buffer.from('de') + await destination.write(secondChunk) + expect(operations.write).toHaveBeenNthCalledWith( + 2, + handle, + secondChunk, + 0, + 2, + 3, + expect.any(Function) + ) + }) + + it('repairs and verifies exact POSIX mode before closing the owned handle', async () => { + const { operations, events, handle } = createOperations() + const destination = await openDestination(operations) + await destination.close() + + expect(operations.fchmod).toHaveBeenCalledWith(handle, 0o755, expect.any(Function)) + expect(operations.fstat).toHaveBeenCalledWith(handle, expect.any(Function)) + expect(events).toEqual(['open', 'fchmod', 'fstat', 'close']) + }) + + it('skips POSIX mode operations only when the caller explicitly disables them', async () => { + const { operations, events } = createOperations() + const destination = await openDestination(operations, { enforcePosixMode: false }) + await destination.close() + + expect(events).toEqual(['open', 'close']) + expect(operations.fchmod).not.toHaveBeenCalled() + expect(operations.fstat).not.toHaveBeenCalled() + }) + + it('rejects a mode mismatch and removes the owned incomplete file on abort', async () => { + const { operations, events } = createOperations({ mode: 0o100644 }) + const destination = await openDestination(operations) + await expect(destination.close()).rejects.toThrow(/mode/i) + await destination.abort(new Error('mode mismatch')) + + expect(events).toEqual(['open', 'fchmod', 'fstat', 'close', 'unlink']) + }) + + it.each(['fchmod', 'fstat'] as const)('fails closed when %s fails', async (operation) => { + const { operations } = createOperations() + vi.mocked(operations[operation]).mockImplementationOnce((...args: unknown[]) => { + const callback = args.at(-1) as Callback + callback(new Error(`${operation} failed`)) + }) + const destination = await openDestination(operations) + + await expect(destination.close()).rejects.toThrow(`${operation} failed`) + await destination.abort(new Error(`${operation} failed`)) + expect(operations.close).toHaveBeenCalledOnce() + expect(operations.unlink).toHaveBeenCalledOnce() + }) + + it('does not unlink a pre-existing file when exclusive open fails', async () => { + const { operations } = createOperations() + vi.mocked(operations.open).mockImplementationOnce((...args: unknown[]) => { + const callback = args.at(-1) as (error: Error) => void + callback(new Error('already exists')) + }) + + await expect(openDestination(operations)).rejects.toThrow('already exists') + expect(operations.close).not.toHaveBeenCalled() + expect(operations.unlink).not.toHaveBeenCalled() + }) + + it('rejects pre-open cancellation without creating a remote file', async () => { + const { operations } = createOperations() + const controller = new AbortController() + controller.abort(new Error('cancelled before open')) + + await expect(openDestination(operations, { signal: controller.signal })).rejects.toThrow( + 'cancelled before open' + ) + expect(operations.open).not.toHaveBeenCalled() + }) + + it('cleans an exclusively opened file when cancellation wins the open callback', async () => { + const { operations, events, handle } = createOperations() + const controller = new AbortController() + vi.mocked(operations.open).mockImplementationOnce((...args: unknown[]) => { + const callback = args.at(-1) as (error: undefined, value: Buffer) => void + controller.abort(new Error('cancelled after open')) + callback(undefined, handle) + }) + + await expect(openDestination(operations, { signal: controller.signal })).rejects.toThrow( + 'cancelled after open' + ) + expect(events).toEqual(['close', 'unlink']) + }) + + it('joins cancellation to the in-flight write callback before cleanup', async () => { + const { operations, events } = createOperations() + const controller = new AbortController() + let writeCallback: Callback | undefined + vi.mocked(operations.write).mockImplementationOnce( + (_handle, _buffer, _offset, _length, _position, callback) => { + events.push('write') + writeCallback = callback + } + ) + const destination = await openDestination(operations, { signal: controller.signal }) + let settled = false + const write = destination.write(Buffer.from('bytes')).finally(() => { + settled = true + }) + + controller.abort(new Error('cancelled during write')) + await Promise.resolve() + expect(settled).toBe(false) + expect(operations.close).not.toHaveBeenCalled() + writeCallback?.() + await expect(write).rejects.toThrow('cancelled during write') + await destination.abort(controller.signal.reason) + expect(events).toEqual(['open', 'write', 'close', 'unlink']) + }) + + it('propagates a write failure and then closes before unlinking', async () => { + const { operations, events } = createOperations() + vi.mocked(operations.write).mockImplementationOnce((...args: unknown[]) => { + const callback = args.at(-1) as Callback + events.push('write') + callback(new Error('remote write failed')) + }) + const destination = await openDestination(operations) + + await expect(destination.write(Buffer.from('bytes'))).rejects.toThrow('remote write failed') + await destination.abort(new Error('remote write failed')) + expect(events).toEqual(['open', 'write', 'close', 'unlink']) + }) + + it('joins close and unlink cleanup failures and makes abort idempotent', async () => { + const { operations } = createOperations() + vi.mocked(operations.close).mockImplementation((_handle, callback) => + callback(new Error('close cleanup failed')) + ) + vi.mocked(operations.unlink).mockImplementation((_path, callback) => + callback(new Error('unlink cleanup failed')) + ) + const destination = await openDestination(operations) + + const firstAbort = destination.abort(new Error('primary')) + const secondAbort = destination.abort(new Error('secondary')) + expect(firstAbort).toBe(secondAbort) + await expect(firstAbort).rejects.toMatchObject({ + errors: [ + expect.objectContaining({ message: 'close cleanup failed' }), + expect.objectContaining({ message: 'unlink cleanup failed' }) + ] + }) + expect(operations.close).toHaveBeenCalledOnce() + expect(operations.unlink).toHaveBeenCalledOnce() + }) + + it('rejects empty, concurrent, and late writes with path-free diagnostics', async () => { + const { operations } = createOperations() + let writeCallback: Callback | undefined + vi.mocked(operations.write).mockImplementationOnce( + (_handle, _buffer, _offset, _length, _position, callback) => { + writeCallback = callback + } + ) + const secretPath = '/home/private-user/staging/node' + const destination = await openDestination(operations, { remotePath: secretPath }) + + await expect(destination.write(Buffer.alloc(0))).rejects.not.toThrow(secretPath) + const pending = destination.write(Buffer.from('first')) + await expect(destination.write(Buffer.from('second'))).rejects.toThrow(/concurrent/i) + writeCallback?.() + await pending + await destination.close() + await expect(destination.write(Buffer.from('late'))).rejects.toThrow(/closed/i) + }) +}) diff --git a/src/main/ssh/ssh-relay-runtime-sftp-file-destination.ts b/src/main/ssh/ssh-relay-runtime-sftp-file-destination.ts new file mode 100644 index 00000000000..2d62d610975 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-sftp-file-destination.ts @@ -0,0 +1,210 @@ +import type { SshRelayRuntimeSourceDestination } from './ssh-relay-runtime-source-stream' + +type SftpCallback = (error?: Error) => void + +export type SshRelayRuntimeSftpFileOperations = Readonly<{ + open: ( + path: string, + flags: 'wx', + attributes: { mode: number }, + callback: (error: Error | undefined, handle: Buffer) => void + ) => void + write: ( + handle: Buffer, + buffer: Buffer, + offset: number, + length: number, + position: number, + callback: SftpCallback + ) => void + fchmod: (handle: Buffer, mode: number, callback: SftpCallback) => void + fstat: ( + handle: Buffer, + callback: (error: Error | undefined, attributes: { mode: number }) => void + ) => void + close: (handle: Buffer, callback: SftpCallback) => void + unlink: (path: string, callback: SftpCallback) => void +}> + +export type OpenSshRelayRuntimeSftpFileDestinationOptions = Readonly<{ + operations: SshRelayRuntimeSftpFileOperations + remotePath: string + mode: 0o644 | 0o755 + enforcePosixMode: boolean + signal: AbortSignal +}> + +type DestinationState = 'open' | 'closed' | 'complete' | 'aborting' | 'aborted' + +function waitForSftpCallback(register: (callback: SftpCallback) => void): Promise { + return new Promise((resolve, reject) => { + register((error) => (error ? reject(error) : resolve())) + }) +} + +function waitForSftpValue( + register: (callback: (error: Error | undefined, value: T) => void) => void +): Promise { + return new Promise((resolve, reject) => { + register((error, value) => (error ? reject(error) : resolve(value))) + }) +} + +function cleanupFailure(failures: readonly unknown[]): unknown | undefined { + if (failures.length === 0) { + return undefined + } + return failures.length === 1 + ? failures[0] + : new AggregateError(failures, 'SSH relay runtime SFTP file cleanup failed') +} + +function validateOptions(options: OpenSshRelayRuntimeSftpFileDestinationOptions): void { + if (!options.operations || typeof options.remotePath !== 'string' || options.remotePath === '') { + throw new Error('SSH relay runtime SFTP file destination input is invalid') + } + if (options.remotePath.includes('\0')) { + throw new Error('SSH relay runtime SFTP file destination path is invalid') + } + if (options.mode !== 0o644 && options.mode !== 0o755) { + throw new Error('SSH relay runtime SFTP file destination mode is invalid') + } +} + +function createDestination( + options: OpenSshRelayRuntimeSftpFileDestinationOptions, + handle: Buffer +): SshRelayRuntimeSourceDestination { + const { operations, remotePath, mode, enforcePosixMode, signal } = options + let state: DestinationState = 'open' + let position = 0 + let activeOperation: Promise | undefined + let abortPromise: Promise | undefined + + const assertWritable = (): void => { + if (state !== 'open') { + throw new Error('SSH relay runtime SFTP file destination is closed') + } + if (activeOperation) { + throw new Error('SSH relay runtime SFTP file destination has a concurrent operation') + } + } + + const runExclusive = (operation: () => Promise): Promise => { + const pending = operation() + activeOperation = pending + void pending + .finally(() => { + if (activeOperation === pending) { + activeOperation = undefined + } + }) + .catch(() => {}) + return pending + } + + const write = (chunk: Buffer): Promise => { + try { + assertWritable() + } catch (error) { + return Promise.reject(error) + } + if (!Buffer.isBuffer(chunk) || chunk.length === 0) { + return Promise.reject(new Error('SSH relay runtime SFTP file destination chunk is empty')) + } + return runExclusive(async () => { + signal.throwIfAborted() + // Why: the callback is the remote-consumption boundary; resolving earlier could let the + // source worker reuse its one buffer while ssh2 still retains the chunk view. + await waitForSftpCallback((callback) => + operations.write(handle, chunk, 0, chunk.length, position, callback) + ) + position += chunk.length + signal.throwIfAborted() + }) + } + + const close = (): Promise => { + try { + assertWritable() + } catch (error) { + return Promise.reject(error) + } + return runExclusive(async () => { + signal.throwIfAborted() + if (enforcePosixMode) { + await waitForSftpCallback((callback) => operations.fchmod(handle, mode, callback)) + signal.throwIfAborted() + const attributes = await waitForSftpValue<{ mode: number }>((callback) => + operations.fstat(handle, callback) + ) + if (!Number.isInteger(attributes.mode) || (attributes.mode & 0o777) !== mode) { + throw new Error('SSH relay runtime SFTP file mode verification failed') + } + } + signal.throwIfAborted() + await waitForSftpCallback((callback) => operations.close(handle, callback)) + state = 'closed' + signal.throwIfAborted() + state = 'complete' + }) + } + + const performAbort = async (): Promise => { + if (state === 'complete' || state === 'aborted') { + return + } + const pending = activeOperation + if (pending) { + await pending.catch(() => {}) + } + const handleNeedsClose = state !== 'closed' + state = 'aborting' + const failures: unknown[] = [] + if (handleNeedsClose) { + await waitForSftpCallback((callback) => operations.close(handle, callback)).catch((error) => + failures.push(error) + ) + } + await waitForSftpCallback((callback) => operations.unlink(remotePath, callback)).catch( + (error) => failures.push(error) + ) + state = 'aborted' + const failure = cleanupFailure(failures) + if (failure !== undefined) { + throw failure + } + } + + const abort = (_reason: unknown): Promise => { + abortPromise ??= performAbort() + return abortPromise + } + + return Object.freeze({ write, close, abort }) +} + +export async function openSshRelayRuntimeSftpFileDestination( + options: OpenSshRelayRuntimeSftpFileDestinationOptions +): Promise { + validateOptions(options) + options.signal.throwIfAborted() + const handle = await waitForSftpValue((callback) => + options.operations.open(options.remotePath, 'wx', { mode: options.mode }, callback) + ) + const destination = createDestination(options, handle) + try { + options.signal.throwIfAborted() + return destination + } catch (error) { + try { + await destination.abort(error) + } catch (cleanupError) { + throw new AggregateError( + [error, cleanupError], + 'SSH relay runtime SFTP file open cancellation cleanup failed' + ) + } + throw error + } +} From c03b8c988f908855d95df2b934bacc42d91a59a0 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 10:24:22 -0400 Subject: [PATCH 215/267] docs: record SSH relay SFTP destination CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 9 ++- ...github-release-implementation-checklist.md | 58 ++++++++++++++++++- 2 files changed, 63 insertions(+), 4 deletions(-) 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 e7240664173..7128da8e995 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 @@ -10,8 +10,8 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected SFTP -per-file destination is locally green; exact-head native proof is next, 2026-07-15, Codex -implementation owner.** `E-M6-SFTP-FILE-DESTINATION-AUDIT-001` fixes exclusive `wx` open, callback-bounded +per-file destination is exact-head green; SFTP session/tree orchestration audit is next, +2026-07-15, Codex implementation owner.** `E-M6-SFTP-FILE-DESTINATION-AUDIT-001` fixes exclusive `wx` open, callback-bounded positional writes, explicit POSIX mode repair/handle-stat proof, and joined close-before-unlink cleanup. It deliberately borrows but does not acquire/end an SFTP session and does not create directories, clean a whole staging tree, or publish/launch anything. Session-wide cancellation, @@ -24,6 +24,11 @@ with one declared skip, 579 broad relay cases with five declared skips, 280/280 typecheck, full lint, targeted format, max-lines, diff, protected-resolver, and no-product-import gates. It proves the exact per-file lifecycle only; SFTP session/tree orchestration, live/full-size remote proof, and every product/default path remain open. +`E-M6-SFTP-FILE-DESTINATION-CI-001` closes exact commit `72d482201`: all six primary native clients +pass all 13 cases and complete full runtime proof; both Linux supplements, Windows x64 floor, PR +Checks, Golden E2E, and computer-use pass. Windows arm64 retains only the declared hosted build-26200 +rejection against 26100 after complete runtime smoke. The suite uses callback mocks and does not +credit a live SFTP/server/remote cell. `E-M6-SOURCE-STREAM-CI-001` closes exact commit `df39c287d`: all six primary native clients pass the 27-case source-stream suite (Windows has only the declared POSIX-mode skip), exact 85–125 MB trees stream in 103.663–530.937 ms with 0–5,492,736 incremental RSS bytes, both Linux supplements and the 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 7b27094bb36..35ceec21c19 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected SFTP per-file destination is locally green; exact-head native proof is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **SFTP PER-FILE DESTINATION LOCAL GREEN — 2026-07-15, Codex implementation owner** — `E-M6-SFTP-FILE-DESTINATION-LOCAL-001` passes the hardened 13-case purpose contract, focused/broad/workflow tests, and all static/isolation gates. It proves exclusive remote open, exact callback-bounded offsets, in-flight cancellation joining, explicit POSIX mode repair/handle-stat proof, Windows mode skip, close-before-unlink cleanup, and idempotent aggregation. Commit/push this disconnected package and require exact-head all-six native/adjacent CI before advancing to SFTP session/tree orchestration. The adapter borrows but does not acquire/end an SFTP session and does not create directories or publish/launch a tree; session-wide cancellation and whole-staging cleanup remain later gates. Do not add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected SFTP per-file destination is exact-head green; SFTP session/tree orchestration audit is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **SFTP PER-FILE DESTINATION EXACT-HEAD GREEN — 2026-07-15, Codex implementation owner** — `E-M6-SFTP-FILE-DESTINATION-CI-001` closes all-six native and adjacent proof for exact implementation commit `72d482201`. Every client passes all 13 cases; exact full-size client artifact metrics and baselines remain within their gates, with only the declared Windows arm64 hosted build-26200 versus required-26100 floor rejection. Audit the smallest disconnected SFTP session/tree orchestrator next; do not credit live SFTP, session cancellation, directory/tree cleanup, remote transfer, or mode behavior from this callback-mock package. Do not add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -16823,6 +16823,60 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 7/7 in 356 ms. The adapter. Product/Beta/fallback/tuple/publication/default behavior and SignPath remain absent; legacy remains the sole production path. +### E-M6-SFTP-FILE-DESTINATION-CI-001 — all-six native clients accept the per-file SFTP lifecycle + +- Date/owner/head: 2026-07-15, Codex implementation owner; exact implementation commit + `72d482201e99975bb0133d1934906a95d2033934` on draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). +- Native workflow: SSH Relay Runtime Artifacts run + [29505153131](https://github.com/stablyai/orca/actions/runs/29505153131). All six primary Node + 24.18.0 jobs pass contract, deterministic-build/smoke, full-size extraction/cache/scan/stream, and + evidence upload: Linux x64 + [87643900797](https://github.com/stablyai/orca/actions/runs/29505153131/job/87643900797), Darwin + arm64 [87643900822](https://github.com/stablyai/orca/actions/runs/29505153131/job/87643900822), + Linux arm64 + [87643900825](https://github.com/stablyai/orca/actions/runs/29505153131/job/87643900825), Windows + x64 [87643900844](https://github.com/stablyai/orca/actions/runs/29505153131/job/87643900844), + Windows arm64 + [87643900854](https://github.com/stablyai/orca/actions/runs/29505153131/job/87643900854), and Darwin + x64 [87643900952](https://github.com/stablyai/orca/actions/runs/29505153131/job/87643900952). +- Purpose/native totals: every client passes all 13 SFTP destination cases in 19–30 ms. Overall + contract totals are 747 passed / one skipped on each Linux client, 746 passed / two skipped on + each Darwin client, and 734 passed / 18 skipped on each Windows client. The skips are all prior + declared platform cases; the new suite has none. +- Regression-sensitive exact full-size metrics remain green even though this disconnected adapter + is not yet the measurement destination: Linux x64 34 files / 124,846,430 bytes, stream + 180.352 ms / 524,288 incremental RSS bytes; Linux arm64 34 / 122,865,324, 142.092 ms / 262,144; + Darwin x64 35 / 124,316,655, 566.028 ms / 307,200; Darwin arm64 35 / 122,027,869, + 130.193 ms / 557,056; Windows x64 42 / 96,527,161, 133.884 ms / 5,402,624; Windows arm64 + 42 / 85,213,511, 377.815 ms / 1,146,880. Every existing stream/cold/warm/scan/retention/eviction + budget passes. +- Baselines: Linux arm64 supplement + [87644986864](https://github.com/stablyai/orca/actions/runs/29505153131/job/87644986864), Linux x64 + supplement + [87644987167](https://github.com/stablyai/orca/actions/runs/29505153131/job/87644987167), and Windows + x64 floor [87646436484](https://github.com/stablyai/orca/actions/runs/29505153131/job/87646436484) + pass. Windows arm64 floor + [87646436470](https://github.com/stablyai/orca/actions/runs/29505153131/job/87646436470) retains + only the declared hosted-image rejection: build 26200 observed versus 26100 required. Before that + rejection it verifies the complete runtime tree and Node/PTY/watcher/resource smoke in + 6,000.5466 ms at 49,496,064 RSS; complete verification is 8,121.8794 ms. +- Adjacent exact-head workflows pass: PR Checks run + [29505152898](https://github.com/stablyai/orca/actions/runs/29505152898), verify job + [87643863526](https://github.com/stablyai/orca/actions/runs/29505152898/job/87643863526); Golden E2E + run [29505152891](https://github.com/stablyai/orca/actions/runs/29505152891), macOS job + [87643863170](https://github.com/stablyai/orca/actions/runs/29505152891/job/87643863170) and Linux job + [87643863283](https://github.com/stablyai/orca/actions/runs/29505152891/job/87643863283); computer-use + run [29505152766](https://github.com/stablyai/orca/actions/runs/29505152766), Ubuntu job + [87643862898](https://github.com/stablyai/orca/actions/runs/29505152766/job/87643862898) and Windows + job [87643862909](https://github.com/stablyai/orca/actions/runs/29505152766/job/87643862909). +- Boundary/residual gaps: the purpose suite uses callback mocks and creates no SSH/SFTP channel or + remote file. SFTP session acquisition/end/error ownership, remote path/directory policy, bounded + tree concurrency, session-wide timeout/cancellation, whole-staging cleanup, live server/full-size + transfer, and POSIX/Windows server-mode behavior remain unproved. The adapter still has no non- + test importer. Remote verification/install/launch, product/Beta/fallback/tuple/publication/default + behavior, and SignPath remain absent; legacy remains the sole production path. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 1388e8071fe8ac91e1e6126af19cb870c71b753c Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 10:38:09 -0400 Subject: [PATCH 216/267] feat: orchestrate bounded SSH relay SFTP trees Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 11 +- ...github-release-implementation-checklist.md | 110 ++++- ...h-relay-runtime-sftp-tree-transfer.test.ts | 375 ++++++++++++++++++ .../ssh-relay-runtime-sftp-tree-transfer.ts | 250 ++++++++++++ 6 files changed, 745 insertions(+), 4 deletions(-) create mode 100644 src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts create mode 100644 src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 7c18c15a716..3180016e962 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -286,6 +286,7 @@ jobs: src/main/ssh/ssh-relay-runtime-source-scan.test.ts \ src/main/ssh/ssh-relay-runtime-source-stream.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts \ + src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts \ src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts \ src/main/ssh/ssh-relay-official-manifest.test.ts \ src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts \ @@ -842,6 +843,7 @@ jobs: src/main/ssh/ssh-relay-runtime-source-scan.test.ts ` src/main/ssh/ssh-relay-runtime-source-stream.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts ` + src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts ` src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts ` src/main/ssh/ssh-relay-official-manifest.test.ts ` src/main/ssh/ssh-relay-manifest-accepted-keys.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 53ff1bf7bfb..65ab35c6183 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -144,6 +144,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-runtime-source-scan.test.ts', 'ssh-relay-runtime-source-stream.test.ts', 'ssh-relay-runtime-sftp-file-destination.test.ts', + 'ssh-relay-runtime-sftp-tree-transfer.test.ts', 'ssh-relay-compiled-manifest-trust.test.ts', 'ssh-relay-official-manifest.test.ts', 'ssh-relay-manifest-accepted-keys.test.ts', 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 7128da8e995..1b34dd3b553 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 @@ -10,8 +10,15 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected SFTP -per-file destination is exact-head green; SFTP session/tree orchestration audit is next, -2026-07-15, Codex implementation owner.** `E-M6-SFTP-FILE-DESTINATION-AUDIT-001` fixes exclusive `wx` open, callback-bounded +session/tree orchestration is locally green; exact-head native proof is next, 2026-07-15, Codex +implementation owner.** `E-M6-SFTP-TREE-TRANSFER-LOCAL-001` passes 8/8 purpose cases, 111 focused +cases with one declared skip, 587 broad relay cases with five declared skips, 280/280 release +contracts, typecheck, full lint, formatting, max-lines, diff, protected-resolver, and no-product- +import gates. `E-M6-SFTP-TREE-TRANSFER-AUDIT-001` limits the package to one caller-owned signal and session +abstraction, one exclusive staging root, manifest-only directory creation, one-to-four composed file +streams, proved-owned reverse cleanup, and awaited session close. Cancellation starts session close +immediately; a later raw-`ssh2` adapter must prove that close settles retained callbacks. No live +SFTP/server or product/default claim is made. `E-M6-SFTP-FILE-DESTINATION-AUDIT-001` fixes exclusive `wx` open, callback-bounded positional writes, explicit POSIX mode repair/handle-stat proof, and joined close-before-unlink cleanup. It deliberately borrows but does not acquire/end an SFTP session and does not create directories, clean a whole staging tree, or publish/launch anything. Session-wide cancellation, 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 35ceec21c19..007bda79289 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected SFTP per-file destination is exact-head green; SFTP session/tree orchestration audit is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **SFTP PER-FILE DESTINATION EXACT-HEAD GREEN — 2026-07-15, Codex implementation owner** — `E-M6-SFTP-FILE-DESTINATION-CI-001` closes all-six native and adjacent proof for exact implementation commit `72d482201`. Every client passes all 13 cases; exact full-size client artifact metrics and baselines remain within their gates, with only the declared Windows arm64 hosted build-26200 versus required-26100 floor rejection. Audit the smallest disconnected SFTP session/tree orchestrator next; do not credit live SFTP, session cancellation, directory/tree cleanup, remote transfer, or mode behavior from this callback-mock package. Do not add a product caller, settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected SFTP session/tree orchestration is locally green; exact-head native proof is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **SFTP SESSION/TREE ORCHESTRATION LOCAL GREEN — 2026-07-15, Codex implementation owner** — `E-M6-SFTP-TREE-TRANSFER-LOCAL-001` passes purpose, focused/broad relay, release-contract, typecheck, full-lint, format, max-lines, diff, protected-resolver, and no-product-import gates. It proves one exclusive manifest staging tree, one-to-four composed file streams, reverse proved-owned cleanup, and cancellation-driven awaited session close at a mockable abstraction. Commit/push this disconnected package and require exact-head all-six native and adjacent CI before auditing the raw `ssh2` session adapter. Do not credit live SFTP/server transfer, raw-session callback settlement, full-size remote metrics, product/settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -16877,6 +16877,112 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 7/7 in 356 ms. The test importer. Remote verification/install/launch, product/Beta/fallback/tuple/publication/default behavior, and SignPath remain absent; legacy remains the sole production path. +### E-M6-SFTP-TREE-TRANSFER-AUDIT-001 — disconnected bounded SFTP staging-tree orchestration + +- Date/owner/base: 2026-07-15, Codex implementation owner; exact clean base `c03b8c988` after + `E-M6-SFTP-FILE-DESTINATION-CI-001` closure. +- Exact input boundary: accept one scanned and lease-backed source tree, one already validated + absolute remote staging-root string, the same required caller-owned `AbortSignal`, an explicit + one-to-four maximum concurrency already reduced to one by a later host-scoped `MaxSessions=1` + decision, an explicit POSIX-mode flag, and an `openSession(signal)` factory. The factory returns + only callback-shaped SFTP file/directory primitives plus an idempotent awaited `close(reason)` + operation. Its later raw-`ssh2` adapter must bound channel open and prove that close forces every + retained operation callback to settle before resolving. +- Ownership/path contract: exclusively create the staging root with mode `0o700`; a failed root + create owns nothing and must never remove a pre-existing path. Create every signed directory in + parent-before-child order with its declared `0o755` mode before opening a file. Construct remote + children only from the validated staging root and slash-delimited signed manifest segments, using + remote-path checks rather than client-native path utilities. Do not list, traverse, or delete + unknown remote content. +- Stream/lifecycle contract: compose `streamSshRelayRuntimeSourceTree` with + `openSshRelayRuntimeSftpFileDestination` and the exact shared signal. Allow one through four file + handles on one SFTP session; preserve the source stream's callback-bounded 64 KiB buffer lifetime, + exact per-file exclusive open, offsets, hash, progress, POSIX mode proof, and explicit Windows + mode skip. Return only after the exact source result and successful awaited session close. +- Failure/cancellation contract: track only directories successfully created below the exclusively + owned root and files whose exclusive open succeeded. On ordinary failure, attempt file unlink and + then directory removal in reverse order, treating only SFTP no-such-file status 2 as already + clean. Preserve the primary failure and join cleanup/session-close failures. On signal abort, + start idempotent session close immediately so a stalled open/write/mode/cleanup callback cannot + retain a source buffer forever; still await the source task and close promise. No operation may + begin after cancellation, and the package creates no hidden timeout. +- Required purpose RED/green: pre-open cancellation; bounded/cancellable session open boundary; + exclusive root collision without deletion; parent-first manifest directory modes; POSIX and + Windows remote-path construction; exact one/four file concurrency; source progress/result + passthrough; session close after success; reverse known-tree cleanup after directory/open/write/ + mode/source failures; no-such-file cleanup tolerance; joined cleanup and close failures; + cancellation that closes the session, settles a retained callback, awaits all work, and causes no + later writes; path-free orchestrator-authored diagnostics; and exact suite inclusion once in both + native workflow families before implementation. +- Explicit residual boundary: this package stops at a mockable session abstraction. A production + importer, raw `SFTPWrapper` adapter, live SSH authentication/channel/server proof, full-size remote + transfer and high-latency comparison, server mode behavior, `MaxSessions=1` capability wiring, + remote bundled-Node tree verification/install/publish/launch, product Beta/settings/fallback/ + tuple/publication/default behavior, and SignPath remain absent. Legacy remains the sole + production/default path. + +### E-M6-SFTP-TREE-TRANSFER-LOCAL-RED-001 — audited SFTP tree orchestrator is absent + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0, + pnpm 10.24.0, atop clean evidence base `c03b8c988` plus the audit/test/workflow-only diff. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts` — EXPECTED FAIL, exit 1: + one failed suite and zero collected tests because `./ssh-relay-runtime-sftp-tree-transfer` is + absent; 530 ms Vitest / 1.82 seconds wall and 133,808,128-byte maximum RSS. +- Workflow-oracle command: the same prefix with + `config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 7/7 in 302 ms Vitest / 1.26 seconds + wall and 132,939,776-byte maximum RSS. The new purpose suite occurs exactly once in both the POSIX + and Windows native commands before implementation. +- RED scope: the test scaffold covers exclusive root/directory ordering, POSIX and Windows paths, + one/four concurrency, exact result/progress, awaited close, collision ownership, reverse cleanup, + joined path-free failures, callback-retaining cancellation, and pre-open cancellation. The + protected resolver files remain untouched. No implementation, raw SFTP adapter, live SSH, + product importer, settings/fallback/tuple/publication/default behavior, or SignPath work exists. + +### E-M6-SFTP-TREE-TRANSFER-LOCAL-001 — disconnected bounded SFTP staging tree is locally green + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0, + pnpm 10.24.0, atop clean evidence base `c03b8c988` plus this isolated implementation/checklist + diff. Exact Node 24 remains the required native CI gate. +- Implementation: the 250-line `ssh-relay-runtime-sftp-tree-transfer.ts` accepts only the audited + scanned tree, validated staging root, exact signal/mode policy/concurrency, progress observer, and + mockable session factory. It creates the root exclusively at `0o700`, creates signed directories + parent-first, constructs slash-safe POSIX/Windows remote paths, composes the proven source stream + and per-file destination at one-to-four concurrency, tracks only successful exclusive opens, + reverse-cleans known owned files/directories, tolerates only status-2 already-clean responses, + joins failures, and awaits one idempotent session close. Signal abort starts close immediately so + the future raw adapter can release retained callbacks and source buffers. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts` — PASS, 8/8 in 830 ms + Vitest / 2.80 seconds wall, 139,919,360-byte maximum RSS, zero swaps. It covers POSIX and Windows + path/mode policy, exact result/progress, parent-before-file order, four-file peak concurrency, + awaited successful close, root collision without deletion, reverse cleanup, joined path-free + cleanup/close failures, cancellation-driven retained-write settlement with no later writes, and + pre-open cancellation. +- Focused command: the source tree/scan/stream, per-file destination, new tree transfer, + acquisition/cache resolution/population/integration, and workflow-oracle eleven-file command — + PASS, 111 passed / one declared platform skip in 8.01 seconds Vitest / 9.09 seconds wall, + 160,235,520-byte maximum RSS, zero swaps. +- Broad commands: `src/main/ssh/ssh-relay-*.test.ts` — PASS, 44 files passed / three declared skipped, + 587 tests passed / five declared platform/full-size skips in 25.86 seconds Vitest / 27.44 seconds + wall, 251,985,920-byte maximum RSS; `config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files + / 280 tests in 17.10 seconds Vitest / 18.66 seconds wall, 190,300,160-byte maximum RSS. The only + broad output is the established stale-lock diagnostic and local Node 26 module deprecation. +- Static/isolation commands: `pnpm typecheck` passes in 3.86 seconds wall at 1,224,179,712-byte + maximum RSS. Targeted `oxlint` and `oxfmt --check`, `pnpm lint`, standalone max-lines ratchet, + `git diff --check`, protected-resolver empty diff, and no non-test importer pass. Full lint takes + 18.28 seconds at 2,102,476,800-byte maximum RSS and passes switch exhaustiveness, 41 reliability + gates, 355-entry max-lines ratchet, bundled-skill verification, 9,837 localization references, + locale parity, and zero-candidate localization coverage; only pre-existing unrelated warnings + remain. Module/test sizes are 250/375 lines with no bypass or vague module. +- Boundary/residual gaps: the session is a callback mock whose `close()` is stipulated to settle + retained callbacks. This is not a raw `SFTPWrapper` adapter, live SSH authentication/channel/SFTP + server, full-size remote transfer, high-latency comparison, real POSIX/Windows server mode proof, + or `MaxSessions=1` wiring. Remote bundled-Node verification/install/publish/launch and every + product/Beta/settings/fallback/tuple/publication/default path remain absent; legacy remains the + sole production/default path and SignPath remains deferred. Exact-head all-six Node 24 and + adjacent CI evidence is required before the raw-session adapter audit. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts b/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts new file mode 100644 index 00000000000..4cb032ad05d --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts @@ -0,0 +1,375 @@ +import { mkdir, mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { publishSshRelayArtifactCacheEntry } from './ssh-relay-artifact-cache-entry' +import { createSshRelayArtifactCacheEntryFixture } from './ssh-relay-artifact-cache-entry-fixture' +import { + acquireSshRelayArtifactCacheInUseLease, + type SshRelayArtifactCacheInUseLease +} from './ssh-relay-artifact-cache-in-use-lease' +import { scanSshRelayRuntimeSourceTree } from './ssh-relay-runtime-source-scan' +import { + transferSshRelayRuntimeTreeViaSftp, + type SshRelayRuntimeSftpTreeSession +} from './ssh-relay-runtime-sftp-tree-transfer' +import { createSshRelayRuntimeSourceTree } from './ssh-relay-runtime-source-tree' + +type Callback = (error?: Error) => void + +const cleanupRoots = new Set() +const cleanupLeases = new Set() + +async function treeFixture(os: 'linux' | 'win32' = 'linux') { + const root = await mkdtemp(join(tmpdir(), 'orca-relay-sftp-tree-')) + cleanupRoots.add(root) + const inputRoot = join(root, 'input') + await mkdir(inputRoot) + const fixture = await createSshRelayArtifactCacheEntryFixture({ root: inputRoot, os }) + const cacheRoot = join(root, 'cache') + const entry = await publishSshRelayArtifactCacheEntry({ + cacheRoot, + artifact: fixture.artifact, + archivePath: fixture.archivePath + }) + const lease = await acquireSshRelayArtifactCacheInUseLease({ cacheRoot, entry }) + cleanupLeases.add(lease) + const source = createSshRelayRuntimeSourceTree({ + kind: 'ready', + source: 'cache', + artifact: fixture.artifact, + entry, + lease + }) + return scanSshRelayRuntimeSourceTree(source, new AbortController().signal) +} + +function sftpError(message: string, code?: number): Error { + return Object.assign(new Error(message), code === undefined ? {} : { code }) +} + +function createSession(): { + session: SshRelayRuntimeSftpTreeSession + events: string[] + files: Map + modes: Map +} { + const events: string[] = [] + const files = new Map() + const modes = new Map() + const handles = new Map() + let nextHandle = 0 + const operations = { + mkdir: vi.fn((path: string, attributes: { mode: number }, callback: Callback) => { + events.push(`mkdir:${path}:${attributes.mode.toString(8)}`) + callback() + }), + open: vi.fn( + ( + path: string, + _flags: 'wx', + attributes: { mode: number }, + callback: (error: Error | undefined, handle: Buffer) => void + ) => { + events.push(`open:${path}`) + const handle = Buffer.from(`handle-${nextHandle++}`) + handles.set(handle.toString(), path) + files.set(path, Buffer.alloc(0)) + modes.set(path, attributes.mode) + callback(undefined, handle) + } + ), + write: vi.fn( + ( + handle: Buffer, + buffer: Buffer, + offset: number, + length: number, + position: number, + callback: Callback + ) => { + const path = handles.get(handle.toString()) as string + events.push(`write:${path}`) + const current = files.get(path) ?? Buffer.alloc(0) + const next = Buffer.alloc(Math.max(current.length, position + length)) + current.copy(next) + buffer.copy(next, position, offset, offset + length) + files.set(path, next) + callback() + } + ), + fchmod: vi.fn((handle: Buffer, mode: number, callback: Callback) => { + const path = handles.get(handle.toString()) as string + events.push(`fchmod:${path}`) + modes.set(path, mode) + callback() + }), + fstat: vi.fn( + (handle: Buffer, callback: (error: Error | undefined, value: { mode: number }) => void) => { + const path = handles.get(handle.toString()) as string + callback(undefined, { mode: modes.get(path) as number }) + } + ), + close: vi.fn((handle: Buffer, callback: Callback) => { + events.push(`close-file:${handles.get(handle.toString())}`) + callback() + }), + unlink: vi.fn((path: string, callback: Callback) => { + events.push(`unlink:${path}`) + if (!files.delete(path)) { + callback(sftpError('missing', 2)) + return + } + callback() + }), + rmdir: vi.fn((path: string, callback: Callback) => { + events.push(`rmdir:${path}`) + callback() + }) + } + const session = { + operations, + close: vi.fn(async () => { + events.push('close-session') + }) + } as unknown as SshRelayRuntimeSftpTreeSession + return { session, events, files, modes } +} + +afterEach(async () => { + await Promise.all([...cleanupLeases].map((lease) => lease.release().catch(() => {}))) + cleanupLeases.clear() + await Promise.all([...cleanupRoots].map((root) => rm(root, { recursive: true, force: true }))) + cleanupRoots.clear() +}) + +describe('SSH relay runtime SFTP tree transfer', () => { + it('creates the exclusive POSIX tree, streams exact bytes, and awaits session close', async () => { + const tree = await treeFixture() + const { session, events, files, modes } = createSession() + let releaseClose: (() => void) | undefined + vi.mocked(session.close).mockImplementationOnce( + () => + new Promise((resolve) => { + releaseClose = () => { + events.push('close-session') + resolve() + } + }) + ) + const progress: unknown[] = [] + const transfer = transferSshRelayRuntimeTreeViaSftp({ + tree, + remoteStagingRoot: '/home/orca/.staging/content', + enforcePosixMode: true, + maximumConcurrency: 2, + signal: new AbortController().signal, + openSession: async () => session, + onProgress: (value) => progress.push(value) + }) + + await vi.waitFor(() => expect(session.close).toHaveBeenCalledOnce()) + expect(await Promise.race([transfer.then(() => 'settled'), Promise.resolve('pending')])).toBe( + 'pending' + ) + releaseClose?.() + const result = await transfer + + expect(result).toMatchObject({ + remoteStagingRoot: '/home/orca/.staging/content', + filesCompleted: tree.fileCount, + bytesTransferred: tree.expandedBytes + }) + expect(events[0]).toBe('mkdir:/home/orca/.staging/content:700') + expect(events.findIndex((event) => event.startsWith('open:'))).toBeGreaterThan( + events.findLastIndex((event) => event.startsWith('mkdir:')) + ) + expect(files.size).toBe(tree.fileCount) + expect( + tree.files.every( + (file) => modes.get(`/home/orca/.staging/content/${file.path}`) === file.mode + ) + ).toBe(true) + expect(progress.length).toBeGreaterThan(0) + expect(events.at(-1)).toBe('close-session') + }) + + it('uses slash-safe Windows paths and explicitly skips POSIX mode repair', async () => { + const tree = await treeFixture('win32') + const { session, events } = createSession() + + await transferSshRelayRuntimeTreeViaSftp({ + tree, + remoteStagingRoot: 'C:\\Users\\orca\\staging\\content', + enforcePosixMode: false, + signal: new AbortController().signal, + openSession: async () => session + }) + + expect(events).toContain('mkdir:C:/Users/orca/staging/content:700') + expect(events.some((event) => event.startsWith('open:C:/Users/orca/staging/content/'))).toBe( + true + ) + expect(session.operations.fchmod).not.toHaveBeenCalled() + }) + + it('never exceeds four concurrently open SFTP files', async () => { + const tree = await treeFixture() + const { session } = createSession() + let active = 0 + let peak = 0 + let releaseWrites: (() => void) | undefined + const gate = new Promise((resolve) => (releaseWrites = resolve)) + vi.mocked(session.operations.write).mockImplementation( + (_handle, _buffer, _offset, _length, _position, callback) => { + active += 1 + peak = Math.max(peak, active) + if (active === Math.min(4, tree.fileCount)) { + releaseWrites?.() + } + void gate.then(() => { + active -= 1 + callback() + }) + } + ) + + await transferSshRelayRuntimeTreeViaSftp({ + tree, + remoteStagingRoot: '/staging/content', + enforcePosixMode: true, + maximumConcurrency: 4, + signal: new AbortController().signal, + openSession: async () => session + }) + expect(peak).toBe(Math.min(4, tree.fileCount)) + }) + + it('does not remove a pre-existing root when exclusive creation fails', async () => { + const tree = await treeFixture() + const { session } = createSession() + vi.mocked(session.operations.mkdir).mockImplementationOnce((_path, _attributes, callback) => + callback(sftpError('already exists', 4)) + ) + + await expect( + transferSshRelayRuntimeTreeViaSftp({ + tree, + remoteStagingRoot: '/private/existing', + enforcePosixMode: true, + signal: new AbortController().signal, + openSession: async () => session + }) + ).rejects.toThrow('already exists') + expect(session.operations.unlink).not.toHaveBeenCalled() + expect(session.operations.rmdir).not.toHaveBeenCalled() + expect(session.close).toHaveBeenCalledOnce() + }) + + it('reverse-cleans only known owned paths after a stream failure', async () => { + const tree = await treeFixture() + const { session, events } = createSession() + vi.mocked(session.operations.write).mockImplementationOnce( + (_handle, _buffer, _offset, _length, _position, callback) => + callback(new Error('remote disk full')) + ) + + await expect( + transferSshRelayRuntimeTreeViaSftp({ + tree, + remoteStagingRoot: '/staging/private-content', + enforcePosixMode: true, + maximumConcurrency: 1, + signal: new AbortController().signal, + openSession: async () => session + }) + ).rejects.toThrow('remote disk full') + + expect(events.findLast((event) => event.startsWith('rmdir:'))).toBe( + 'rmdir:/staging/private-content' + ) + expect(events.at(-1)).toBe('close-session') + expect(JSON.stringify(events)).not.toContain('unknown') + }) + + it('joins cleanup and session-close failures without exposing remote paths', async () => { + const tree = await treeFixture() + const { session } = createSession() + vi.mocked(session.operations.write).mockImplementationOnce( + (_handle, _buffer, _offset, _length, _position, callback) => + callback(new Error('primary transfer failure')) + ) + vi.mocked(session.operations.rmdir).mockImplementation((_path, callback) => + callback(new Error('directory cleanup failure')) + ) + vi.mocked(session.close).mockRejectedValueOnce(new Error('session close failure')) + + const outcome = transferSshRelayRuntimeTreeViaSftp({ + tree, + remoteStagingRoot: '/home/secret-user/private-staging', + enforcePosixMode: true, + maximumConcurrency: 1, + signal: new AbortController().signal, + openSession: async () => session + }) + await expect(outcome).rejects.toMatchObject({ + errors: expect.arrayContaining([ + expect.objectContaining({ message: 'primary transfer failure' }), + expect.objectContaining({ message: 'directory cleanup failure' }), + expect.objectContaining({ message: 'session close failure' }) + ]) + }) + await expect(outcome).rejects.not.toThrow('/home/secret-user/private-staging') + }) + + it('closes the session to settle a retained write callback on cancellation', async () => { + const tree = await treeFixture() + const { session } = createSession() + const controller = new AbortController() + let retainedCallback: Callback | undefined + let writes = 0 + vi.mocked(session.operations.write).mockImplementationOnce( + (_handle, _buffer, _offset, _length, _position, callback) => { + writes += 1 + retainedCallback = callback + } + ) + vi.mocked(session.close).mockImplementationOnce(async () => { + retainedCallback?.(new Error('session closed')) + }) + const transfer = transferSshRelayRuntimeTreeViaSftp({ + tree, + remoteStagingRoot: '/staging/content', + enforcePosixMode: true, + maximumConcurrency: 1, + signal: controller.signal, + openSession: async () => session + }) + + await vi.waitFor(() => expect(writes).toBe(1)) + controller.abort(new Error('cancelled')) + await expect(transfer).rejects.toThrow(/cancelled|session closed/) + expect(session.close).toHaveBeenCalledOnce() + expect(writes).toBe(1) + }) + + it('rejects pre-open cancellation without acquiring a session', async () => { + const tree = await treeFixture() + const controller = new AbortController() + controller.abort(new Error('cancelled before session open')) + const openSession = vi.fn() + + await expect( + transferSshRelayRuntimeTreeViaSftp({ + tree, + remoteStagingRoot: '/staging/content', + enforcePosixMode: true, + signal: controller.signal, + openSession + }) + ).rejects.toThrow('cancelled before session open') + expect(openSession).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.ts b/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.ts new file mode 100644 index 00000000000..75714a33b38 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.ts @@ -0,0 +1,250 @@ +import { assertSafeRemotePathSegment, normalizeWindowsRemotePath } from './ssh-remote-platform' +import { + openSshRelayRuntimeSftpFileDestination, + type SshRelayRuntimeSftpFileOperations +} from './ssh-relay-runtime-sftp-file-destination' +import type { SshRelayRuntimeScannedSourceTree } from './ssh-relay-runtime-source-scan' +import { + streamSshRelayRuntimeSourceTree, + type SshRelayRuntimeSourceStreamOptions, + type SshRelayRuntimeSourceStreamResult +} from './ssh-relay-runtime-source-stream' + +type SftpCallback = (error?: Error) => void + +export type SshRelayRuntimeSftpTreeOperations = SshRelayRuntimeSftpFileOperations & + Readonly<{ + mkdir: (path: string, attributes: { mode: number }, callback: SftpCallback) => void + rmdir: (path: string, callback: SftpCallback) => void + }> + +export type SshRelayRuntimeSftpTreeSession = Readonly<{ + operations: SshRelayRuntimeSftpTreeOperations + // Why: the raw-session adapter must settle retained callbacks before resolving close. + close: (reason?: unknown) => Promise +}> + +export type SshRelayRuntimeSftpTreeTransferOptions = Readonly<{ + tree: SshRelayRuntimeScannedSourceTree + remoteStagingRoot: string + enforcePosixMode: boolean + signal: AbortSignal + maximumConcurrency?: number + openSession: (signal: AbortSignal) => Promise + onProgress?: SshRelayRuntimeSourceStreamOptions['onProgress'] +}> + +export type SshRelayRuntimeSftpTreeTransferResult = Readonly< + SshRelayRuntimeSourceStreamResult & { remoteStagingRoot: string } +> + +const MAXIMUM_CONCURRENT_FILES = 4 + +function waitForSftpCallback(register: (callback: SftpCallback) => void): Promise { + return new Promise((resolve, reject) => { + register((error) => (error ? reject(error) : resolve())) + }) +} + +function isNoSuchFile(error: unknown): boolean { + return typeof error === 'object' && error !== null && 'code' in error && error.code === 2 +} + +function normalizeStagingRoot(tree: SshRelayRuntimeScannedSourceTree, value: string): string { + if ( + typeof value !== 'string' || + value === '' || + value.includes('\0') || + value.includes('\r') || + value.includes('\n') + ) { + throw new Error('SSH relay runtime SFTP staging root is invalid') + } + const normalized = tree.os === 'win32' ? normalizeWindowsRemotePath(value) : value + const trimmed = normalized.replace(/\/+$/u, '') + if ( + trimmed === '' || + trimmed === '/' || + (tree.os === 'win32' && !(/^[A-Za-z]:\//u.test(trimmed) || trimmed.startsWith('//'))) + ) { + throw new Error('SSH relay runtime SFTP staging root is invalid') + } + return trimmed +} + +function remoteManifestPath( + tree: SshRelayRuntimeScannedSourceTree, + remoteStagingRoot: string, + manifestPath: string +): string { + const pathFlavor = tree.os === 'win32' ? 'windows' : 'posix' + const segments = manifestPath.split('/') + for (const segment of segments) { + assertSafeRemotePathSegment(segment, pathFlavor) + } + // Why: manifest paths are remote slash paths; client-native path.join would corrupt cross-OS use. + return `${remoteStagingRoot}/${segments.join('/')}` +} + +function validateOptions(options: SshRelayRuntimeSftpTreeTransferOptions): number { + const concurrency = options.maximumConcurrency ?? 1 + if ( + !options.tree || + typeof options.openSession !== 'function' || + !Number.isInteger(concurrency) || + concurrency < 1 || + concurrency > MAXIMUM_CONCURRENT_FILES + ) { + throw new Error('SSH relay runtime SFTP tree transfer input is invalid') + } + if (options.enforcePosixMode !== (options.tree.os !== 'win32')) { + throw new Error('SSH relay runtime SFTP tree mode policy is inconsistent') + } + return concurrency +} + +function validateSession(session: SshRelayRuntimeSftpTreeSession): void { + const operations = session?.operations + for (const name of [ + 'mkdir', + 'rmdir', + 'open', + 'write', + 'fchmod', + 'fstat', + 'close', + 'unlink' + ] as const) { + if (typeof operations?.[name] !== 'function') { + throw new Error('SSH relay runtime SFTP session is incomplete') + } + } + if (typeof session.close !== 'function') { + throw new Error('SSH relay runtime SFTP session is incomplete') + } +} + +async function cleanupOwnedTree( + operations: SshRelayRuntimeSftpTreeOperations, + ownedFiles: ReadonlySet, + ownedDirectories: readonly string[] +): Promise { + const failures: unknown[] = [] + for (const path of [...ownedFiles].toReversed()) { + await waitForSftpCallback((callback) => operations.unlink(path, callback)).catch((error) => { + if (!isNoSuchFile(error)) { + failures.push(error) + } + }) + } + for (const path of ownedDirectories.toReversed()) { + await waitForSftpCallback((callback) => operations.rmdir(path, callback)).catch((error) => { + if (!isNoSuchFile(error)) { + failures.push(error) + } + }) + } + return failures +} + +function joinedFailure(primary: unknown, failures: readonly unknown[]): unknown { + const unique = failures.filter( + (failure, index) => failure !== primary && failures.indexOf(failure) === index + ) + return unique.length === 0 + ? primary + : new AggregateError( + [primary, ...unique], + 'SSH relay runtime SFTP tree transfer cleanup failed' + ) +} + +export async function transferSshRelayRuntimeTreeViaSftp( + options: SshRelayRuntimeSftpTreeTransferOptions +): Promise { + const maximumConcurrency = validateOptions(options) + const { tree, signal, openSession, enforcePosixMode, onProgress } = options + const remoteStagingRoot = normalizeStagingRoot(tree, options.remoteStagingRoot) + signal.throwIfAborted() + const session = await openSession(signal) + let closePromise: Promise | undefined + const closeSession = (reason?: unknown): Promise => { + closePromise ??= Promise.resolve().then(() => session.close(reason)) + return closePromise + } + const onAbort = (): void => { + // Why: closing the session is what releases callbacks and their borrowed source buffers. + void closeSession(signal.reason).catch(() => {}) + } + signal.addEventListener('abort', onAbort, { once: true }) + if (signal.aborted) { + onAbort() + } + + const ownedDirectories: string[] = [] + const ownedFiles = new Set() + try { + validateSession(session) + signal.throwIfAborted() + await waitForSftpCallback((callback) => + session.operations.mkdir(remoteStagingRoot, { mode: 0o700 }, callback) + ) + ownedDirectories.push(remoteStagingRoot) + signal.throwIfAborted() + + const directories = [...tree.directories].sort( + (left, right) => + left.path.split('/').length - right.path.split('/').length || + (left.path < right.path ? -1 : left.path > right.path ? 1 : 0) + ) + for (const directory of directories) { + signal.throwIfAborted() + const remotePath = remoteManifestPath(tree, remoteStagingRoot, directory.path) + await waitForSftpCallback((callback) => + session.operations.mkdir(remotePath, { mode: directory.mode }, callback) + ) + ownedDirectories.push(remotePath) + signal.throwIfAborted() + } + + const result = await streamSshRelayRuntimeSourceTree({ + tree, + signal, + maximumConcurrency, + onProgress, + openDestination: async (file, exactSignal) => { + const remotePath = remoteManifestPath(tree, remoteStagingRoot, file.path) + const trackedOperations: SshRelayRuntimeSftpFileOperations = { + ...session.operations, + open: (path, flags, attributes, callback) => + session.operations.open(path, flags, attributes, (error, handle) => { + if (!error) { + ownedFiles.add(path) + } + callback(error, handle) + }) + } + return openSshRelayRuntimeSftpFileDestination({ + operations: trackedOperations, + remotePath, + mode: file.mode, + enforcePosixMode, + signal: exactSignal + }) + } + }) + signal.throwIfAborted() + await closeSession() + signal.throwIfAborted() + return Object.freeze({ remoteStagingRoot, ...result }) + } catch (error) { + const cleanupFailures = + ownedDirectories.length === 0 + ? [] + : await cleanupOwnedTree(session.operations, ownedFiles, ownedDirectories) + await closeSession(error).catch((closeError) => cleanupFailures.push(closeError)) + throw joinedFailure(error, cleanupFailures) + } finally { + signal.removeEventListener('abort', onAbort) + } +} From 28e7a6e5f92be98514dbd3a62bc9d5aae9331888 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 10:42:23 -0400 Subject: [PATCH 217/267] fix: require absolute SFTP staging roots Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 4 ++-- ...github-release-implementation-checklist.md | 21 +++++++++++-------- ...h-relay-runtime-sftp-tree-transfer.test.ts | 16 ++++++++++++++ .../ssh-relay-runtime-sftp-tree-transfer.ts | 1 + 4 files changed, 31 insertions(+), 11 deletions(-) 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 1b34dd3b553..c8da45ab7dc 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 @@ -11,8 +11,8 @@ complete a box. Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected SFTP session/tree orchestration is locally green; exact-head native proof is next, 2026-07-15, Codex -implementation owner.** `E-M6-SFTP-TREE-TRANSFER-LOCAL-001` passes 8/8 purpose cases, 111 focused -cases with one declared skip, 587 broad relay cases with five declared skips, 280/280 release +implementation owner.** `E-M6-SFTP-TREE-TRANSFER-LOCAL-001` passes 9/9 purpose cases after absolute- +root hardening, 112 focused cases with one declared skip, 588 broad relay cases with five declared skips, 280/280 release contracts, typecheck, full lint, formatting, max-lines, diff, protected-resolver, and no-product- import gates. `E-M6-SFTP-TREE-TRANSFER-AUDIT-001` limits the package to one caller-owned signal and session abstraction, one exclusive staging root, manifest-only directory creation, one-to-four composed file 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 007bda79289..81be6af6c24 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 @@ -16946,26 +16946,29 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 7/7 in 356 ms. The diff. Exact Node 24 remains the required native CI gate. - Implementation: the 250-line `ssh-relay-runtime-sftp-tree-transfer.ts` accepts only the audited scanned tree, validated staging root, exact signal/mode policy/concurrency, progress observer, and - mockable session factory. It creates the root exclusively at `0o700`, creates signed directories + mockable session factory. It rejects non-absolute roots before session acquisition, creates the + root exclusively at `0o700`, creates signed directories parent-first, constructs slash-safe POSIX/Windows remote paths, composes the proven source stream and per-file destination at one-to-four concurrency, tracks only successful exclusive opens, reverse-cleans known owned files/directories, tolerates only status-2 already-clean responses, joins failures, and awaits one idempotent session close. Signal abort starts close immediately so the future raw adapter can release retained callbacks and source buffers. - Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts ---maxWorkers=1 src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts` — PASS, 8/8 in 830 ms - Vitest / 2.80 seconds wall, 139,919,360-byte maximum RSS, zero swaps. It covers POSIX and Windows +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts` — PASS after absolute-root + hardening, 9/9 in 1.97 seconds tests / 2.81 seconds Vitest / 5.05 seconds wall, 139,231,232-byte + maximum RSS, zero swaps. It covers POSIX and Windows path/mode policy, exact result/progress, parent-before-file order, four-file peak concurrency, awaited successful close, root collision without deletion, reverse cleanup, joined path-free cleanup/close failures, cancellation-driven retained-write settlement with no later writes, and - pre-open cancellation. + pre-open cancellation, and relative-root rejection before session acquisition. - Focused command: the source tree/scan/stream, per-file destination, new tree transfer, acquisition/cache resolution/population/integration, and workflow-oracle eleven-file command — - PASS, 111 passed / one declared platform skip in 8.01 seconds Vitest / 9.09 seconds wall, - 160,235,520-byte maximum RSS, zero swaps. + PASS after hardening, 112 passed / one declared platform skip in 9.97 seconds Vitest. The earlier + timed 111-case run was 8.01 seconds Vitest / 9.09 seconds wall at 160,235,520-byte maximum RSS. - Broad commands: `src/main/ssh/ssh-relay-*.test.ts` — PASS, 44 files passed / three declared skipped, - 587 tests passed / five declared platform/full-size skips in 25.86 seconds Vitest / 27.44 seconds - wall, 251,985,920-byte maximum RSS; `config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files + 588 tests passed / five declared platform/full-size skips in 25.57 seconds Vitest after hardening; + the earlier timed 587-case run was 25.86 seconds Vitest / 27.44 seconds wall at 251,985,920-byte + maximum RSS. `config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / 280 tests in 17.10 seconds Vitest / 18.66 seconds wall, 190,300,160-byte maximum RSS. The only broad output is the established stale-lock diagnostic and local Node 26 module deprecation. - Static/isolation commands: `pnpm typecheck` passes in 3.86 seconds wall at 1,224,179,712-byte @@ -16974,7 +16977,7 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 7/7 in 356 ms. The 18.28 seconds at 2,102,476,800-byte maximum RSS and passes switch exhaustiveness, 41 reliability gates, 355-entry max-lines ratchet, bundled-skill verification, 9,837 localization references, locale parity, and zero-candidate localization coverage; only pre-existing unrelated warnings - remain. Module/test sizes are 250/375 lines with no bypass or vague module. + remain. Module/test sizes after hardening are 251/391 lines with no bypass or vague module. - Boundary/residual gaps: the session is a callback mock whose `close()` is stipulated to settle retained callbacks. This is not a raw `SFTPWrapper` adapter, live SSH authentication/channel/SFTP server, full-size remote transfer, high-latency comparison, real POSIX/Windows server mode proof, diff --git a/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts b/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts index 4cb032ad05d..5b86ff8f1bd 100644 --- a/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts +++ b/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts @@ -372,4 +372,20 @@ describe('SSH relay runtime SFTP tree transfer', () => { ).rejects.toThrow('cancelled before session open') expect(openSession).not.toHaveBeenCalled() }) + + it('rejects a relative staging root before acquiring a session', async () => { + const tree = await treeFixture() + const openSession = vi.fn() + + await expect( + transferSshRelayRuntimeTreeViaSftp({ + tree, + remoteStagingRoot: 'relative/staging/content', + enforcePosixMode: true, + signal: new AbortController().signal, + openSession + }) + ).rejects.toThrow(/staging root/i) + expect(openSession).not.toHaveBeenCalled() + }) }) diff --git a/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.ts b/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.ts index 75714a33b38..cf9523827e2 100644 --- a/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.ts +++ b/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.ts @@ -65,6 +65,7 @@ function normalizeStagingRoot(tree: SshRelayRuntimeScannedSourceTree, value: str if ( trimmed === '' || trimmed === '/' || + (tree.os !== 'win32' && !trimmed.startsWith('/')) || (tree.os === 'win32' && !(/^[A-Za-z]:\//u.test(trimmed) || trimmed.startsWith('//'))) ) { throw new Error('SSH relay runtime SFTP staging root is invalid') From d82a8abb1c13197a9fa475f4c8a29b0292bdebd9 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 11:01:28 -0400 Subject: [PATCH 218/267] docs: record SSH relay SFTP tree CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 8 ++- ...github-release-implementation-checklist.md | 62 ++++++++++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) 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 c8da45ab7dc..749199b4703 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 @@ -10,8 +10,12 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected SFTP -session/tree orchestration is locally green; exact-head native proof is next, 2026-07-15, Codex -implementation owner.** `E-M6-SFTP-TREE-TRANSFER-LOCAL-001` passes 9/9 purpose cases after absolute- +session/tree orchestration is exact-head green; raw `ssh2` session-adapter audit is next, 2026-07-15, +Codex implementation owner.** `E-M6-SFTP-TREE-TRANSFER-CI-001` closes all six primary native jobs, +both Linux supplements, Windows x64 floor, PR Checks, Golden E2E, and computer-use on exact commit +`28e7a6e5f`; Windows arm64 retains only the declared hosted build-26200 versus required-26100 +rejection after complete runtime smoke. No live SFTP/server or product/default claim is made. +`E-M6-SFTP-TREE-TRANSFER-LOCAL-001` passes 9/9 purpose cases after absolute- root hardening, 112 focused cases with one declared skip, 588 broad relay cases with five declared skips, 280/280 release contracts, typecheck, full lint, formatting, max-lines, diff, protected-resolver, and no-product- import gates. `E-M6-SFTP-TREE-TRANSFER-AUDIT-001` limits the package to one caller-owned signal and session 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 81be6af6c24..8778b096c4a 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected SFTP session/tree orchestration is locally green; exact-head native proof is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **SFTP SESSION/TREE ORCHESTRATION LOCAL GREEN — 2026-07-15, Codex implementation owner** — `E-M6-SFTP-TREE-TRANSFER-LOCAL-001` passes purpose, focused/broad relay, release-contract, typecheck, full-lint, format, max-lines, diff, protected-resolver, and no-product-import gates. It proves one exclusive manifest staging tree, one-to-four composed file streams, reverse proved-owned cleanup, and cancellation-driven awaited session close at a mockable abstraction. Commit/push this disconnected package and require exact-head all-six native and adjacent CI before auditing the raw `ssh2` session adapter. Do not credit live SFTP/server transfer, raw-session callback settlement, full-size remote metrics, product/settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected SFTP session/tree orchestration is exact-head green; raw `ssh2` session-adapter audit is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **SFTP SESSION/TREE ORCHESTRATION EXACT-HEAD GREEN — 2026-07-15, Codex implementation owner** — `E-M6-SFTP-TREE-TRANSFER-CI-001` closes all-six primary native, both Linux supplement, Windows x64 floor, PR Checks, Golden E2E, and computer-use proof for exact implementation commit `28e7a6e5f`; Windows arm64 retains only the declared hosted build-26200 versus required-26100 floor rejection after complete runtime smoke. Audit the smallest disconnected raw `ssh2` session adapter next. Do not credit a live SFTP server/remote cell, full-size network transfer, high-latency comparison, product/settings/fallback wiring, tuple enablement, publication, or default behavior from the callback abstraction. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -16986,6 +16986,64 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 7/7 in 356 ms. The sole production/default path and SignPath remains deferred. Exact-head all-six Node 24 and adjacent CI evidence is required before the raw-session adapter audit. +### E-M6-SFTP-TREE-TRANSFER-CI-001 — all-six clients accept disconnected SFTP tree orchestration + +- Date/owner/head: 2026-07-15, Codex implementation owner; exact hardened implementation commit + `28e7a6e5f92be98514dbd3a62bc9d5aae9331888` on draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). +- Native workflow: SSH Relay Runtime Artifacts run + [29507821804](https://github.com/stablyai/orca/actions/runs/29507821804). All six primary Node + 24.18.0 jobs pass the nine-case tree suite, complete contract set, deterministic build/smoke, + full-size extraction/cache/scan/stream, and evidence upload: Windows arm64 + [87653187734](https://github.com/stablyai/orca/actions/runs/29507821804/job/87653187734), Darwin arm64 + [87653187777](https://github.com/stablyai/orca/actions/runs/29507821804/job/87653187777), Linux arm64 + [87653187857](https://github.com/stablyai/orca/actions/runs/29507821804/job/87653187857), Windows x64 + [87653187877](https://github.com/stablyai/orca/actions/runs/29507821804/job/87653187877), Darwin x64 + [87653187906](https://github.com/stablyai/orca/actions/runs/29507821804/job/87653187906), and Linux x64 + [87653187933](https://github.com/stablyai/orca/actions/runs/29507821804/job/87653187933). +- Purpose/native totals: every client passes all nine new cases with no suite skip: Linux x64 + 1,196 ms, Linux arm64 2,155 ms, Darwin x64 3,646 ms, Darwin arm64 1,186 ms, Windows x64 + 2,799 ms, and Windows arm64 4,981 ms. Overall contract totals are 756 passed / one declared skip on + each Linux client, 755 passed / two declared skips on each Darwin client, and 743 passed / 18 + declared skips on each Windows client. +- Exact full-size client baselines remain green: Linux x64 34 files / 124,846,430 bytes, stream + 183.762342 ms / 786,432 incremental RSS bytes; Linux arm64 34 / 122,865,324, + 142.800937 ms / 131,072; Darwin x64 35 / 124,316,655, 548.450994 ms / 282,624; Darwin arm64 + 35 / 122,027,869, 115.096708 ms / 688,128; Windows x64 42 / 96,527,161, 182.7795 ms / + 5,341,184; Windows arm64 42 / 85,213,511, 360.4851 ms / 1,482,752. Every existing cold/warm/ + scan/stream/retention/eviction budget passes. These are client-source regression baselines, not + live SFTP network measurements. +- Baselines: Linux arm64 supplement + [87654529646](https://github.com/stablyai/orca/actions/runs/29507821804/job/87654529646), Linux x64 + supplement + [87654529663](https://github.com/stablyai/orca/actions/runs/29507821804/job/87654529663), and Windows + x64 floor [87655923525](https://github.com/stablyai/orca/actions/runs/29507821804/job/87655923525) + pass. Windows x64 verifies 42 files / 96,527,161 bytes, Node/PTY/watcher/two-second settlement in + 5,400.4459 ms at 50,401,280 RSS, and completes verification in 6,610.5719 ms on build 20348. + Windows arm64 floor + [87655923544](https://github.com/stablyai/orca/actions/runs/29507821804/job/87655923544) retains only + the declared hosted-image rejection: build 26200 observed versus 26100 required. Before that + rejection it verifies 42 files / 85,213,511 bytes and Node/PTY/watcher/two-second settlement in + 6,860.7918 ms at 48,672,768 RSS; complete verification is 9,414.1715 ms. +- Adjacent exact-head workflows pass: PR Checks run + [29507822115](https://github.com/stablyai/orca/actions/runs/29507822115), verify job + [87653099635](https://github.com/stablyai/orca/actions/runs/29507822115/job/87653099635); Golden E2E + run [29507828242](https://github.com/stablyai/orca/actions/runs/29507828242), macOS job + [87653119889](https://github.com/stablyai/orca/actions/runs/29507828242/job/87653119889) and Linux + job [87653119935](https://github.com/stablyai/orca/actions/runs/29507828242/job/87653119935); + computer-use run [29507829190](https://github.com/stablyai/orca/actions/runs/29507829190), Ubuntu + native-smoke job + [87653123437](https://github.com/stablyai/orca/actions/runs/29507829190/job/87653123437) and Windows + native-smoke job + [87653123462](https://github.com/stablyai/orca/actions/runs/29507829190/job/87653123462). +- Boundary/residual gaps: native clients run callback mocks, not a raw `SFTPWrapper` or server. They + do not prove SFTP channel open/end/error behavior, that raw session termination settles retained + callbacks, real remote directory/mode semantics, full-size network memory/cancellation, high-RTT + one-versus-four behavior, `MaxSessions=1` wiring, or a live remote matrix cell. The module still + has no non-test importer. Remote bundled-Node verification/install/publish/launch and every + product/Beta/settings/fallback/tuple/publication/default path remain absent; legacy remains the + sole production/default path and SignPath remains deferred. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From fbec34cf128c3800738d48264ab8b471148b7d57 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 11:14:50 -0400 Subject: [PATCH 219/267] feat: add bounded SSH relay SFTP sessions Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 13 +- ...github-release-implementation-checklist.md | 110 +++++++- src/main/ssh/ssh-connection.test.ts | 30 ++ src/main/ssh/ssh-connection.ts | 17 +- .../ssh-relay-runtime-sftp-session.test.ts | 263 ++++++++++++++++++ .../ssh/ssh-relay-runtime-sftp-session.ts | 208 ++++++++++++++ 8 files changed, 632 insertions(+), 12 deletions(-) create mode 100644 src/main/ssh/ssh-relay-runtime-sftp-session.test.ts create mode 100644 src/main/ssh/ssh-relay-runtime-sftp-session.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 3180016e962..60aaea0a288 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -286,6 +286,7 @@ jobs: src/main/ssh/ssh-relay-runtime-source-scan.test.ts \ src/main/ssh/ssh-relay-runtime-source-stream.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts \ + src/main/ssh/ssh-relay-runtime-sftp-session.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts \ src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts \ src/main/ssh/ssh-relay-official-manifest.test.ts \ @@ -843,6 +844,7 @@ jobs: src/main/ssh/ssh-relay-runtime-source-scan.test.ts ` src/main/ssh/ssh-relay-runtime-source-stream.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts ` + src/main/ssh/ssh-relay-runtime-sftp-session.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts ` src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts ` src/main/ssh/ssh-relay-official-manifest.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 65ab35c6183..990f4ae5fa7 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -144,6 +144,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-runtime-source-scan.test.ts', 'ssh-relay-runtime-source-stream.test.ts', 'ssh-relay-runtime-sftp-file-destination.test.ts', + 'ssh-relay-runtime-sftp-session.test.ts', 'ssh-relay-runtime-sftp-tree-transfer.test.ts', 'ssh-relay-compiled-manifest-trust.test.ts', 'ssh-relay-official-manifest.test.ts', 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 749199b4703..ed2fb860795 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 @@ -9,9 +9,16 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected SFTP -session/tree orchestration is exact-head green; raw `ssh2` session-adapter audit is next, 2026-07-15, -Codex implementation owner.** `E-M6-SFTP-TREE-TRANSFER-CI-001` closes all six primary native jobs, +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected raw `ssh2` +session adapter is locally green; exact-head native proof is next, 2026-07-15, Codex implementation +owner.** `E-M6-SFTP-SESSION-ADAPTER-LOCAL-001` passes 9/9 purpose, 70/70 adapter+connection, +182 focused cases with one declared skip, 597 broad relay cases with five declared skips, 280/280 +release contracts, typecheck, full lint, formatting, max-lines, diff, protected-resolver, and +isolation gates. No live SFTP/tree or product/default claim is made. +`E-M6-SFTP-SESSION-ADAPTER-AUDIT-001` limits the package to optional-signal channel acquisition, +bound raw operations, raw-close callback settlement, and a five-second grace followed by a required +caller-owned connection-force-close hook. No tree importer or live/product claim is included. +`E-M6-SFTP-TREE-TRANSFER-CI-001` closes all six primary native jobs, both Linux supplements, Windows x64 floor, PR Checks, Golden E2E, and computer-use on exact commit `28e7a6e5f`; Windows arm64 retains only the declared hosted build-26200 versus required-26100 rejection after complete runtime smoke. No live SFTP/server or product/default claim is made. 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 8778b096c4a..9d7fee9da64 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected SFTP session/tree orchestration is exact-head green; raw `ssh2` session-adapter audit is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **SFTP SESSION/TREE ORCHESTRATION EXACT-HEAD GREEN — 2026-07-15, Codex implementation owner** — `E-M6-SFTP-TREE-TRANSFER-CI-001` closes all-six primary native, both Linux supplement, Windows x64 floor, PR Checks, Golden E2E, and computer-use proof for exact implementation commit `28e7a6e5f`; Windows arm64 retains only the declared hosted build-26200 versus required-26100 floor rejection after complete runtime smoke. Audit the smallest disconnected raw `ssh2` session adapter next. Do not credit a live SFTP server/remote cell, full-size network transfer, high-latency comparison, product/settings/fallback wiring, tuple enablement, publication, or default behavior from the callback abstraction. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected raw `ssh2` session adapter is locally green; exact-head native proof is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **RAW SFTP SESSION ADAPTER LOCAL GREEN — 2026-07-15, Codex implementation owner** — `E-M6-SFTP-SESSION-ADAPTER-LOCAL-001` passes the nine-case purpose contract, optional-signal connection lifecycle, focused/broad/release tests, typecheck, full lint, format, max-lines, diff, protected-resolver, and isolation gates. It proves bound raw operations, raw-close callback settlement, and five-second force-close escalation at a disconnected factory; existing no-argument `SshConnection.sftp()` behavior is preserved. Commit/push and require exact-head all-six native and adjacent CI before any live adapter/tree composition. Do not add a tree-transfer importer, live SFTP/server claim, product/settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -17044,6 +17044,112 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 7/7 in 356 ms. The product/Beta/settings/fallback/tuple/publication/default path remain absent; legacy remains the sole production/default path and SignPath remains deferred. +### E-M6-SFTP-SESSION-ADAPTER-AUDIT-001 — raw ssh2 channel acquisition and callback settlement + +- Date/owner/base: 2026-07-15, Codex implementation owner; exact clean evidence base `d82a8abb1` + after `E-M6-SFTP-TREE-TRANSFER-CI-001` closure. +- Dependency evidence: audited installed `ssh2` 1.17.0. `SFTP.end()` delegates to `destroy()` and + sends channel close. `onCHANNEL_CLOSE` invokes the SFTP channel's `push(null)`; SFTP `push(null)` + runs `cleanupRequests`, invoking every pending request callback with an error, before emitting + `end`; only afterward does the channel emit `close`. Therefore the public raw `close` event is the + minimum safe normal-close boundary for retained callback/source-buffer release and session-slot + settlement. Do not call or depend on private `_requests` or synthesize stream events. +- Acquisition boundary: extend `SshConnection.sftp` with an optional `AbortSignal` only, preserving + all existing no-argument behavior. Pass it through the existing session-limit retry and bounded + callback-open lifecycle so pre-open abort performs no channel request, abort during open awaits a + late channel's close up to the established five-second grace, and timeout still ends a late SFTP + channel. No authentication, connection selection, retry classification, or transport default + changes. +- Adapter input/output: accept the same required caller-owned signal, an `openRawSession(signal)` + factory, and a required `forceCloseConnection(reason)` hook. Return the exact tree-session shape + with explicitly bound raw `mkdir`, `rmdir`, `open`, `write`, `fchmod`, `fstat`, `close`, and + `unlink` callbacks. Do not spread unbound ssh2 methods, copy/source-buffer chunks, expose remote + paths in adapter-authored diagnostics, or acquire any second channel. +- Close/error contract: install a raw error listener before exposing operations. Session close is + idempotent, calls raw `end()` once, and resolves only after raw `close`, which is the audited point + after pending callback cleanup. If close is absent for five seconds, invoke the caller-owned + force-close hook and still require the raw close event before settlement. Preserve raw/end/force + failures and replace the listener with a late-error sink only after close. Reject operations that + begin after closing without invoking raw methods. +- Required purpose RED/green: exact signal forwarding; bound-operation receiver/argument/callback + fidelity; normal end-to-close ordering; a retained write callback settles before session close; + idempotent concurrent close; raw error joined after close; synchronous end failure; five-second + force-close escalation that still awaits raw close; failed force close; post-open cancellation; + pre-open cancellation through `SshConnection.sftp`; mid-open abort with late session end/close; + no later raw operations; path-free adapter diagnostics; and suite inclusion exactly once in both + native workflow families before implementation. +- Explicit residual boundary: the force-close hook remains caller-provided and has no product + implementation in this package. Live OpenSSH server behavior, real connection-force-close and + reconnect effects, full-size network transfer, high-RTT concurrency, `MaxSessions=1`, tree + importer, bundled-Node verification/install/publish/launch, and every product/Beta/settings/ + fallback/tuple/publication/default path remain absent. Legacy remains the sole production/default + path and SignPath remains deferred. + +### E-M6-SFTP-SESSION-ADAPTER-LOCAL-RED-001 — audited raw ssh2 adapter is absent + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0, + pnpm 10.24.0, atop exact evidence base `d82a8abb1` plus the audit/test/workflow-only diff. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-sftp-session.test.ts` — EXPECTED FAIL, exit 1: one + failed suite and zero collected tests because `./ssh-relay-runtime-sftp-session` is absent; 234 ms + Vitest / 1.44 seconds wall and 131,432,448-byte maximum RSS. +- Workflow-oracle command: the same prefix with + `config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 7/7 in 289 ms Vitest / 1.21 seconds + wall and 132,595,712-byte maximum RSS. The purpose suite occurs exactly once in both native + command families before implementation. +- RED scope: the adapter scaffold covers signal forwarding, bound receiver/callback fidelity, + retained callback before close, idempotent close, raw error ordering, five-second force-close + escalation/failure, post-open cancellation, late-operation rejection, and path-free diagnostics. + Two connection lifecycle cases cover pre-open and mid-open SFTP cancellation. The protected + resolver files remain untouched. No adapter implementation, tree importer, live SSH, product/ + settings/fallback/tuple/publication/default behavior, or SignPath work exists. + +### E-M6-SFTP-SESSION-ADAPTER-LOCAL-001 — disconnected raw ssh2 session lifecycle is locally green + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0, + pnpm 10.24.0, atop exact evidence base `d82a8abb1` plus this isolated implementation/checklist + diff. Exact Node 24 remains the native CI gate. +- Implementation: the 208-line `ssh-relay-runtime-sftp-session.ts` accepts only the exact signal, + raw opener, and required connection-force-close hook. It attaches error/close listeners before + exposing a deeply frozen session, binds all eight operations to the raw receiver, preserves raw + callbacks as source-buffer boundaries, rejects late operations, calls raw `end()` exactly once, + waits for audited raw close, escalates after five seconds, still requires raw close after a + successful force hook, and joins raw/end/force failures with path-free adapter diagnostics. The + optional signal added to `SshConnection.sftp` flows through its existing retry/open/late-close + lifecycle; every existing no-argument caller is unchanged. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-sftp-session.test.ts` — PASS, 9/9 in 240 ms Vitest / + 1.46 seconds wall, 132,349,952-byte maximum RSS, zero swaps. Cases cover exact signal and bound + receiver fidelity, retained callback before close, concurrent idempotent close, raw-error order, + five-second escalation that still awaits raw close, synchronous raw-end and force-hook failures, + post-open cancellation cleanup, late-operation rejection, and path-free diagnostics. +- Connection lifecycle command: the purpose suite plus `ssh-connection.test.ts` — PASS, 70/70 in + 1.19 seconds Vitest / 2.18 seconds wall, 144,179,200-byte maximum RSS. The two new connection + cases prove pre-open abort creates no SFTP request and mid-open abort ends a late session but does + not settle until its close event frees the session slot. +- Focused command: connection, source tree/scan/stream, per-file destination, raw session, tree + transfer, acquisition/cache resolution/population/integration, and workflow-oracle thirteen-file + command — PASS, 182 passed / one declared platform skip in 9.05 seconds Vitest / 10.12 seconds + wall, 167,854,080-byte maximum RSS, zero swaps. +- Broad commands: `src/main/ssh/ssh-relay-*.test.ts` — PASS, 45 files passed / three declared skipped, + 597 tests passed / five declared platform/full-size skips in 24.42 seconds Vitest / 25.82 seconds + wall, 249,495,552-byte maximum RSS; `config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files + / 280 tests in 17.40 seconds Vitest / 18.76 seconds wall, 195,608,576-byte maximum RSS. Only the + established stale-lock diagnostic and local Node 26 module deprecation appear. +- Static/isolation commands: `pnpm typecheck`, targeted `oxlint`, targeted `oxfmt --check`, full + `pnpm lint`, standalone max-lines ratchet, `git diff --check`, protected-resolver empty diff, and + no non-test importer pass. Full lint takes 19.75 seconds at 2,089,172,992-byte maximum RSS and + passes switch exhaustiveness, 41 reliability gates, 355-entry max-lines ratchet, bundled-skill + verification, 9,837 localization references, locale parity, and zero-candidate coverage; only + pre-existing warnings remain. Module/test sizes are 208/263 lines with no bypass or vague module. +- Boundary/residual gaps: the raw object and force-close hook are mocked. This does not prove live + OpenSSH/SFTP channel behavior, a real connection-force-close implementation, reconnect effects, + full-size network memory/cancellation, server filesystem/modes, high-RTT concurrency, + `MaxSessions=1`, or raw adapter/tree composition. The adapter has no non-test importer. Remote + bundled-Node verification/install/publish/launch and every product/Beta/settings/fallback/tuple/ + publication/default path remain absent; legacy remains the sole production/default path and + SignPath remains deferred. Exact-head all-six native and adjacent CI is required next. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/src/main/ssh/ssh-connection.test.ts b/src/main/ssh/ssh-connection.test.ts index 005d43c9ca8..90807615000 100644 --- a/src/main/ssh/ssh-connection.test.ts +++ b/src/main/ssh/ssh-connection.test.ts @@ -900,6 +900,36 @@ describe('SshConnection', () => { } }) + it('rejects pre-open SFTP cancellation without requesting a channel', async () => { + const conn = new SshConnection(createTarget(), createCallbacks()) + await conn.connect() + sftpBehavior = 'pending' + const controller = new AbortController() + controller.abort() + + await expect(conn.sftp(controller.signal)).rejects.toMatchObject({ name: 'AbortError' }) + expect(pendingSftpCallback).toBeNull() + }) + + it('ends and awaits a late SFTP channel after mid-open cancellation', async () => { + const conn = new SshConnection(createTarget(), createCallbacks()) + await conn.connect() + sftpBehavior = 'pending' + const controller = new AbortController() + const lateSftp = Object.assign(new EventEmitter(), { end: vi.fn() }) + const outcome = conn + .sftp(controller.signal) + .then(() => 'opened') + .catch((error: Error) => error.name) + + controller.abort() + pendingSftpCallback?.(undefined, lateSftp) + expect(await Promise.race([outcome, Promise.resolve('pending')])).toBe('pending') + expect(lateSftp.end).toHaveBeenCalledOnce() + lateSftp.emit('close') + await expect(outcome).resolves.toBe('AbortError') + }) + it('uses system SSH transport when ProxyUseFdpass is resolved by OpenSSH', async () => { vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig()) const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks()) diff --git a/src/main/ssh/ssh-connection.ts b/src/main/ssh/ssh-connection.ts index af74bdc712a..a1e55027080 100644 --- a/src/main/ssh/ssh-connection.ts +++ b/src/main/ssh/ssh-connection.ts @@ -161,7 +161,7 @@ export class SshConnection { ) } - async sftp(): Promise { + async sftp(signal?: AbortSignal): Promise { if (this.useSystemSshTransport) { throw new Error('SFTP is not available when using system SSH transport') } @@ -169,12 +169,15 @@ export class SshConnection { throw new Error('Not connected') } const client = this.client - return this.openSessionChannelWithRetry(() => - this.waitForSshCallback( - 'SSH SFTP channel timed out', - (callback) => client.sftp(callback), - (sftp) => sftp.end() - ) + return this.openSessionChannelWithRetry( + () => + this.waitForSshCallback( + 'SSH SFTP channel timed out', + (callback) => client.sftp(callback), + (sftp) => sftp.end(), + signal + ), + signal ) } diff --git a/src/main/ssh/ssh-relay-runtime-sftp-session.test.ts b/src/main/ssh/ssh-relay-runtime-sftp-session.test.ts new file mode 100644 index 00000000000..c2db8d003c4 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-sftp-session.test.ts @@ -0,0 +1,263 @@ +import { EventEmitter } from 'node:events' + +import type { SFTPWrapper } from 'ssh2' +import { describe, expect, it, vi } from 'vitest' + +import { openSshRelayRuntimeSftpTreeSession } from './ssh-relay-runtime-sftp-session' + +type Callback = (error?: Error) => void + +function createRawSession() { + const emitter = new EventEmitter() + const raw = Object.assign(emitter, { + mkdir: vi.fn(function (this: unknown, _path: string, _attributes: unknown, callback: Callback) { + callback() + }), + rmdir: vi.fn(function (this: unknown, _path: string, callback: Callback) { + callback() + }), + open: vi.fn(function ( + this: unknown, + _path: string, + _flags: string, + _attributes: unknown, + callback: (error: Error | undefined, handle: Buffer) => void + ) { + callback(undefined, Buffer.from('handle')) + }), + write: vi.fn(function ( + this: unknown, + _handle: Buffer, + _buffer: Buffer, + _offset: number, + _length: number, + _position: number, + callback: Callback + ) { + callback() + }), + fchmod: vi.fn(function (this: unknown, _handle: Buffer, _mode: number, callback: Callback) { + callback() + }), + fstat: vi.fn(function ( + this: unknown, + _handle: Buffer, + callback: (error: Error | undefined, value: { mode: number }) => void + ) { + callback(undefined, { mode: 0o100755 }) + }), + close: vi.fn(function (this: unknown, _handle: Buffer, callback: Callback) { + callback() + }), + unlink: vi.fn(function (this: unknown, _path: string, callback: Callback) { + callback() + }), + end: vi.fn() + }) as unknown as SFTPWrapper + return raw +} + +function openSession( + raw: SFTPWrapper, + options: { + signal?: AbortSignal + forceCloseConnection?: (reason: unknown) => Promise + } = {} +) { + const signal = options.signal ?? new AbortController().signal + const openRawSession = vi.fn(async (receivedSignal: AbortSignal) => { + expect(receivedSignal).toBe(signal) + return raw + }) + const forceCloseConnection = vi.fn(options.forceCloseConnection ?? (async () => {})) + return { + opened: openSshRelayRuntimeSftpTreeSession({ + signal, + openRawSession, + forceCloseConnection + }), + openRawSession, + forceCloseConnection + } +} + +describe('SSH relay runtime raw SFTP session adapter', () => { + it('forwards the exact signal and binds every operation to the raw session', async () => { + const raw = createRawSession() + const { opened, openRawSession } = openSession(raw) + const session = await opened + const callback = vi.fn() + + session.operations.mkdir('/stage', { mode: 0o700 }, callback) + session.operations.rmdir('/stage', callback) + session.operations.open('/stage/node', 'wx', { mode: 0o755 }, callback) + session.operations.write(Buffer.from('h'), Buffer.from('x'), 0, 1, 0, callback) + session.operations.fchmod(Buffer.from('h'), 0o755, callback) + session.operations.fstat(Buffer.from('h'), callback) + session.operations.close(Buffer.from('h'), callback) + session.operations.unlink('/stage/node', callback) + + expect(openRawSession).toHaveBeenCalledOnce() + for (const operation of [ + raw.mkdir, + raw.rmdir, + raw.open, + raw.write, + raw.fchmod, + raw.fstat, + raw.close, + raw.unlink + ]) { + expect(operation).toHaveBeenCalledOnce() + expect(vi.mocked(operation).mock.instances[0]).toBe(raw) + } + }) + + it('awaits raw close after retained callbacks settle', async () => { + const raw = createRawSession() + let retainedCallback: Callback | undefined + vi.mocked(raw.write).mockImplementationOnce( + (_handle, _buffer, _offset, _length, _position, callback) => { + retainedCallback = callback + } + ) + const { opened, forceCloseConnection } = openSession(raw) + const session = await opened + let writeSettled = false + session.operations.write(Buffer.from('h'), Buffer.from('x'), 0, 1, 0, () => { + writeSettled = true + }) + let closeSettled = false + const close = session.close().then(() => { + closeSettled = true + }) + + await Promise.resolve() + expect(raw.end).toHaveBeenCalledOnce() + expect(closeSettled).toBe(false) + retainedCallback?.(new Error('session closed')) + raw.emit('close') + await close + expect(writeSettled).toBe(true) + expect(forceCloseConnection).not.toHaveBeenCalled() + }) + + it('makes concurrent close idempotent', async () => { + const raw = createRawSession() + const { opened } = openSession(raw) + const session = await opened + + const first = session.close() + const second = session.close(new Error('later reason')) + expect(first).toBe(second) + raw.emit('close') + await first + expect(raw.end).toHaveBeenCalledOnce() + }) + + it('waits for close before rejecting a raw session error', async () => { + const raw = createRawSession() + const { opened } = openSession(raw) + const session = await opened + const close = session.close() + + raw.emit('error', new Error('raw session failed')) + const early = await Promise.race([ + close.then( + () => 'settled', + () => 'settled' + ), + Promise.resolve('pending') + ]) + expect(early).toBe('pending') + raw.emit('close') + await expect(close).rejects.toThrow('raw session failed') + }) + + it('forces the owning connection after the five-second close grace', async () => { + vi.useFakeTimers() + try { + const raw = createRawSession() + const forceCloseConnection = vi.fn(async () => {}) + const { opened } = openSession(raw, { forceCloseConnection }) + const session = await opened + const reason = new Error('cancelled transfer') + const close = session.close(reason) + + await vi.advanceTimersByTimeAsync(4_999) + expect(forceCloseConnection).not.toHaveBeenCalled() + await vi.advanceTimersByTimeAsync(1) + expect(await Promise.race([close.then(() => 'settled'), Promise.resolve('pending')])).toBe( + 'pending' + ) + raw.emit('close') + await close + expect(forceCloseConnection).toHaveBeenCalledWith(reason) + } finally { + vi.useRealTimers() + } + }) + + it('force-closes and reports a synchronous raw end failure', async () => { + const raw = createRawSession() + vi.mocked(raw.end).mockImplementationOnce(() => { + throw new Error('raw end failed') + }) + const forceCloseConnection = vi.fn(async () => { + raw.emit('close') + }) + const { opened } = openSession(raw, { forceCloseConnection }) + const session = await opened + + await expect(session.close()).rejects.toThrow('raw end failed') + expect(forceCloseConnection).toHaveBeenCalledOnce() + }) + + it('reports force-close failure and does not invent raw close', async () => { + vi.useFakeTimers() + try { + const raw = createRawSession() + const { opened } = openSession(raw, { + forceCloseConnection: async () => { + throw new Error('force close failed') + } + }) + const session = await opened + const close = session.close(new Error('primary')) + const rejected = expect(close).rejects.toThrow('force close failed') + + await vi.advanceTimersByTimeAsync(5_000) + await rejected + } finally { + vi.useRealTimers() + } + }) + + it('closes a raw session returned after cancellation and rejects opening', async () => { + const raw = createRawSession() + const controller = new AbortController() + const { opened } = openSession(raw, { signal: controller.signal }) + controller.abort(new Error('cancelled after raw open')) + + await Promise.resolve() + raw.emit('close') + await expect(opened).rejects.toThrow('cancelled after raw open') + expect(raw.end).toHaveBeenCalledOnce() + }) + + it('rejects late operations without invoking the raw session or exposing paths', async () => { + const raw = createRawSession() + const { opened } = openSession(raw) + const session = await opened + const close = session.close() + raw.emit('close') + await close + const callback = vi.fn() + const secret = '/home/private-user/stage/node' + + session.operations.unlink(secret, callback) + expect(raw.unlink).not.toHaveBeenCalled() + expect(callback).toHaveBeenCalledWith(expect.any(Error)) + expect(callback.mock.calls[0]?.[0]?.message).not.toContain(secret) + }) +}) diff --git a/src/main/ssh/ssh-relay-runtime-sftp-session.ts b/src/main/ssh/ssh-relay-runtime-sftp-session.ts new file mode 100644 index 00000000000..1d245fcabe5 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-sftp-session.ts @@ -0,0 +1,208 @@ +import type { SFTPWrapper } from 'ssh2' + +import type { + SshRelayRuntimeSftpTreeOperations, + SshRelayRuntimeSftpTreeSession +} from './ssh-relay-runtime-sftp-tree-transfer' + +const SESSION_CLOSE_GRACE_MS = 5_000 + +export type OpenSshRelayRuntimeSftpTreeSessionOptions = Readonly<{ + signal: AbortSignal + openRawSession: (signal: AbortSignal) => Promise + // Why: a peer that ignores channel close must not retain source buffers indefinitely. + forceCloseConnection: (reason: unknown) => Promise +}> + +type SessionState = 'open' | 'closing' | 'closed' + +function closedOperationError(): Error { + return new Error('SSH relay runtime SFTP session is closing or closed') +} + +function uniqueFailures(failures: readonly unknown[]): unknown[] { + return failures.filter((failure, index) => failures.indexOf(failure) === index) +} + +function cleanupFailure(failures: readonly unknown[]): unknown | undefined { + const unique = uniqueFailures(failures) + if (unique.length === 0) { + return undefined + } + return unique.length === 1 + ? unique[0] + : new AggregateError(unique, 'SSH relay runtime SFTP session close failed') +} + +function createBoundOperations( + raw: SFTPWrapper, + getState: () => SessionState +): SshRelayRuntimeSftpTreeOperations { + const isClosed = (): boolean => getState() !== 'open' + return Object.freeze({ + mkdir: (path, attributes, callback) => { + if (isClosed()) { + callback(closedOperationError()) + return + } + raw.mkdir(path, attributes, (error) => callback(error ?? undefined)) + }, + rmdir: (path, callback) => { + if (isClosed()) { + callback(closedOperationError()) + return + } + raw.rmdir(path, (error) => callback(error ?? undefined)) + }, + open: (path, flags, attributes, callback) => { + if (isClosed()) { + callback(closedOperationError(), Buffer.alloc(0)) + return + } + raw.open(path, flags, attributes, (error, handle) => callback(error ?? undefined, handle)) + }, + write: (handle, buffer, offset, length, position, callback) => { + if (isClosed()) { + callback(closedOperationError()) + return + } + // Why: only the raw callback releases the source stream's borrowed chunk view. + raw.write(handle, buffer, offset, length, position, (error) => callback(error ?? undefined)) + }, + fchmod: (handle, mode, callback) => { + if (isClosed()) { + callback(closedOperationError()) + return + } + raw.fchmod(handle, mode, (error) => callback(error ?? undefined)) + }, + fstat: (handle, callback) => { + if (isClosed()) { + callback(closedOperationError(), { mode: 0 } as never) + return + } + raw.fstat(handle, (error, attributes) => callback(error ?? undefined, attributes)) + }, + close: (handle, callback) => { + if (isClosed()) { + callback(closedOperationError()) + return + } + raw.close(handle, (error) => callback(error ?? undefined)) + }, + unlink: (path, callback) => { + if (isClosed()) { + callback(closedOperationError()) + return + } + raw.unlink(path, (error) => callback(error ?? undefined)) + } + }) +} + +function createSession( + raw: SFTPWrapper, + forceCloseConnection: (reason: unknown) => Promise +): SshRelayRuntimeSftpTreeSession { + let state: SessionState = 'open' + let rawError: unknown + let closePromise: Promise | undefined + let resolveRawClose: (() => void) | undefined + const rawClose = new Promise((resolve) => { + resolveRawClose = resolve + }) + const onRawClose = (): void => resolveRawClose?.() + const onRawError = (error: unknown): void => { + rawError ??= error + } + const swallowLateRawError = (): void => {} + raw.once('close', onRawClose) + raw.on('error', onRawError) + + const finishListeners = (): void => { + raw.removeListener('error', onRawError) + // Why: ssh2 can emit a final socket error after channel close; no owner remains to observe it. + raw.on('error', swallowLateRawError) + } + + const waitForCloseGrace = async (): Promise => { + let timer: ReturnType | undefined + const grace = new Promise((resolve) => { + timer = setTimeout(() => resolve(false), SESSION_CLOSE_GRACE_MS) + }) + const closed = await Promise.race([rawClose.then(() => true as const), grace]) + clearTimeout(timer) + return closed + } + + const performClose = async (reason?: unknown): Promise => { + state = 'closing' + const failures: unknown[] = [] + let ended = false + try { + raw.end() + ended = true + } catch (error) { + failures.push(error) + } + + if (!ended || !(await waitForCloseGrace())) { + const forceReason = reason ?? failures[0] ?? rawError ?? closedOperationError() + try { + await forceCloseConnection(forceReason) + } catch (error) { + failures.push(error) + state = 'closed' + finishListeners() + throw cleanupFailure(failures) + } + // Why: force-close completion alone is insufficient; raw close proves request callbacks ran. + await rawClose + } + state = 'closed' + finishListeners() + if (rawError !== undefined) { + failures.push(rawError) + } + const failure = cleanupFailure(failures) + if (failure !== undefined) { + throw failure + } + } + + const close = (reason?: unknown): Promise => { + closePromise ??= performClose(reason) + return closePromise + } + const operations = createBoundOperations(raw, () => state) + return Object.freeze({ operations, close }) +} + +export async function openSshRelayRuntimeSftpTreeSession( + options: OpenSshRelayRuntimeSftpTreeSessionOptions +): Promise { + if ( + !options || + typeof options.openRawSession !== 'function' || + typeof options.forceCloseConnection !== 'function' + ) { + throw new Error('SSH relay runtime SFTP session adapter input is invalid') + } + options.signal.throwIfAborted() + const raw = await options.openRawSession(options.signal) + const session = createSession(raw, options.forceCloseConnection) + try { + options.signal.throwIfAborted() + return session + } catch (error) { + try { + await session.close(error) + } catch (cleanupError) { + throw new AggregateError( + [error, cleanupError], + 'SSH relay runtime SFTP session open cancellation cleanup failed' + ) + } + throw error + } +} From d11261706729a44c55937bedb1cf6a6764a97969 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 11:34:13 -0400 Subject: [PATCH 220/267] docs: record SSH relay SFTP session CI Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 8 ++- ...github-release-implementation-checklist.md | 61 ++++++++++++++++++- 2 files changed, 65 insertions(+), 4 deletions(-) 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 ed2fb860795..72eea955141 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 @@ -10,8 +10,12 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected raw `ssh2` -session adapter is locally green; exact-head native proof is next, 2026-07-15, Codex implementation -owner.** `E-M6-SFTP-SESSION-ADAPTER-LOCAL-001` passes 9/9 purpose, 70/70 adapter+connection, +session adapter is exact-head green; live adapter/tree composition audit is next, 2026-07-15, Codex +implementation owner.** `E-M6-SFTP-SESSION-ADAPTER-CI-001` closes all six primary native jobs, both +Linux supplements, Windows x64 floor, PR Checks, Golden E2E, and computer-use on exact commit +`fbec34cf1`; Windows arm64 retains only the declared hosted build-26200 versus required-26100 +rejection after complete runtime smoke. No live SFTP/tree or product/default claim is made. +`E-M6-SFTP-SESSION-ADAPTER-LOCAL-001` passes 9/9 purpose, 70/70 adapter+connection, 182 focused cases with one declared skip, 597 broad relay cases with five declared skips, 280/280 release contracts, typecheck, full lint, formatting, max-lines, diff, protected-resolver, and isolation gates. No live SFTP/tree or product/default claim is made. 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 9d7fee9da64..4eb155a3c41 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected raw `ssh2` session adapter is locally green; exact-head native proof is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **RAW SFTP SESSION ADAPTER LOCAL GREEN — 2026-07-15, Codex implementation owner** — `E-M6-SFTP-SESSION-ADAPTER-LOCAL-001` passes the nine-case purpose contract, optional-signal connection lifecycle, focused/broad/release tests, typecheck, full lint, format, max-lines, diff, protected-resolver, and isolation gates. It proves bound raw operations, raw-close callback settlement, and five-second force-close escalation at a disconnected factory; existing no-argument `SshConnection.sftp()` behavior is preserved. Commit/push and require exact-head all-six native and adjacent CI before any live adapter/tree composition. Do not add a tree-transfer importer, live SFTP/server claim, product/settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected raw `ssh2` session adapter is exact-head green; live adapter/tree composition audit is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **RAW SFTP SESSION ADAPTER EXACT-HEAD GREEN — 2026-07-15, Codex implementation owner** — `E-M6-SFTP-SESSION-ADAPTER-CI-001` closes all-six primary native, both Linux supplement, Windows x64 floor, PR Checks, Golden E2E, and computer-use proof for exact implementation commit `fbec34cf1`; Windows arm64 retains only the declared hosted build-26200 versus required-26100 rejection after complete runtime smoke. Audit the smallest disconnected live adapter/tree composition next, including a concrete real connection-force-close owner and OpenSSH SFTP server. Do not credit live transfer, server filesystem/modes, full-size network metrics, product/settings/fallback wiring, tuple enablement, publication, or default behavior from native callback mocks. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -17150,6 +17150,63 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 7/7 in 356 ms. The publication/default path remain absent; legacy remains the sole production/default path and SignPath remains deferred. Exact-head all-six native and adjacent CI is required next. +### E-M6-SFTP-SESSION-ADAPTER-CI-001 — all-six clients accept raw-session lifecycle contracts + +- Date/owner/head: 2026-07-15, Codex implementation owner; exact implementation commit + `fbec34cf128c3800738d48264ab8b471148b7d57` on draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). +- Native workflow: SSH Relay Runtime Artifacts run + [29510291373](https://github.com/stablyai/orca/actions/runs/29510291373). All six primary Node + 24.18.0 jobs pass the nine-case raw-session suite, complete contract set, deterministic build/ + smoke, full-size extraction/cache/scan/stream, and evidence upload: Linux x64 + [87661707668](https://github.com/stablyai/orca/actions/runs/29510291373/job/87661707668), Darwin arm64 + [87661707691](https://github.com/stablyai/orca/actions/runs/29510291373/job/87661707691), Darwin x64 + [87661707695](https://github.com/stablyai/orca/actions/runs/29510291373/job/87661707695), Windows arm64 + [87661707709](https://github.com/stablyai/orca/actions/runs/29510291373/job/87661707709), Linux arm64 + [87661707745](https://github.com/stablyai/orca/actions/runs/29510291373/job/87661707745), and Windows x64 + [87661707769](https://github.com/stablyai/orca/actions/runs/29510291373/job/87661707769). +- Purpose/native totals: every client passes all nine new cases with no suite skip: Linux x64 20 ms, + Linux arm64 15 ms, Darwin x64 26 ms, Darwin arm64 9 ms, Windows x64 26 ms, and Windows arm64 + 18 ms. Overall totals are 765 passed / one declared skip on each Linux client, 764 passed / two + declared skips on each Darwin client, and 752 passed / 18 declared skips on each Windows client. +- Exact full-size client baselines remain green: Linux x64 34 files / 124,846,430 bytes, stream + 186.439686 ms / 262,144 incremental RSS bytes; Linux arm64 34 / 122,865,324, + 139.640399 ms / 655,360; Darwin x64 35 / 124,316,655, 463.918131 ms / 294,912; Darwin arm64 + 35 / 122,027,869, 150.561458 ms / 98,304; Windows x64 42 / 96,527,161, 188.4208 ms / + 4,845,568; Windows arm64 42 / 85,213,511, 357.8897 ms / 1,687,552. Every existing cold/warm/ + scan/stream/retention/eviction budget passes. These remain client-source regression baselines, not + live SFTP network measurements. +- Baselines: Linux arm64 supplement + [87662625445](https://github.com/stablyai/orca/actions/runs/29510291373/job/87662625445), Linux x64 + supplement + [87662625495](https://github.com/stablyai/orca/actions/runs/29510291373/job/87662625495), and Windows + x64 floor [87664691249](https://github.com/stablyai/orca/actions/runs/29510291373/job/87664691249) + pass. Windows x64 verifies 42 files / 96,527,161 bytes, Node/PTY/watcher/two-second settlement in + 5,367.371 ms at 49,827,840 RSS, and completes verification in 6,575.312 ms on build 20348. Windows + arm64 floor [87664691235](https://github.com/stablyai/orca/actions/runs/29510291373/job/87664691235) + retains only the declared hosted-image rejection: build 26200 observed versus 26100 required. + Before that rejection it verifies 42 files / 85,213,511 bytes and Node/PTY/watcher/two-second + settlement in 6,086.7266 ms at 48,390,144 RSS; complete verification is 7,972.4327 ms. +- Adjacent exact-head workflows pass: PR Checks run + [29510290584](https://github.com/stablyai/orca/actions/runs/29510290584), verify job + [87661655083](https://github.com/stablyai/orca/actions/runs/29510290584/job/87661655083); Golden E2E + run [29510290671](https://github.com/stablyai/orca/actions/runs/29510290671), Linux job + [87661655199](https://github.com/stablyai/orca/actions/runs/29510290671/job/87661655199) and macOS + job [87661655233](https://github.com/stablyai/orca/actions/runs/29510290671/job/87661655233); + computer-use run [29510290118](https://github.com/stablyai/orca/actions/runs/29510290118), Ubuntu + native-smoke job + [87661653654](https://github.com/stablyai/orca/actions/runs/29510290118/job/87661653654) and Windows + native-smoke job + [87661653716](https://github.com/stablyai/orca/actions/runs/29510290118/job/87661653716). +- Boundary/residual gaps: the suite uses an EventEmitter callback fake and does not create a raw + `SFTPWrapper` from an authenticated connection. Native clients therefore do not prove real + `ssh2` open/end/error/close ordering, connection-force-close, reconnect effects, or adapter/tree + composition against OpenSSH. They also do not prove server filesystem/mode behavior, full-size + network memory/cancellation, high-RTT one-versus-four transfer, `MaxSessions=1`, or a live remote + matrix cell. The adapter still has no non-test importer. Remote bundled-Node verification/install/ + publish/launch and every product/Beta/settings/fallback/tuple/publication/default path remain + absent; legacy remains the sole production/default path and SignPath remains deferred. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 32f78d720febdb5581282cfbc64b6aae374930f4 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 11:52:28 -0400 Subject: [PATCH 221/267] feat: compose bounded SSH relay SFTP transfer Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 85 +++++ .../ssh-relay-runtime-workflow.test.mjs | 20 ++ ...elease-implementation-checklist-summary.md | 17 +- ...github-release-implementation-checklist.md | 130 +++++++- ...y-runtime-sftp-connection-transfer.test.ts | 170 ++++++++++ ...-relay-runtime-sftp-connection-transfer.ts | 140 ++++++++ ...lay-runtime-sftp-openssh-full-size.test.ts | 311 ++++++++++++++++++ ...h-relay-runtime-sftp-tree-transfer.test.ts | 69 +++- .../ssh-relay-runtime-sftp-tree-transfer.ts | 24 +- 9 files changed, 936 insertions(+), 30 deletions(-) create mode 100644 src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.test.ts create mode 100644 src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.ts create mode 100644 src/main/ssh/ssh-relay-runtime-sftp-openssh-full-size.test.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 60aaea0a288..6287cd1da6c 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -13,6 +13,7 @@ on: - 'package.json' - 'pnpm-lock.yaml' - 'src/main/ipc/parcel-watcher-process-entry.ts' + - 'src/main/ssh/ssh-relay-runtime-*' - 'src/relay/**' workflow_dispatch: # Why: callers may gate on the native graph only after separate release wiring is reviewed. @@ -286,6 +287,7 @@ jobs: src/main/ssh/ssh-relay-runtime-source-scan.test.ts \ src/main/ssh/ssh-relay-runtime-source-stream.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts \ + src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-session.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts \ src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts \ @@ -553,6 +555,88 @@ jobs: src/main/ssh/ssh-relay-artifact-cache-entry-full-size.test.ts test ! -e "$extraction_output" + - name: Start loopback OpenSSH SFTP fixture + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + if ! test -x /usr/sbin/sshd; then + apt_options=(-o Acquire::Retries=3 -o Acquire::https::Timeout=15 -o Acquire::ForceIPv4=true) + timeout --signal=TERM --kill-after=15s 180s sudo apt-get "${apt_options[@]}" update -qq + timeout --signal=TERM --kill-after=15s 180s sudo apt-get "${apt_options[@]}" install -y -qq openssh-server + fi + fixture="$RUNNER_TEMP/orca-live-sftp" + mkdir -p "$fixture" "$HOME/.ssh" "$fixture/remote" + chmod 700 "$HOME/.ssh" "$fixture" + ssh-keygen -q -t ed25519 -N '' -f "$fixture/client-key" + ssh-keygen -q -t ed25519 -N '' -f "$fixture/host-key" + cp "$fixture/client-key.pub" "$HOME/.ssh/authorized_keys" + chmod 600 "$HOME/.ssh/authorized_keys" "$fixture/client-key" "$fixture/host-key" + sudo chown root:root "$fixture/host-key" + port=22222 + server_version=$(/usr/sbin/sshd -V 2>&1 | head -1) + config="$fixture/sshd_config" + printf '%s\n' \ + "Port $port" \ + 'ListenAddress 127.0.0.1' \ + "HostKey $fixture/host-key" \ + "PidFile $fixture/sshd.pid" \ + "AuthorizedKeysFile $HOME/.ssh/authorized_keys" \ + "AllowUsers $USER" \ + 'PasswordAuthentication no' \ + 'KbdInteractiveAuthentication no' \ + 'ChallengeResponseAuthentication no' \ + 'UsePAM no' \ + 'StrictModes yes' \ + 'LogLevel VERBOSE' \ + 'Subsystem sftp internal-sftp' > "$config" + sudo mkdir -p /run/sshd + sudo /usr/sbin/sshd -f "$config" -E "$fixture/sshd.log" + for attempt in {1..20}; do + if ssh -F /dev/null -i "$fixture/client-key" -p "$port" \ + -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + "$USER@127.0.0.1" true; then + break + fi + if test "$attempt" = 20; then + cat "$fixture/sshd.log" + exit 1 + fi + sleep 0.25 + done + { + printf 'ORCA_SSH_RELAY_LIVE_SFTP_HOST=127.0.0.1\n' + printf 'ORCA_SSH_RELAY_LIVE_SFTP_PORT=%s\n' "$port" + printf 'ORCA_SSH_RELAY_LIVE_SFTP_USER=%s\n' "$USER" + printf 'ORCA_SSH_RELAY_LIVE_SFTP_IDENTITY=%s\n' "$fixture/client-key" + printf 'ORCA_SSH_RELAY_LIVE_SFTP_REMOTE_ROOT=%s\n' "$fixture/remote" + printf 'ORCA_SSH_RELAY_LIVE_SFTP_SERVER_VERSION=%s\n' "$server_version" + } >> "$GITHUB_ENV" + + - name: Measure exact full-size runtime over live OpenSSH SFTP + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + first_output="$RUNNER_TEMP/ssh-relay-runtime/${{ matrix.tuple }}/first" + ORCA_SSH_RELAY_FULL_SIZE_RUNTIME_ROOT="$first_output/runtime" \ + ORCA_SSH_RELAY_FULL_SIZE_IDENTITY="$first_output/orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json" \ + pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 --reporter=verbose \ + src/main/ssh/ssh-relay-runtime-sftp-openssh-full-size.test.ts + + - name: Stop loopback OpenSSH SFTP fixture + if: always() && runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + fixture="$RUNNER_TEMP/orca-live-sftp" + if test -s "$fixture/sshd.pid"; then + sudo kill "$(cat "$fixture/sshd.pid")" || true + fi + if test -f "$fixture/sshd.log"; then + sudo tail -200 "$fixture/sshd.log" + fi + - name: Upload unpublished artifact evidence uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: @@ -844,6 +928,7 @@ jobs: src/main/ssh/ssh-relay-runtime-source-scan.test.ts ` src/main/ssh/ssh-relay-runtime-source-stream.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts ` + src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-session.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts ` src/main/ssh/ssh-relay-compiled-manifest-trust.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 990f4ae5fa7..97bf9a9a7aa 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -83,6 +83,25 @@ describe('SSH relay runtime artifact workflow', () => { ) }) + it('runs exact full-size Linux runtime transfer against loopback-only stock OpenSSH', async () => { + const workflow = parse(await readFile(workflowUrl, 'utf8')) + const steps = workflow.jobs['build-posix-runtime'].steps + const start = steps.find((step) => step.name === 'Start loopback OpenSSH SFTP fixture') + const measure = steps.find( + (step) => step.name === 'Measure exact full-size runtime over live OpenSSH SFTP' + ) + const stop = steps.find((step) => step.name === 'Stop loopback OpenSSH SFTP fixture') + + expect(start.if).toBe("runner.os == 'Linux'") + expect(start.run).toContain('ListenAddress 127.0.0.1') + expect(start.run).toContain('Subsystem sftp internal-sftp') + expect(start.run).toContain('timeout --signal=TERM --kill-after=15s 180s sudo apt-get') + expect(measure.if).toBe("runner.os == 'Linux'") + expect(measure.run).toContain('ssh-relay-runtime-sftp-openssh-full-size.test.ts') + expect(measure.run).toContain('first_output/runtime') + expect(stop.if).toBe("always() && runner.os == 'Linux'") + }) + it('uploads only the first output after two clean builds verify and compare', async () => { const source = await readFile(workflowUrl, 'utf8') const workflow = parse(source) @@ -144,6 +163,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-runtime-source-scan.test.ts', 'ssh-relay-runtime-source-stream.test.ts', 'ssh-relay-runtime-sftp-file-destination.test.ts', + 'ssh-relay-runtime-sftp-connection-transfer.test.ts', 'ssh-relay-runtime-sftp-session.test.ts', 'ssh-relay-runtime-sftp-tree-transfer.test.ts', 'ssh-relay-compiled-manifest-trust.test.ts', 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 72eea955141..1f38d7ffc10 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 @@ -9,9 +9,20 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected raw `ssh2` -session adapter is exact-head green; live adapter/tree composition audit is next, 2026-07-15, Codex -implementation owner.** `E-M6-SFTP-SESSION-ADAPTER-CI-001` closes all six primary native jobs, both +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected live +OpenSSH SFTP composition is in progress, 2026-07-15, Codex implementation owner.** +`E-M6-SFTP-LIVE-COMPOSITION-AUDIT-001` limits this package to one authenticated built-in +`SshConnection`, one raw SFTP channel, the existing one-to-four file transfer, captured-transport- +only force close with awaited teardown, and exact full-size Linux x64/arm64 runtime transfer against +target-native OpenSSH runners. It also requires ordinary cancellation cleanup to run before a short +retained-callback breaker closes the session. No product/default, system-SSH, Windows-remote, +high-RTT, `MaxSessions=1`, or tuple-enablement claim is made. SignPath remains deferred. +`E-M6-SFTP-LIVE-COMPOSITION-LOCAL-001` is locally green: 15/15 purpose cases, 189 focused cases +with two declared skips, 603 broad relay cases with six declared skips, 281/281 release contracts, +typecheck, full lint, formatting, max-lines, diff, protected-resolver, and isolation gates pass. The +full-size live suite is deliberately skipped without runner OpenSSH/runtime inputs, so exact-head +Linux x64/arm64 live CI remains the next gate. +`E-M6-SFTP-SESSION-ADAPTER-CI-001` closes all six primary native jobs, both Linux supplements, Windows x64 floor, PR Checks, Golden E2E, and computer-use on exact commit `fbec34cf1`; Windows arm64 retains only the declared hosted build-26200 versus required-26100 rejection after complete runtime smoke. No live SFTP/tree or product/default claim is made. 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 4eb155a3c41..8df452fb05e 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected raw `ssh2` session adapter is exact-head green; live adapter/tree composition audit is next**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **RAW SFTP SESSION ADAPTER EXACT-HEAD GREEN — 2026-07-15, Codex implementation owner** — `E-M6-SFTP-SESSION-ADAPTER-CI-001` closes all-six primary native, both Linux supplement, Windows x64 floor, PR Checks, Golden E2E, and computer-use proof for exact implementation commit `fbec34cf1`; Windows arm64 retains only the declared hosted build-26200 versus required-26100 rejection after complete runtime smoke. Audit the smallest disconnected live adapter/tree composition next, including a concrete real connection-force-close owner and OpenSSH SFTP server. Do not credit live transfer, server filesystem/modes, full-size network metrics, product/settings/fallback wiring, tuple enablement, publication, or default behavior from native callback mocks. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected authenticated-connection/SFTP-tree composition and live OpenSSH proof**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **LIVE SFTP COMPOSITION LOCALLY GREEN / EXACT-HEAD RUNNER PROOF NEXT — 2026-07-15, Codex implementation owner** — `E-M6-SFTP-LIVE-COMPOSITION-LOCAL-001` proves the disconnected authenticated-connection owner, captured-client force close, cancellation cleanup/breaker ordering, workflow oracle, focused/broad/release suites, typecheck, full lint, formatting, max-lines, diff, protected-resolver, and no-product-import gates locally. The purpose suite is 15/15; the live full-size suite is deliberately skipped locally because no runner OpenSSH/runtime inputs are present and therefore earns no live cell. Push this isolated package and require exact-head Linux x64/arm64 stock-OpenSSH full-size metrics before crediting live SFTP. Do not credit Windows/POSIX system SSH, high-RTT, `MaxSessions=1`, product/settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -17207,6 +17207,122 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` — PASS, 7/7 in 356 ms. The publish/launch and every product/Beta/settings/fallback/tuple/publication/default path remain absent; legacy remains the sole production/default path and SignPath remains deferred. +### E-M6-SFTP-LIVE-COMPOSITION-AUDIT-001 — authenticated connection ownership and live OpenSSH boundary + +- Date/owner/base: 2026-07-15, Codex implementation owner; exact clean evidence base `d11261706` + after `E-M6-SFTP-SESSION-ADAPTER-CI-001` closure. +- Audit commands: `sed -n '1,290p' src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.ts`, + `sed -n '1,280p' src/main/ssh/ssh-relay-runtime-sftp-session.ts`, relevant lifecycle ranges from + `ssh-connection.ts`, and searches for existing live-SSH/OpenSSH/workflow fixtures. Result: the raw + adapter and tree contracts compose without changing authentication or relay RPC, but no + non-test owner captures the exact `ssh2` transport that opened the raw SFTP channel or awaits + connection teardown if channel close exceeds its grace. +- Composition boundary: add one purpose-named module that accepts the existing authenticated + `SshConnection`, captures its exact current built-in `ssh2` client, opens exactly one signal-bound + raw SFTP session, rejects a connection-generation race after closing the returned channel, and + delegates to the existing session adapter and tree orchestrator. Force-close may destroy only the + captured client, must await that client's public `close` event with a bounded timeout, and must not + dispose the `SshConnection` or destroy a replacement client; the existing disconnect handler owns + reconnect state. System-SSH targets reject this composition without opening anything. +- Cancellation correction: current tree abort starts session close immediately. That is required to + release a retained raw callback, but it can race and prevent reverse cleanup when the active + callback settles normally. On abort, arm a short breaker below the 10-second cancellation-and-join + ceiling; when the stream reaches its catch boundary, cancel the breaker before reverse cleanup. + A genuinely retained callback is still released by timed session close, while normal cancellation + gets one opportunity to remove proved-owned paths before closing the session. Tests must prove + both orders and that no write begins after abort. +- Layer-A proof: purpose tests cover exact signal and connection identity, one SFTP open, system-SSH + rejection, connection replacement race, captured-client-only destroy/await-close, force-close + timeout/failure, normal owned-tree cancellation cleanup before close, and retained-callback breaker + settlement. Existing focused/broad/static/isolation gates remain mandatory. +- Layer-B proof: on target-native Ubuntu x64 and arm64 GitHub runners, start a loopback-only stock + OpenSSH `sshd` with key authentication and internal SFTP, then transfer the exact runtime produced + by that runner's build job. Validate the complete staged tree bytes/count/modes, exclusive-root + behavior, serial and four-file measurements, full-size incremental desktop RSS under 80 MiB, + cancellation settlement under 10 seconds, no later writes, and owned-stage cleanup. Record runner + image, architecture, OpenSSH version, runtime tuple/content/size/count, duration, RSS, and gaps. +- Explicit residual boundary: this package does not prove a Windows remote, cross-client matrix, + high-RTT comparison, `MaxSessions=1`, network interruption/disk/quota/inode/noexec/AV failures, + POSIX or Windows system-SSH transfer, remote bundled-Node verification/install/publish/launch, + product/Beta/settings/fallback wiring, tuple enablement, publication, or default behavior. Legacy + remains the sole production/default path and SignPath remains deferred. + +### E-M6-SFTP-LIVE-COMPOSITION-LOCAL-RED-001 — missing owner and cancellation ordering fail as audited + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0, + pnpm 10.24.0, exact base `d11261706` plus audit/test/workflow-only diff. Exact Node 24 remains CI. +- Command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.test.ts +src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts` — EXPECTED FAIL: the five-case + connection suite collects zero tests because the purpose-named composition module is absent, and + the new ordinary-cancellation case observes the final owned-tree `rmdir` after session close + (event indices 23 versus 11). Nine prior tree cases pass. Vitest 1.87 seconds / 3.24 seconds wall, + 141,524,992-byte maximum RSS. The temporary unhandled-rejection diagnostic came from attaching + the retained-callback test's rejection expectation after advancing fake time and was corrected in + the test before GREEN; it is not counted as product evidence. +- RED scope: tests and native workflow inclusion only. Protected resolver files remain untouched; + no owner module, live server, product importer, settings/fallback/tuple/default behavior, or + SignPath work exists. + +### E-M6-SFTP-LIVE-COMPOSITION-LOCAL-001 — disconnected owner and live-runner oracle are locally green + +- Date/owner/environment: 2026-07-15, Codex implementation owner; local macOS arm64, Node 26.0.0, + pnpm 10.24.0, atop exact evidence base `d11261706` plus this isolated diff. Exact Node 24 and stock + OpenSSH remain the native CI gate. +- Implementation: the 140-line connection module accepts only the existing authenticated built-in + connection boundary, captures the exact current `ssh2` client, opens one signal-bound raw SFTP + channel, and rejects/awaits cleanup if the connection changes during open. Its escalation destroys + only the captured client and awaits public client close for five seconds; a replacement client is + never touched, normal raw-session close leaves the connection open, and existing disconnect logic + retains reconnect ownership. No `SshConnection` auth/RPC/default behavior changes. +- Cancellation correction: the tree orchestrator arms a 250 ms raw-callback breaker on abort. If the + callback settles and the stream reaches its catch boundary, it cancels the breaker, reverse-cleans + proved-owned files/directories, then closes the session. If a callback remains retained, the + breaker closes the raw session to release the borrowed 64 KiB source buffer. Tests prove both + orders and no second write after abort; the overall 10-second cancellation-and-join ceiling is + unchanged. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.test.ts +src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts` — PASS, 15/15 in 1.59 seconds Vitest / + 2.68 seconds wall, 139,755,520-byte maximum RSS. Five owner cases cover exact signal/one open, + normal connection retention, system-SSH rejection, generation replacement cleanup, captured-only + force close, awaited raw/client close, and bounded missing-close failure. Ten tree cases include + ordinary cancellation cleanup before close and retained-callback breaker settlement. +- Focused command: connection, source tree/scan/stream, three SFTP boundaries, acquisition/cache + resolution/population/integration, live-suite skip oracle, and workflow contract — PASS, 14 files + plus one declared live-input skip; 189 passed / two declared skips in 14.22 seconds Vitest / + 15.70 seconds wall, 172,146,688-byte maximum RSS. +- Broad commands: `src/main/ssh/ssh-relay-*.test.ts` — PASS, 46 files passed / four declared skipped, + 603 tests passed / six declared skipped in 25.09 seconds Vitest / 27.10 seconds wall, + 301,694,976-byte maximum RSS; `config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files / + 281 tests in 22.94 seconds Vitest / 24.87 seconds wall, 190,398,464-byte maximum RSS. Only the + established stale-lock diagnostic and local Node 26 module deprecation appear. +- Static/isolation commands: `pnpm typecheck` passes in 4.75 seconds wall with 1,249,492,992-byte + maximum RSS; targeted `oxlint`, targeted `oxfmt --check`, standalone max-lines ratchet, + `git diff --check`, protected-resolver empty diff, workflow YAML/oracle, and no non-test consumer + outside this disconnected module pass. Full `pnpm lint` passes in 17.44 seconds wall with + 2,025,259,008-byte maximum RSS: switch exhaustiveness, 41 reliability gates, 355-entry max-lines + ratchet, bundled-skill verification, 9,837 localization references, locale parity, and zero- + candidate coverage pass with only pre-existing warnings. Module/purpose/live-test sizes are + 140/170/311 lines, and the updated tree module/test are 271/426 lines, with no new bypass. +- Live runner oracle: the existing artifact workflow now triggers for runtime-transfer changes and + includes the five owner cases once in each native command family. Linux x64 and arm64 build jobs, + after producing and locally verifying the exact runtime, start a loopback-only stock OpenSSH + `sshd` with key authentication/internal SFTP. The purpose-named full-size suite scans the exact + identity/runtime, transfers it serially and with four files, validates every path/byte/hash/mode, + proves exclusive-root refusal leaves the tree intact, cancels after the first completed write, + requires cleanup/no later writes and sub-10-second settlement, enforces the 80 MiB desktop RSS + ceiling, and emits tuple/content/file/byte/server/runner/duration/RSS metrics. The fixture setup is + bounded and cleanup runs under `always()`. +- Local live result: deliberately SKIP, one case, because the required seven OpenSSH/runtime environment + inputs are absent. This proves the skip gate only and earns no live/server/filesystem/full-size + evidence. Exact-head Linux x64/arm64 CI is mandatory next. +- Residual gaps: no Windows remote, cross-client matrix, high-RTT comparison, `MaxSessions=1`, live + peak channel/open-handle instrumentation, interruption/disk/quota/inode/noexec/AV failure, POSIX + or Windows system-SSH path, remote bundled-Node verification/install/publish/launch, product/Beta/ + settings/fallback wiring, tuple enablement, publication, or default behavior is proved. Legacy + remains the sole production/default path and SignPath remains deferred. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -17265,12 +17381,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit/push the locally green Windows test-oracle correction, then require fresh all-six exact-head -native/full-size and adjacent CI before checking the package or advancing. The stream must create no -remote files or open SSH channels. -Do not open an SSH channel, transfer to or install on a remote, modify platform detection/selectors, -add an Electron/startup consumer, mode wiring, fallback, tuple enablement, release publication, or -default behavior. +Commit and push the locally green disconnected SFTP composition package, then require exact-head +target-native Linux x64/arm64 OpenSSH full-size evidence and all-six native/adjacent regression CI +before checking any Milestone 6 SFTP item or advancing. Do not add an Electron/startup/product +importer, per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. diff --git a/src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.test.ts b/src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.test.ts new file mode 100644 index 00000000000..c382cfe2f46 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.test.ts @@ -0,0 +1,170 @@ +import { EventEmitter } from 'node:events' + +import type { Client, SFTPWrapper } from 'ssh2' +import { describe, expect, it, vi } from 'vitest' + +import type { SshConnection } from './ssh-connection' +import { openSshRelayRuntimeSftpTreeSessionForConnection } from './ssh-relay-runtime-sftp-connection-transfer' + +type Callback = (error?: Error) => void +type ConnectionBoundary = Pick + +function createRawSession(options: { closeOnEnd?: boolean } = {}): SFTPWrapper { + const emitter = new EventEmitter() + const raw = Object.assign(emitter, { + mkdir: vi.fn((_path: string, _attributes: unknown, callback: Callback) => callback()), + rmdir: vi.fn((_path: string, callback: Callback) => callback()), + open: vi.fn( + ( + _path: string, + _flags: string, + _attributes: unknown, + callback: (error: Error | undefined, handle: Buffer) => void + ) => callback(undefined, Buffer.from('handle')) + ), + write: vi.fn( + ( + _handle: Buffer, + _buffer: Buffer, + _offset: number, + _length: number, + _position: number, + callback: Callback + ) => callback() + ), + fchmod: vi.fn((_handle: Buffer, _mode: number, callback: Callback) => callback()), + fstat: vi.fn( + (_handle: Buffer, callback: (error: Error | undefined, value: { mode: number }) => void) => + callback(undefined, { mode: 0o100755 }) + ), + close: vi.fn((_handle: Buffer, callback: Callback) => callback()), + unlink: vi.fn((_path: string, callback: Callback) => callback()), + end: vi.fn(() => { + if (options.closeOnEnd) { + queueMicrotask(() => emitter.emit('close')) + } + }) + }) + return raw as unknown as SFTPWrapper +} + +function createClient(onDestroy?: () => void): Client & { destroy: ReturnType } { + const emitter = new EventEmitter() + const client = Object.assign(emitter, { + destroy: vi.fn(() => onDestroy?.()) + }) + return client as unknown as Client & { destroy: ReturnType } +} + +function createConnection( + client: Client | null, + raw: SFTPWrapper, + options: { systemSsh?: boolean; onSftp?: () => void } = {} +): ConnectionBoundary & { + setClient: (next: Client | null) => void + sftp: ReturnType +} { + let currentClient = client + const sftp = vi.fn(async (_signal?: AbortSignal) => { + options.onSftp?.() + return raw + }) + return { + getClient: () => currentClient, + setClient: (next) => { + currentClient = next + }, + usesSystemSshTransport: () => options.systemSsh ?? false, + sftp + } +} + +describe('SSH relay runtime authenticated SFTP connection transfer', () => { + it('opens one raw session with the exact signal and leaves the connection open after normal close', async () => { + const raw = createRawSession({ closeOnEnd: true }) + const client = createClient() + const connection = createConnection(client, raw) + const signal = new AbortController().signal + + const session = await openSshRelayRuntimeSftpTreeSessionForConnection(connection, signal) + await session.close() + + expect(connection.sftp).toHaveBeenCalledOnce() + expect(connection.sftp).toHaveBeenCalledWith(signal) + expect(raw.end).toHaveBeenCalledOnce() + expect(client.destroy).not.toHaveBeenCalled() + }) + + it('rejects system SSH without opening an SFTP channel', async () => { + const raw = createRawSession() + const connection = createConnection(null, raw, { systemSsh: true }) + + await expect( + openSshRelayRuntimeSftpTreeSessionForConnection(connection, new AbortController().signal) + ).rejects.toThrow(/built-in SSH/i) + expect(connection.sftp).not.toHaveBeenCalled() + }) + + it('closes a raw channel opened by a replaced connection generation', async () => { + const raw = createRawSession({ closeOnEnd: true }) + const first = createClient() + const replacement = createClient() + const connection = createConnection(first, raw, { + onSftp: () => connection.setClient(replacement) + }) + + await expect( + openSshRelayRuntimeSftpTreeSessionForConnection(connection, new AbortController().signal) + ).rejects.toThrow(/connection changed/i) + expect(raw.end).toHaveBeenCalledOnce() + expect(first.destroy).not.toHaveBeenCalled() + expect(replacement.destroy).not.toHaveBeenCalled() + }) + + it('force-closes only the captured client and awaits both client and raw close', async () => { + vi.useFakeTimers() + try { + const raw = createRawSession() + const first = createClient(() => { + raw.emit('close') + first.emit('close') + }) + const replacement = createClient() + const connection = createConnection(first, raw) + const session = await openSshRelayRuntimeSftpTreeSessionForConnection( + connection, + new AbortController().signal + ) + connection.setClient(replacement) + const close = session.close(new Error('cancelled transfer')) + + await vi.advanceTimersByTimeAsync(5_000) + await close + expect(first.destroy).toHaveBeenCalledOnce() + expect(replacement.destroy).not.toHaveBeenCalled() + } finally { + vi.useRealTimers() + } + }) + + it('fails within the bounded force-close window when client teardown never confirms', async () => { + vi.useFakeTimers() + try { + const raw = createRawSession() + const client = createClient() + const connection = createConnection(client, raw) + const session = await openSshRelayRuntimeSftpTreeSessionForConnection( + connection, + new AbortController().signal + ) + const close = session.close() + const rejected = expect(close).rejects.toThrow(/connection close timed out/i) + + await vi.advanceTimersByTimeAsync(10_000) + await rejected + expect(client.destroy).toHaveBeenCalledOnce() + } finally { + vi.useRealTimers() + } + }) +}) diff --git a/src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.ts b/src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.ts new file mode 100644 index 00000000000..cb7193fd1aa --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.ts @@ -0,0 +1,140 @@ +import type { Client, SFTPWrapper } from 'ssh2' + +import type { SshConnection } from './ssh-connection' +import { openSshRelayRuntimeSftpTreeSession } from './ssh-relay-runtime-sftp-session' +import { + transferSshRelayRuntimeTreeViaSftp, + type SshRelayRuntimeSftpTreeSession, + type SshRelayRuntimeSftpTreeTransferOptions, + type SshRelayRuntimeSftpTreeTransferResult +} from './ssh-relay-runtime-sftp-tree-transfer' + +const TRANSPORT_CLOSE_TIMEOUT_MS = 5_000 + +export type SshRelayRuntimeBuiltInSftpConnection = Pick< + SshConnection, + 'getClient' | 'sftp' | 'usesSystemSshTransport' +> + +export type SshRelayRuntimeSftpConnectionTransferOptions = Omit< + SshRelayRuntimeSftpTreeTransferOptions, + 'openSession' +> & + Readonly<{ connection: SshRelayRuntimeBuiltInSftpConnection }> + +function waitForClose( + emitter: Pick, + beginClose: () => void, + timeoutMessage: string +): Promise { + return new Promise((resolve, reject) => { + let settled = false + const finish = (error?: unknown): void => { + if (settled) { + return + } + settled = true + clearTimeout(timer) + emitter.removeListener('close', onClose) + if (error === undefined) { + resolve() + } else { + reject(error) + } + } + const onClose = (): void => finish() + const timer = setTimeout(() => finish(new Error(timeoutMessage)), TRANSPORT_CLOSE_TIMEOUT_MS) + emitter.once('close', onClose) + try { + beginClose() + } catch (error) { + finish(error) + } + }) +} + +async function forceCloseCapturedClient(client: Client): Promise { + await waitForClose( + client, + () => client.destroy(), + 'SSH relay runtime owning connection close timed out' + ) +} + +async function closeSessionFromReplacedConnection(raw: SFTPWrapper, client: Client): Promise { + try { + await waitForClose( + raw, + () => raw.end(), + 'SSH relay runtime replaced connection SFTP close timed out' + ) + } catch (error) { + try { + await forceCloseCapturedClient(client) + } catch (forceError) { + throw new AggregateError( + [error, forceError], + 'SSH relay runtime replaced connection cleanup failed' + ) + } + throw error + } +} + +function validateConnection(connection: SshRelayRuntimeBuiltInSftpConnection): void { + if ( + !connection || + typeof connection.getClient !== 'function' || + typeof connection.sftp !== 'function' || + typeof connection.usesSystemSshTransport !== 'function' + ) { + throw new Error('SSH relay runtime SFTP connection boundary is invalid') + } + if (connection.usesSystemSshTransport()) { + throw new Error('SSH relay runtime SFTP transfer requires the built-in SSH transport') + } +} + +export async function openSshRelayRuntimeSftpTreeSessionForConnection( + connection: SshRelayRuntimeBuiltInSftpConnection, + signal: AbortSignal +): Promise { + validateConnection(connection) + signal.throwIfAborted() + const capturedClient = connection.getClient() + if (!capturedClient) { + throw new Error('SSH relay runtime SFTP transfer requires a connected built-in SSH client') + } + + return openSshRelayRuntimeSftpTreeSession({ + signal, + openRawSession: async (exactSignal) => { + if (connection.getClient() !== capturedClient) { + throw new Error('SSH relay runtime SFTP connection changed before channel open') + } + const raw = await connection.sftp(exactSignal) + if (connection.getClient() !== capturedClient) { + // Why: never return a channel whose owning connection generation is no longer current. + await closeSessionFromReplacedConnection(raw, capturedClient) + throw new Error('SSH relay runtime SFTP connection changed during channel open') + } + return raw + }, + // Why: escalation may tear down only the transport that owns the stuck raw callback. + forceCloseConnection: () => forceCloseCapturedClient(capturedClient) + }) +} + +export function transferSshRelayRuntimeTreeOverSftpConnection( + options: SshRelayRuntimeSftpConnectionTransferOptions +): Promise { + const { connection, ...treeOptions } = options + return transferSshRelayRuntimeTreeViaSftp({ + ...treeOptions, + openSession: (signal) => openSshRelayRuntimeSftpTreeSessionForConnection(connection, signal) + }) +} + +export const SSH_RELAY_RUNTIME_SFTP_CONNECTION_LIMITS = Object.freeze({ + transportCloseTimeoutMs: TRANSPORT_CLOSE_TIMEOUT_MS +}) diff --git a/src/main/ssh/ssh-relay-runtime-sftp-openssh-full-size.test.ts b/src/main/ssh/ssh-relay-runtime-sftp-openssh-full-size.test.ts new file mode 100644 index 00000000000..20a18db9e63 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-sftp-openssh-full-size.test.ts @@ -0,0 +1,311 @@ +import { createHash } from 'node:crypto' +import { lstat, open, opendir, readFile, rm } from 'node:fs/promises' +import { join } from 'node:path' +import { performance } from 'node:perf_hooks' + +import { describe, expect, it, vi } from 'vitest' + +vi.mock('electron', () => ({ app: { getAppPath: () => process.cwd() } })) + +import type { SshTarget } from '../../shared/ssh-types' +import { SshConnection } from './ssh-connection' +import { transferSshRelayRuntimeTreeOverSftpConnection } from './ssh-relay-runtime-sftp-connection-transfer' +import { + scanSshRelayRuntimeSourceTree, + type SshRelayRuntimeScannedSourceTree +} from './ssh-relay-runtime-source-scan' +import type { SshRelayRuntimeSourceTree } from './ssh-relay-runtime-source-tree' +import { SSH_RELAY_RUNTIME_SOURCE_STREAM_LIMITS } from './ssh-relay-runtime-source-stream' + +type MeasurementIdentity = Pick< + SshRelayRuntimeSourceTree, + 'tupleId' | 'contentId' | 'os' | 'architecture' +> & { + entries: ( + | { path: string; type: 'directory'; mode: 0o755 } + | { + path: string + type: 'file' + role: SshRelayRuntimeSourceTree['files'][number]['role'] + size: number + mode: 0o644 | 0o755 + sha256: SshRelayRuntimeSourceTree['files'][number]['sha256'] + } + )[] + archive: { expandedSize: number; fileCount: number } +} + +type Measurement = { + result: T + elapsedMs: number + baselineRss: number + peakRss: number + incrementalRssBytes: number +} + +const host = process.env.ORCA_SSH_RELAY_LIVE_SFTP_HOST +const user = process.env.ORCA_SSH_RELAY_LIVE_SFTP_USER +const identityFile = process.env.ORCA_SSH_RELAY_LIVE_SFTP_IDENTITY +const remoteRoot = process.env.ORCA_SSH_RELAY_LIVE_SFTP_REMOTE_ROOT +const runtimeRoot = process.env.ORCA_SSH_RELAY_FULL_SIZE_RUNTIME_ROOT +const identityPath = process.env.ORCA_SSH_RELAY_FULL_SIZE_IDENTITY +const port = Number.parseInt(process.env.ORCA_SSH_RELAY_LIVE_SFTP_PORT ?? '', 10) +const hasLiveInput = Boolean( + host && + user && + identityFile && + remoteRoot && + runtimeRoot && + identityPath && + Number.isInteger(port) +) + +function parseIdentity(value: unknown): MeasurementIdentity { + if ( + typeof value !== 'object' || + value === null || + !('entries' in value) || + !Array.isArray(value.entries) || + !('archive' in value) || + typeof value.archive !== 'object' || + value.archive === null + ) { + throw new Error('Live SFTP measurement identity is incomplete') + } + return value as MeasurementIdentity +} + +function sourceTree(identity: MeasurementIdentity): SshRelayRuntimeSourceTree { + const directories = identity.entries + .filter((entry) => entry.type === 'directory') + .map((entry) => ({ + ...entry, + localPath: join(runtimeRoot as string, ...entry.path.split('/')) + })) + const files = identity.entries + .filter((entry) => entry.type === 'file') + .map((entry) => ({ + ...entry, + localPath: join(runtimeRoot as string, ...entry.path.split('/')) + })) + return Object.freeze({ + tupleId: identity.tupleId, + contentId: identity.contentId, + releaseTag: 'measurement-only', + os: identity.os, + architecture: identity.architecture, + runtimeRoot: runtimeRoot as string, + directories, + files, + fileCount: identity.archive.fileCount, + expandedBytes: identity.archive.expandedSize, + assertLeaseOwned: async () => {} + }) +} + +async function measure(operation: () => Promise): Promise> { + const baselineRss = process.memoryUsage().rss + let peakRss = baselineRss + const sample = (): void => { + peakRss = Math.max(peakRss, process.memoryUsage().rss) + } + const sampler = setInterval(sample, 1) + const startedAt = performance.now() + try { + const result = await operation() + sample() + return { + result, + elapsedMs: performance.now() - startedAt, + baselineRss, + peakRss, + incrementalRssBytes: Math.max(0, peakRss - baselineRss) + } + } finally { + clearInterval(sampler) + } +} + +async function hashFile(path: string): Promise { + const handle = await open(path, 'r') + const digest = createHash('sha256') + const buffer = Buffer.allocUnsafe(64 * 1024) + let position = 0 + try { + while (true) { + const { bytesRead } = await handle.read(buffer, 0, buffer.length, position) + if (bytesRead === 0) { + break + } + digest.update(buffer.subarray(0, bytesRead)) + position += bytesRead + } + } finally { + await handle.close() + } + return `sha256:${digest.digest('hex')}` +} + +async function collectRemotePaths(root: string): Promise { + const paths: string[] = [] + const pending = [{ localPath: root, relativePath: '' }] + while (pending.length > 0) { + const current = pending.pop()! + const directory = await opendir(current.localPath) + for await (const entry of directory) { + const relativePath = current.relativePath + ? `${current.relativePath}/${entry.name}` + : entry.name + paths.push(relativePath) + if (entry.isDirectory()) { + pending.push({ localPath: join(current.localPath, entry.name), relativePath }) + } + } + } + return paths.sort() +} + +async function validateTransferredTree( + tree: SshRelayRuntimeScannedSourceTree, + stage: string +): Promise { + const expectedPaths = [...tree.directories, ...tree.files].map((entry) => entry.path).sort() + // Why: the loopback runner can inspect sshd's filesystem without adding another proof channel. + expect(await collectRemotePaths(stage)).toEqual(expectedPaths) + for (const directory of tree.directories) { + const metadata = await lstat(join(stage, ...directory.path.split('/'))) + expect(metadata.isDirectory()).toBe(true) + expect(metadata.mode & 0o777).toBe(directory.mode) + } + for (const file of tree.files) { + const path = join(stage, ...file.path.split('/')) + const metadata = await lstat(path) + expect(metadata.isFile()).toBe(true) + expect(metadata.size).toBe(file.size) + expect(metadata.mode & 0o777).toBe(file.mode) + expect(await hashFile(path)).toBe(file.sha256) + } +} + +function stagePath(name: string, identity: MeasurementIdentity): string { + return join(remoteRoot as string, `${name}-${identity.contentId.slice('sha256:'.length, 23)}`) +} + +async function transfer( + connection: SshConnection, + tree: SshRelayRuntimeScannedSourceTree, + stage: string, + maximumConcurrency: number, + signal = new AbortController().signal, + onProgress?: (bytes: number) => void +) { + return transferSshRelayRuntimeTreeOverSftpConnection({ + connection, + tree, + remoteStagingRoot: stage, + enforcePosixMode: true, + maximumConcurrency, + signal, + onProgress: ({ bytesTransferred }) => onProgress?.(bytesTransferred) + }) +} + +describe.skipIf(!hasLiveInput)('SSH relay full-size transfer through live OpenSSH SFTP', () => { + it( + 'preserves exact bytes and modes with bounded serial, concurrent, and cancellation behavior', + { timeout: 20 * 60_000 }, + async () => { + const identity = parseIdentity(JSON.parse(await readFile(identityPath as string, 'utf8'))) + expect(identity.os).toBe('linux') + const tree = await scanSshRelayRuntimeSourceTree( + sourceTree(identity), + new AbortController().signal + ) + const target: SshTarget = { + id: 'live-sftp-measurement', + label: 'live-sftp-measurement', + host: host as string, + port, + username: user as string, + identityFile: identityFile as string, + source: 'manual' + } + const connection = new SshConnection(target, { onStateChange: () => {} }) + const serialStage = stagePath('serial', identity) + const concurrentStage = stagePath('concurrent', identity) + const cancelledStage = stagePath('cancelled', identity) + await Promise.all( + [serialStage, concurrentStage, cancelledStage].map((stage) => + rm(stage, { recursive: true, force: true }) + ) + ) + try { + await connection.connect() + const serial = await measure(() => transfer(connection, tree, serialStage, 1)) + await validateTransferredTree(tree, serialStage) + const concurrent = await measure(() => transfer(connection, tree, concurrentStage, 4)) + await validateTransferredTree(tree, concurrentStage) + await expect(transfer(connection, tree, concurrentStage, 1)).rejects.toBeTruthy() + await validateTransferredTree(tree, concurrentStage) + + const controller = new AbortController() + let abortRequestedAt = 0 + const cancelled = await measure(async () => { + const outcome = transfer( + connection, + tree, + cancelledStage, + 4, + controller.signal, + (bytes) => { + if (bytes > 0 && !controller.signal.aborted) { + abortRequestedAt = performance.now() + controller.abort(new Error('live SFTP cancellation')) + } + } + ) + await expect(outcome).rejects.toThrow('live SFTP cancellation') + return performance.now() - abortRequestedAt + }) + await expect(lstat(cancelledStage)).rejects.toMatchObject({ code: 'ENOENT' }) + await new Promise((resolve) => setTimeout(resolve, 500)) + await expect(lstat(cancelledStage)).rejects.toMatchObject({ code: 'ENOENT' }) + + const metrics = { + tupleId: identity.tupleId, + contentId: identity.contentId, + files: tree.fileCount, + bytes: tree.expandedBytes, + serverVersion: process.env.ORCA_SSH_RELAY_LIVE_SFTP_SERVER_VERSION, + runnerImage: process.env.ImageOS, + runnerVersion: process.env.ImageVersion, + runnerArchitecture: process.env.RUNNER_ARCH, + serialElapsedMs: serial.elapsedMs, + serialIncrementalRssBytes: serial.incrementalRssBytes, + concurrentElapsedMs: concurrent.elapsedMs, + concurrentIncrementalRssBytes: concurrent.incrementalRssBytes, + cancellationSettlementMs: cancelled.result, + cancellationIncrementalRssBytes: cancelled.incrementalRssBytes + } + console.log(`ssh_relay_live_sftp=${JSON.stringify(metrics)}`) + expect(serial.result.bytesTransferred).toBe(tree.expandedBytes) + expect(concurrent.result.bytesTransferred).toBe(tree.expandedBytes) + expect(serial.incrementalRssBytes).toBeLessThanOrEqual( + SSH_RELAY_RUNTIME_SOURCE_STREAM_LIMITS.maximumIncrementalMemoryBytes + ) + expect(concurrent.incrementalRssBytes).toBeLessThanOrEqual( + SSH_RELAY_RUNTIME_SOURCE_STREAM_LIMITS.maximumIncrementalMemoryBytes + ) + expect(cancelled.result).toBeLessThan(10_000) + expect(connection.getState().status).toBe('connected') + } finally { + await connection.disconnect() + await Promise.all( + [serialStage, concurrentStage, cancelledStage].map((stage) => + rm(stage, { recursive: true, force: true }) + ) + ) + } + } + ) +}) diff --git a/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts b/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts index 5b86ff8f1bd..ef113b81d7c 100644 --- a/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts +++ b/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts @@ -325,33 +325,68 @@ describe('SSH relay runtime SFTP tree transfer', () => { }) it('closes the session to settle a retained write callback on cancellation', async () => { + vi.useFakeTimers() + try { + const tree = await treeFixture() + const { session } = createSession() + const controller = new AbortController() + let retainedCallback: Callback | undefined + let writes = 0 + vi.mocked(session.operations.write).mockImplementationOnce( + (_handle, _buffer, _offset, _length, _position, callback) => { + writes += 1 + retainedCallback = callback + } + ) + vi.mocked(session.close).mockImplementationOnce(async () => { + retainedCallback?.(new Error('session closed')) + }) + const transfer = transferSshRelayRuntimeTreeViaSftp({ + tree, + remoteStagingRoot: '/staging/content', + enforcePosixMode: true, + maximumConcurrency: 1, + signal: controller.signal, + openSession: async () => session + }) + + await vi.waitFor(() => expect(writes).toBe(1)) + controller.abort(new Error('cancelled')) + const rejected = expect(transfer).rejects.toThrow(/cancelled|session closed/) + await vi.advanceTimersByTimeAsync(250) + await rejected + expect(session.close).toHaveBeenCalledOnce() + expect(writes).toBe(1) + } finally { + vi.useRealTimers() + } + }) + + it('reverse-cleans owned paths before session close when cancellation is not callback-stuck', async () => { const tree = await treeFixture() - const { session } = createSession() + const { session, events } = createSession() const controller = new AbortController() - let retainedCallback: Callback | undefined let writes = 0 - vi.mocked(session.operations.write).mockImplementationOnce( - (_handle, _buffer, _offset, _length, _position, callback) => { - writes += 1 - retainedCallback = callback - } - ) - vi.mocked(session.close).mockImplementationOnce(async () => { - retainedCallback?.(new Error('session closed')) - }) const transfer = transferSshRelayRuntimeTreeViaSftp({ tree, - remoteStagingRoot: '/staging/content', + remoteStagingRoot: '/staging/cancelled-content', enforcePosixMode: true, maximumConcurrency: 1, signal: controller.signal, - openSession: async () => session + openSession: async () => session, + onProgress: ({ bytesTransferred }) => { + writes += 1 + if (bytesTransferred > 0) { + controller.abort(new Error('ordinary cancellation')) + } + } }) - await vi.waitFor(() => expect(writes).toBe(1)) - controller.abort(new Error('cancelled')) - await expect(transfer).rejects.toThrow(/cancelled|session closed/) - expect(session.close).toHaveBeenCalledOnce() + await expect(transfer).rejects.toThrow('ordinary cancellation') + expect(events.findLastIndex((event) => event.startsWith('rmdir:'))).toBeLessThan( + events.indexOf('close-session') + ) + expect(events).toContain('rmdir:/staging/cancelled-content') expect(writes).toBe(1) }) diff --git a/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.ts b/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.ts index cf9523827e2..abb92b2033b 100644 --- a/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.ts +++ b/src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.ts @@ -39,6 +39,7 @@ export type SshRelayRuntimeSftpTreeTransferResult = Readonly< > const MAXIMUM_CONCURRENT_FILES = 4 +const ABORTED_CALLBACK_BREAKER_MS = 250 function waitForSftpCallback(register: (callback: SftpCallback) => void): Promise { return new Promise((resolve, reject) => { @@ -169,13 +170,24 @@ export async function transferSshRelayRuntimeTreeViaSftp( signal.throwIfAborted() const session = await openSession(signal) let closePromise: Promise | undefined + let abortedCallbackBreaker: ReturnType | undefined const closeSession = (reason?: unknown): Promise => { closePromise ??= Promise.resolve().then(() => session.close(reason)) return closePromise } + const clearAbortedCallbackBreaker = (): void => { + clearTimeout(abortedCallbackBreaker) + abortedCallbackBreaker = undefined + } const onAbort = (): void => { - // Why: closing the session is what releases callbacks and their borrowed source buffers. - void closeSession(signal.reason).catch(() => {}) + if (abortedCallbackBreaker || closePromise) { + return + } + // Why: normal cancellation gets a cleanup opportunity, but a retained raw callback needs close. + abortedCallbackBreaker = setTimeout(() => { + abortedCallbackBreaker = undefined + void closeSession(signal.reason).catch(() => {}) + }, ABORTED_CALLBACK_BREAKER_MS) } signal.addEventListener('abort', onAbort, { once: true }) if (signal.aborted) { @@ -239,6 +251,8 @@ export async function transferSshRelayRuntimeTreeViaSftp( signal.throwIfAborted() return Object.freeze({ remoteStagingRoot, ...result }) } catch (error) { + // The stream reached a settled callback boundary, so reverse cleanup can safely own shutdown. + clearAbortedCallbackBreaker() const cleanupFailures = ownedDirectories.length === 0 ? [] @@ -246,6 +260,12 @@ export async function transferSshRelayRuntimeTreeViaSftp( await closeSession(error).catch((closeError) => cleanupFailures.push(closeError)) throw joinedFailure(error, cleanupFailures) } finally { + clearAbortedCallbackBreaker() signal.removeEventListener('abort', onAbort) } } + +export const SSH_RELAY_RUNTIME_SFTP_TREE_TRANSFER_LIMITS = Object.freeze({ + maximumConcurrentFiles: MAXIMUM_CONCURRENT_FILES, + abortedCallbackBreakerMs: ABORTED_CALLBACK_BREAKER_MS +}) From a72c32f630712547b4b51ff9eda5e073ea010ca5 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 11:58:49 -0400 Subject: [PATCH 222/267] ci: unlock live SSH runner fixture Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 6 +++- .../ssh-relay-runtime-workflow.test.mjs | 2 ++ ...elease-implementation-checklist-summary.md | 7 ++++- ...github-release-implementation-checklist.md | 28 ++++++++++++++++++- 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 6287cd1da6c..4e4aecb241a 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -573,6 +573,10 @@ jobs: cp "$fixture/client-key.pub" "$HOME/.ssh/authorized_keys" chmod 600 "$HOME/.ssh/authorized_keys" "$fixture/client-key" "$fixture/host-key" sudo chown root:root "$fixture/host-key" + # Why: hosted runner accounts are shadow-locked, which sshd rejects before public-key auth. + random_password=$(openssl rand -hex 32) + sudo usermod --password "$(openssl passwd -6 "$random_password")" "$USER" + unset random_password port=22222 server_version=$(/usr/sbin/sshd -V 2>&1 | head -1) config="$fixture/sshd_config" @@ -599,7 +603,7 @@ jobs: break fi if test "$attempt" = 20; then - cat "$fixture/sshd.log" + sudo tail -200 "$fixture/sshd.log" exit 1 fi sleep 0.25 diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 97bf9a9a7aa..427b5e93b42 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -95,6 +95,8 @@ describe('SSH relay runtime artifact workflow', () => { expect(start.if).toBe("runner.os == 'Linux'") expect(start.run).toContain('ListenAddress 127.0.0.1') expect(start.run).toContain('Subsystem sftp internal-sftp') + expect(start.run).toContain('sudo usermod --password') + expect(start.run).toContain('PasswordAuthentication no') expect(start.run).toContain('timeout --signal=TERM --kill-after=15s 180s sudo apt-get') expect(measure.if).toBe("runner.os == 'Linux'") expect(measure.run).toContain('ssh-relay-runtime-sftp-openssh-full-size.test.ts') 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 1f38d7ffc10..f8475922b92 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 @@ -10,7 +10,12 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected live -OpenSSH SFTP composition is in progress, 2026-07-15, Codex implementation owner.** +OpenSSH SFTP runner fixture auth correction is in progress, 2026-07-15, Codex implementation owner.** +`E-M6-SFTP-LIVE-COMPOSITION-CI-RED-001` records both target-native Linux jobs passing build, +contracts, and existing full-size measurement before stock sshd rejected GitHub's shadow-locked +`runner` account ahead of public-key validation. No SFTP bytes were sent and no live cell is credited. +The narrow rerun keeps password authentication disabled and gives only the ephemeral account a fresh +random password hash so public-key authentication can proceed. `E-M6-SFTP-LIVE-COMPOSITION-AUDIT-001` limits this package to one authenticated built-in `SshConnection`, one raw SFTP channel, the existing one-to-four file transfer, captured-transport- only force close with awaited teardown, and exact full-size Linux x64/arm64 runtime transfer against 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 8df452fb05e..0228bafd963 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected authenticated-connection/SFTP-tree composition and live OpenSSH proof**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **LIVE SFTP COMPOSITION LOCALLY GREEN / EXACT-HEAD RUNNER PROOF NEXT — 2026-07-15, Codex implementation owner** — `E-M6-SFTP-LIVE-COMPOSITION-LOCAL-001` proves the disconnected authenticated-connection owner, captured-client force close, cancellation cleanup/breaker ordering, workflow oracle, focused/broad/release suites, typecheck, full lint, formatting, max-lines, diff, protected-resolver, and no-product-import gates locally. The purpose suite is 15/15; the live full-size suite is deliberately skipped locally because no runner OpenSSH/runtime inputs are present and therefore earns no live cell. Push this isolated package and require exact-head Linux x64/arm64 stock-OpenSSH full-size metrics before crediting live SFTP. Do not credit Windows/POSIX system SSH, high-RTT, `MaxSessions=1`, product/settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Session checkpoint: **LIVE SFTP RUNNER FIXTURE RED / NARROW AUTH-FIX IN PROGRESS — 2026-07-15, Codex implementation owner** — `E-M6-SFTP-LIVE-COMPOSITION-CI-RED-001` proves both target-native Linux builds and all pre-live contracts passed, then the loopback stock-OpenSSH fixture rejected GitHub's shadow-locked `runner` account before public-key validation. No SFTP bytes were sent and no live cell is credited. Keep password authentication disabled; assign only a fresh random ephemeral password hash so stock sshd accepts the runner account for public-key authentication, fix failure-log readability, rerun exact head, and require full metrics. Do not credit Windows/POSIX system SSH, high-RTT, `MaxSessions=1`, product/settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -17323,6 +17323,32 @@ src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts` — PASS, 15/15 in 1. settings/fallback wiring, tuple enablement, publication, or default behavior is proved. Legacy remains the sole production/default path and SignPath remains deferred. +### E-M6-SFTP-LIVE-COMPOSITION-CI-RED-001 — stock sshd rejects the hosted runner's locked account + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact implementation commit + `32f78d720febdb5581282cfbc64b6aae374930f4`, SSH Relay Runtime Artifacts run + [29513130758](https://github.com/stablyai/orca/actions/runs/29513130758). +- Passing boundary before RED: Linux x64 job + [87671358587](https://github.com/stablyai/orca/actions/runs/29513130758/job/87671358587) and Linux + arm64 job [87671358592](https://github.com/stablyai/orca/actions/runs/29513130758/job/87671358592) + both pass checkout, exact Node 24.18.0 setup, native identity, prerequisites, the complete runtime + contract suite including all five new connection-owner cases, exact Node input download, target- + native build/reproducibility/read-back/smoke, and full-size extraction/cache/scan/stream measurement. +- Expected evidence failure: both jobs then fail `Start loopback OpenSSH SFTP fixture`; the live + transfer step is skipped and sends zero SFTP bytes. The server binds only `127.0.0.1:22222`, but + every key-auth probe is rejected with `User runner not allowed because account is locked` before + public-key validation. The arm64 cleanup log confirms stock sshd accepted the config/listener and + rejected only the shadow account state. The x64 runner is Ubuntu 24.04.4 image + `20260714.240.1`, X64, kernel `6.17.0-1020-azure`; arm64 is the target-native hosted counterpart. +- Narrow correction: retain `PasswordAuthentication no`, `KbdInteractiveAuthentication no`, + loopback-only listen, one ephemeral Ed25519 client key, and strict modes. Generate a fresh random + 32-byte password, store only its SHA-512 shadow hash on the ephemeral runner account, immediately + unset the plaintext shell variable, and continue to authenticate only with the generated public + key. Also read root-owned sshd logs through `sudo` on failure. This changes only CI fixture account + eligibility; no product authentication, connection, transfer, or fallback code changes. +- Residual: no live SFTP transfer/filesystem/mode/RSS/cancellation evidence is credited from this + run. Exact-head Linux x64/arm64 rerun plus all-six/adjacent regression proof remains mandatory. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 4945645e81af971d8ba874d83ee5975abfd6e79d Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 12:23:07 -0400 Subject: [PATCH 223/267] test: sync hosted Windows launcher budget Co-authored-by: Orca --- .../build-windows-cli-launcher.test.mjs | 7 +- ...elease-implementation-checklist-summary.md | 16 ++-- ...github-release-implementation-checklist.md | 81 +++++++++++++++++-- src/main/ssh/ssh-remote-cli-launcher.test.ts | 7 +- 4 files changed, 97 insertions(+), 14 deletions(-) diff --git a/config/scripts/build-windows-cli-launcher.test.mjs b/config/scripts/build-windows-cli-launcher.test.mjs index 4af5a68dd5a..0d14e2373b6 100644 --- a/config/scripts/build-windows-cli-launcher.test.mjs +++ b/config/scripts/build-windows-cli-launcher.test.mjs @@ -4,9 +4,14 @@ import { dirname, join, resolve } from 'node:path' import { spawnSync } from 'node:child_process' import { describe, expect, it } from 'vitest' -const itWindows = process.platform === 'win32' ? it : it.skip const itCrossHost = process.platform === 'win32' ? it.skip : it const projectRoot = resolve(import.meta.dirname, '../..') +// Why: cold csc.exe startup exceeds Vitest's 5s unit budget on hosted Windows; +// keep the larger allowance scoped to the real compiler integration test. +function itWindows(name, test) { + const runner = process.platform === 'win32' ? it : it.skip + runner(name, { timeout: 15_000 }, test) +} describe('Windows CLI launcher', () => { itCrossHost('fails closed when the Windows launcher cannot be compiled on this host', () => { 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 f8475922b92..60ea300fde8 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 @@ -9,13 +9,15 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected live -OpenSSH SFTP runner fixture auth correction is in progress, 2026-07-15, Codex implementation owner.** -`E-M6-SFTP-LIVE-COMPOSITION-CI-RED-001` records both target-native Linux jobs passing build, -contracts, and existing full-size measurement before stock sshd rejected GitHub's shadow-locked -`runner` account ahead of public-key validation. No SFTP bytes were sent and no live cell is credited. -The narrow rerun keeps password authentication disabled and gives only the ephemeral account a fresh -random password hash so public-key authentication can proceed. +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — exact full-size Linux +x64/arm64 OpenSSH SFTP is green; repeated adjacent Windows compiler-test timeout correction is in +progress, 2026-07-15, Codex implementation owner.** `E-M6-SFTP-LIVE-COMPOSITION-CI-001` records +complete exact-tree/mode/exclusive-root/cancellation/RSS proof against stock OpenSSH on native Linux +x64 and arm64. `E-M6-SFTP-LIVE-COMPOSITION-ADJACENT-CI-RED-001` records the only adjacent failure: +the unchanged real Windows C# compiler integration case exceeded Vitest's five-second default twice. +Current main already fixes that pre-existing test under reviewed commit `45a772cb4` / PR #8897 by +scoping 15 seconds only to the real compiler cases. Apply that exact test-only patch and require a +fresh exact-head computer-use pass before starting disconnected POSIX system-SSH transfer. `E-M6-SFTP-LIVE-COMPOSITION-AUDIT-001` limits this package to one authenticated built-in `SshConnection`, one raw SFTP channel, the existing one-to-four file transfer, captured-transport- only force close with awaited teardown, and exact full-size Linux x64/arm64 runtime transfer against 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 0228bafd963..b438fffc14b 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected authenticated-connection/SFTP-tree composition and live OpenSSH proof**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **LIVE SFTP RUNNER FIXTURE RED / NARROW AUTH-FIX IN PROGRESS — 2026-07-15, Codex implementation owner** — `E-M6-SFTP-LIVE-COMPOSITION-CI-RED-001` proves both target-native Linux builds and all pre-live contracts passed, then the loopback stock-OpenSSH fixture rejected GitHub's shadow-locked `runner` account before public-key validation. No SFTP bytes were sent and no live cell is credited. Keep password authentication disabled; assign only a fresh random ephemeral password hash so stock sshd accepts the runner account for public-key authentication, fix failure-log readability, rerun exact head, and require full metrics. Do not credit Windows/POSIX system SSH, high-RTT, `MaxSessions=1`, product/settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: close exact-head live-SFTP adjacent regression evidence before starting disconnected POSIX system-SSH transfer**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **LIVE SFTP GREEN / REPEATED ADJACENT WINDOWS TIMEOUT DIAGNOSED — 2026-07-15, Codex implementation owner** — `E-M6-SFTP-LIVE-COMPOSITION-CI-001` proves exact full-size Linux x64/arm64 transfer through stock OpenSSH with complete tree/mode/exclusive-root/cancellation/RSS evidence. `E-M6-SFTP-LIVE-COMPOSITION-ADJACENT-CI-RED-001` records the only adjacent failure: the unchanged real Windows launcher compilation case exceeded Vitest's five-second default on both attempts. Current `main` independently fixed that exact pre-existing test in reviewed commit `45a772cb4` / PR #8897 by scoping 15 seconds only to the real compiler integration cases. Apply that exact test-only patch, rerun exact head, and do not credit Windows/POSIX system SSH, high-RTT, `MaxSessions=1`, product/settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -17349,6 +17349,75 @@ src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts` — PASS, 15/15 in 1. - Residual: no live SFTP transfer/filesystem/mode/RSS/cancellation evidence is credited from this run. Exact-head Linux x64/arm64 rerun plus all-six/adjacent regression proof remains mandatory. +### E-M6-SFTP-LIVE-COMPOSITION-CI-001 — exact full-size Linux x64/arm64 OpenSSH transfer passes + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact fixture-correction commit + `a72c32f630712547b4b51ff9eda5e073ea010ca5`, SSH Relay Runtime Artifacts run + [29513599328](https://github.com/stablyai/orca/actions/runs/29513599328). +- Native contract/build results: all six primary Node 24.18.0 jobs pass their runtime contract, + target-native build, reproducibility, read-back, bundled-Node/native-module/PTY/watcher smoke, and + evidence upload: Darwin arm64 + [87672984492](https://github.com/stablyai/orca/actions/runs/29513599328/job/87672984492), Darwin x64 + [87672984529](https://github.com/stablyai/orca/actions/runs/29513599328/job/87672984529), Windows x64 + [87672984544](https://github.com/stablyai/orca/actions/runs/29513599328/job/87672984544), Windows arm64 + [87672984568](https://github.com/stablyai/orca/actions/runs/29513599328/job/87672984568), Linux arm64 + [87672984606](https://github.com/stablyai/orca/actions/runs/29513599328/job/87672984606), and Linux x64 + [87672984630](https://github.com/stablyai/orca/actions/runs/29513599328/job/87672984630). Linux arm64 + supplement [87674335272](https://github.com/stablyai/orca/actions/runs/29513599328/job/87674335272), + Linux x64 supplement + [87674335444](https://github.com/stablyai/orca/actions/runs/29513599328/job/87674335444), and Windows + x64 floor [87675902858](https://github.com/stablyai/orca/actions/runs/29513599328/job/87675902858) + pass. Windows arm64 floor + [87675902918](https://github.com/stablyai/orca/actions/runs/29513599328/job/87675902918) completes the + exact 42-file / 85,213,511-byte runtime smoke in 5,871.70 ms and 48,713,728 incremental RSS bytes, + then retains only the declared hosted build-26200 versus required-26100 rejection. +- Linux x64 live cell: `ubuntu-24.04` / `ubuntu24` image `20260714.240.1`, X64, stock + `OpenSSH_9.6p1 Ubuntu-3ubuntu13.18`, public-key authentication only. Exact + `linux-x64-glibc` content `sha256:fc63ca342a5990f460ec6d72262a8542173dab20ce03c9b9cfb755b1c6057e6d`, + 34 files / 124,846,430 bytes. Serial transfer is 1,539.61 ms / 57,503,744 incremental RSS bytes; + four-file transfer is 1,420.82 ms / 5,488,640 bytes; cancellation settles in 9.99 ms / zero + incremental RSS bytes. +- Linux arm64 live cell: `ubuntu-24.04-arm` / `ubuntu24-arm64` image `20260714.61.1`, ARM64, the + same stock OpenSSH version and public-key-only policy. Exact `linux-arm64-glibc` content + `sha256:96f07f62af9b35304bb8ca0870ca4d8095e059bfa61dd1bc57e81b20f3fbca67`, 34 files / + 122,865,324 bytes. Serial transfer is 1,497.40 ms / 58,568,704 incremental RSS bytes; four-file + transfer is 1,385.27 ms / 5,984,256 bytes; cancellation settles in 9.13 ms / zero incremental RSS + bytes. +- Live assertions: each cell transfers the exact runtime built in that job; validates every path, + SHA-256, byte count, and POSIX mode; proves exclusive-root refusal preserves the first tree; + cleans an aborted owned stage with no later writes; and stays below the 80 MiB desktop transfer + ceiling. PR Checks run + [29513598690](https://github.com/stablyai/orca/actions/runs/29513598690) and Golden E2E run + [29513600476](https://github.com/stablyai/orca/actions/runs/29513600476) pass at the same SHA. +- Residual boundary: this closes only the two same-family Linux SFTP cells. Windows remote, + cross-client coverage, high RTT, `MaxSessions=1`, peak live channel/handle counts, fault injection, + POSIX/Windows system SSH, remote verification/install/publish/launch, and every product/Beta/ + settings/fallback/tuple/publication/default path remain open. The adjacent computer-use workflow + is separately RED below, so the package does not advance yet. Legacy remains the only production + path and SignPath remains deferred. + +### E-M6-SFTP-LIVE-COMPOSITION-ADJACENT-CI-RED-001 — repeated pre-existing Windows compiler timeout + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact commit `a72c32f630712547b4b51ff9eda5e073ea010ca5`, + computer-use run [29513600918](https://github.com/stablyai/orca/actions/runs/29513600918), first + Windows job [87672944323](https://github.com/stablyai/orca/actions/runs/29513600918/job/87672944323) + and same-SHA failed-job rerun + [87677479785](https://github.com/stablyai/orca/actions/runs/29513600918/job/87677479785). +- Result: both attempts pass 400 of 401 applicable tests with four declared skips, but + `src/main/ssh/ssh-remote-cli-launcher.test.ts:55` exceeds Vitest's unchanged five-second default + while synchronously invoking the real .NET Framework compiler. The first attempt lasts 11.76 + seconds overall; the rerun lasts 12.23 seconds. Neither the launcher source/test nor its imports + overlap this package. The same workflow's Ubuntu job passes, and the independent all-six native + artifact jobs plus PR Checks and Golden E2E pass. +- Independent diagnosis: current `origin/main` commit + `45a772cb42f8525ce0189fe63a509e562c5b6928` (`test(windows): allow native launcher compilation +time`, reviewed PR #8897) changes only the two real Windows compiler integration tests and scopes a + 15-second timeout to them because cold hosted `csc.exe` startup exceeds the unit default. It is + not an SFTP change and changes no production behavior. This feature branch predates that commit. +- Disposition: apply the exact reviewed test-only patch without broad timeout relaxation, then + require a fresh exact-head computer-use pass before advancing. No adjacent green evidence is + credited from either failed attempt. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -17407,9 +17476,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the locally green disconnected SFTP composition package, then require exact-head -target-native Linux x64/arm64 OpenSSH full-size evidence and all-six native/adjacent regression CI -before checking any Milestone 6 SFTP item or advancing. Do not add an Electron/startup/product +Apply current-main commit `45a772cb4`'s exact scoped Windows compiler-test timeout patch, commit and +push this evidence checkpoint, and require a fresh exact-head computer-use pass. Then audit the next +smallest disconnected POSIX system-SSH/no-tar transfer package before changing code. Do not check +the broad Milestone 6 SFTP item yet: Windows remote, cross-client, high-RTT, `MaxSessions=1`, live +channel/handle, and fault-injection cells remain open. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, diff --git a/src/main/ssh/ssh-remote-cli-launcher.test.ts b/src/main/ssh/ssh-remote-cli-launcher.test.ts index ae0a2b18619..c9f9c0865de 100644 --- a/src/main/ssh/ssh-remote-cli-launcher.test.ts +++ b/src/main/ssh/ssh-remote-cli-launcher.test.ts @@ -7,7 +7,12 @@ import { describe, expect, it } from 'vitest' import { createRemoteCliInstallPlan } from './ssh-remote-cli-launcher' import { getRemoteHostPlatform } from './ssh-remote-platform' -const itWindows = process.platform === 'win32' ? it : it.skip +// Why: cold csc.exe startup exceeds Vitest's 5s unit budget on hosted Windows; +// keep the larger allowance scoped to the real compiler integration test. +function itWindows(name: string, test: () => void): void { + const runner = process.platform === 'win32' ? it : it.skip + runner(name, { timeout: 15_000 }, test) +} function decodePowerShellCommand(command: string): string { const encoded = command.match(/-EncodedCommand\s+([A-Za-z0-9+/=]+)/)?.[1] From fac052b7714e2c93c5c84b41b784869d4b3fe125 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 12:42:49 -0400 Subject: [PATCH 224/267] feat(ssh): add POSIX relay file destination Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 24 +- ...github-release-implementation-checklist.md | 188 +++++++++- ...lay-runtime-posix-file-destination.test.ts | 325 ++++++++++++++++++ ...sh-relay-runtime-posix-file-destination.ts | 318 +++++++++++++++++ 6 files changed, 832 insertions(+), 26 deletions(-) create mode 100644 src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts create mode 100644 src/main/ssh/ssh-relay-runtime-posix-file-destination.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 4e4aecb241a..83d675d4ac0 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -286,6 +286,7 @@ jobs: src/main/ssh/ssh-relay-runtime-source-tree.test.ts \ src/main/ssh/ssh-relay-runtime-source-scan.test.ts \ src/main/ssh/ssh-relay-runtime-source-stream.test.ts \ + src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-session.test.ts \ @@ -931,6 +932,7 @@ jobs: src/main/ssh/ssh-relay-runtime-source-tree.test.ts ` src/main/ssh/ssh-relay-runtime-source-scan.test.ts ` src/main/ssh/ssh-relay-runtime-source-stream.test.ts ` + src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-session.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 427b5e93b42..673b8990b4d 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -164,6 +164,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-runtime-source-tree.test.ts', 'ssh-relay-runtime-source-scan.test.ts', 'ssh-relay-runtime-source-stream.test.ts', + 'ssh-relay-runtime-posix-file-destination.test.ts', 'ssh-relay-runtime-sftp-file-destination.test.ts', 'ssh-relay-runtime-sftp-connection-transfer.test.ts', 'ssh-relay-runtime-sftp-session.test.ts', 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 60ea300fde8..a5c86899c41 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 @@ -9,15 +9,21 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — exact full-size Linux -x64/arm64 OpenSSH SFTP is green; repeated adjacent Windows compiler-test timeout correction is in -progress, 2026-07-15, Codex implementation owner.** `E-M6-SFTP-LIVE-COMPOSITION-CI-001` records -complete exact-tree/mode/exclusive-root/cancellation/RSS proof against stock OpenSSH on native Linux -x64 and arm64. `E-M6-SFTP-LIVE-COMPOSITION-ADJACENT-CI-RED-001` records the only adjacent failure: -the unchanged real Windows C# compiler integration case exceeded Vitest's five-second default twice. -Current main already fixes that pre-existing test under reviewed commit `45a772cb4` / PR #8897 by -scoping 15 seconds only to the real compiler cases. Apply that exact test-only patch and require a -fresh exact-head computer-use pass before starting disconnected POSIX system-SSH transfer. +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected POSIX +system-SSH/no-tar per-file boundary is locally green and exact-head CI is next, 2026-07-15, Codex +implementation owner.** `E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001` records 25/25 purpose cases, 87 focused +cases with one declared skip, 628 broad relay cases with six declared skips, 281/281 release +contracts, typecheck, full lint, formatting, max-lines, diff, protected-resolver, and no-product- +consumer gates. The command uses only the four declared POSIX primitives and cancellation is bounded +to a 250 ms graceful / 2,000 ms total ceiling. Exact-head all-six native and adjacent Actions proof +is required before the next package. +`E-M6-SFTP-LIVE-COMPOSITION-CI-001` records complete exact-tree/mode/exclusive-root/cancellation/RSS +proof against stock OpenSSH on native Linux x64 and arm64. The repeated unrelated Windows compiler +timeout is closed at exact head `4945645e8` by computer-use run `29515352845` under +`E-M6-SFTP-LIVE-COMPOSITION-ADJACENT-CI-001`. `E-M6-POSIX-SYSTEM-SSH-FILE-AUDIT-001` limits the next +slice to one injected command channel and one exclusively created manifest file with bounded chunk, +EOF, mode, cancellation, and forced-settlement contracts. No real SSH connection, remote tree, +semantic probe, live/full-size claim, product path, mode/fallback, or default behavior is included. `E-M6-SFTP-LIVE-COMPOSITION-AUDIT-001` limits this package to one authenticated built-in `SshConnection`, one raw SFTP channel, the existing one-to-four file transfer, captured-transport- only force close with awaited teardown, and exact full-size Linux x64/arm64 runtime transfer against 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 b438fffc14b..72d7b245f9d 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: close exact-head live-SFTP adjacent regression evidence before starting disconnected POSIX system-SSH transfer**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **LIVE SFTP GREEN / REPEATED ADJACENT WINDOWS TIMEOUT DIAGNOSED — 2026-07-15, Codex implementation owner** — `E-M6-SFTP-LIVE-COMPOSITION-CI-001` proves exact full-size Linux x64/arm64 transfer through stock OpenSSH with complete tree/mode/exclusive-root/cancellation/RSS evidence. `E-M6-SFTP-LIVE-COMPOSITION-ADJACENT-CI-RED-001` records the only adjacent failure: the unchanged real Windows launcher compilation case exceeded Vitest's five-second default on both attempts. Current `main` independently fixed that exact pre-existing test in reviewed commit `45a772cb4` / PR #8897 by scoping 15 seconds only to the real compiler integration cases. Apply that exact test-only patch, rerun exact head, and do not credit Windows/POSIX system SSH, high-RTT, `MaxSessions=1`, product/settings/fallback wiring, tuple enablement, publication, or default behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected POSIX system-SSH/no-tar per-file command and destination contract**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **POSIX NO-TAR FILE BOUNDARY LOCALLY GREEN / EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001` proves the isolated destination across 25 purpose cases, 628 broad relay cases, 281 release contracts, typecheck, lint, formatting, max-lines, diff, protected-resolver, and no-product-consumer gates. The module still accepts only an injected command channel and has no real SSH/tree/product caller, so exact-head all-six native and adjacent Actions evidence is required before the next package. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -150,14 +150,12 @@ same change as the work it records. - Completed package: disconnected full host-evidence composition is green locally and on every primary native client under E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001 and E-M5-HOST-EVIDENCE-COMPOSITION-CI-001. It has no product caller. -- Active package: **In progress — 2026-07-15, Codex implementation owner** — audit, RED, and local - GREEN complete under E-M6-SOURCE-STREAM-AUDIT-001, E-M6-SOURCE-STREAM-LOCAL-RED-001, and - E-M6-SOURCE-STREAM-LOCAL-001. E-M6-SOURCE-STREAM-CI-RED-001 exposes a Windows-only test-oracle - mismatch; its POSIX-only correction is locally green under - E-M6-SOURCE-STREAM-WINDOWS-ORACLE-CORRECTION-LOCAL-001. Commit/push it, then require fresh - exact-head all-six native/full-size and adjacent CI. Do not import SSH, open channels, construct - remote paths, create remote staging, or add product/settings/fallback/tuple/publication/default - behavior. +- Active package: **Local implementation complete / exact-head CI pending — 2026-07-15, Codex + implementation owner** — the disconnected POSIX no-tar per-file command/destination boundary is + green under E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001. It consumes the existing source-stream + destination interface and an injected command-channel factory only. Do not import or modify the + legacy tar/base64 directory uploader, wire `SshConnection`, create a remote tree, add a semantic + probe, or add product/settings/fallback/tuple/publication/default behavior. - 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 @@ -17410,14 +17408,171 @@ src/main/ssh/ssh-relay-runtime-sftp-tree-transfer.test.ts` — PASS, 15/15 in 1. overlap this package. The same workflow's Ubuntu job passes, and the independent all-six native artifact jobs plus PR Checks and Golden E2E pass. - Independent diagnosis: current `origin/main` commit - `45a772cb42f8525ce0189fe63a509e562c5b6928` (`test(windows): allow native launcher compilation -time`, reviewed PR #8897) changes only the two real Windows compiler integration tests and scopes a + `45a772cb42f8525ce0189fe63a509e562c5b6928` + (`test(windows): allow native launcher compilation time`, reviewed PR #8897) changes only the two + real Windows compiler integration tests and scopes a 15-second timeout to them because cold hosted `csc.exe` startup exceeds the unit default. It is not an SFTP change and changes no production behavior. This feature branch predates that commit. - Disposition: apply the exact reviewed test-only patch without broad timeout relaxation, then require a fresh exact-head computer-use pass before advancing. No adjacent green evidence is credited from either failed attempt. +### E-M6-SFTP-LIVE-COMPOSITION-ADJACENT-CI-001 — scoped upstream test budget closes exact-head adjacent gate + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact evidence/test-only commit + `4945645e81af971d8ba874d83ee5975abfd6e79d`, computer-use run + [29515352845](https://github.com/stablyai/orca/actions/runs/29515352845). +- Windows result: job + [87678882484](https://github.com/stablyai/orca/actions/runs/29515352845/job/87678882484) passes the + 401 applicable tests with four declared skips, including both real Windows C# compiler integration + cases under their exact reviewed 15-second budgets. It then passes native verification, CLI and + packaged Electron builds, daemon boot, Windows daemon workspace-close repro, and Windows + computer-use E2E in 4 minutes 38 seconds. No product source or timeout outside the two compiler + cases changed. +- Control result: Ubuntu job + [87678882558](https://github.com/stablyai/orca/actions/runs/29515352845/job/87678882558) passes its + unit/native/build/daemon/Linux E2E path in 2 minutes 49 seconds. Local macOS runs passed three + applicable launcher cases with the two Windows cases declared skipped plus the eight-case relay + workflow oracle; formatting and diff gates passed before commit. +- Disposition: the repeated adjacent RED is resolved by the exact independently reviewed upstream + test-only correction. Prior live SFTP evidence remains anchored to `a72c32f63`; this commit changes + no SFTP/runtime/product code. The separately triggered all-six artifact rerun is redundant control + evidence and may continue while the next disconnected package begins. + +### E-M6-SFTP-LIVE-COMPOSITION-CONTROL-CI-001 — redundant exact-head control classified + +- Date/owner/head: 2026-07-15, Codex implementation owner; exact test/docs-only head + `4945645e81af971d8ba874d83ee5975abfd6e79d` after the adjacent correction above. +- Runtime artifact run: [29515352928](https://github.com/stablyai/orca/actions/runs/29515352928). + All six primary native jobs pass: Windows arm64 [87678883773](https://github.com/stablyai/orca/actions/runs/29515352928/job/87678883773), + Windows x64 [87678883819](https://github.com/stablyai/orca/actions/runs/29515352928/job/87678883819), + macOS arm64 [87678883853](https://github.com/stablyai/orca/actions/runs/29515352928/job/87678883853), + Linux arm64 [87678883887](https://github.com/stablyai/orca/actions/runs/29515352928/job/87678883887), + Linux x64 [87678883950](https://github.com/stablyai/orca/actions/runs/29515352928/job/87678883950), + and macOS x64 [87678883952](https://github.com/stablyai/orca/actions/runs/29515352928/job/87678883952). + Both Linux oldest-userland supplements pass in jobs 87680141129 and 87680141145, as does the + Windows x64 floor in job 87681071541. +- Declared residual: only Windows arm64 floor job + [87681071534](https://github.com/stablyai/orca/actions/runs/29515352928/job/87681071534) fails after + complete runtime smoke because the hosted runner reports build 26200 while the immutable contract + requires build 26100; platform and architecture pass, `osBuild` fails. This is the already recorded + runner gap, not a test-only correction or SFTP regression. +- Adjacent controls: PR Checks run + [29515352911](https://github.com/stablyai/orca/actions/runs/29515352911) passes lint, typecheck, Git + compatibility, tests, unpacked app build, and packaged CLI smoke. Golden E2E run + [29515352860](https://github.com/stablyai/orca/actions/runs/29515352860) passes both macOS job + 87678882828 and Linux job 87678882845. +- Disposition: this closes classification of the redundant control run only. Live SFTP evidence + remains anchored to `a72c32f63`; no POSIX system-SSH claim or later-package evidence is inferred. + +### E-M6-POSIX-SYSTEM-SSH-FILE-AUDIT-001 — isolated no-tar file channel boundary + +- Date/owner/base: 2026-07-15, Codex implementation owner; clean exact base + `4945645e81af971d8ba874d83ee5975abfd6e79d` after the adjacent gate above. +- Existing-code audit: `system-ssh-file-transfer.ts` is the proven legacy/default uploader and uses + local plus remote tar on POSIX and a whole-tree JSON/base64 buffer on Windows. Those paths remain + available but violate the new runtime bootstrap contract and must not be reused, narrowed, or + modified in this slice. `ssh-relay-runtime-source-stream.ts` already owns the exact scanned source, + one 64 KiB buffer per worker, hash/size/snapshot checks, and joined destination cleanup. +- New boundary: add one purpose-named POSIX file-destination module that implements only + `SshRelayRuntimeSourceDestination`. It receives an absolute POSIX manifest path, authenticated + mode (`0644` or `0755`), the exact caller signal, and an injected command-channel factory. The + generated POSIX `sh` command uses only `umask`, `set -C`, byte-preserving `cat`, and `chmod`; it + shell-quotes the path, exclusively creates the file with mode `0600` during transfer, and applies + the authenticated final mode only after stdin EOF and successful `cat` settlement. It never uses + Node, Python, Perl, tar, base64, a checksum tool, a whole-file buffer, or a remote download. +- Lifecycle contract: one write may be active; each write resolves only after the channel accepts + the exact non-empty chunk so the source worker may safely reuse its buffer. Close sends EOF once + and awaits a successful remote exit before declaring completion. Abort is idempotent, settles any + active write, requests graceful channel termination, escalates after a short fixed grace to forced + termination, and fails closed if the bounded total settlement window expires. A later tree owner, + not this file boundary, owns removal of an incomplete proved-owned path after the channel settles. +- Layer-A proof: purpose tests must cover hostile/relative/newline/NUL paths, invalid modes, exact + shell quoting and forbidden-command absence, exclusive/restrictive/final-mode command order, + zero-byte EOF, multi-chunk buffer callback order, concurrent-operation rejection, remote nonzero + exit, pre/mid-write/mid-close cancellation, graceful and forced settlement, final timeout, cleanup + failure propagation, idempotent abort, and no write after cancellation. Pin the purpose suite once + in both native workflow command families before implementation. +- Explicit residual boundary: no real SSH process or connection, directory/tree staging, primitive + semantic probe, `MaxSessions=1`, one-versus-four policy, tar optimization, full-size/live transfer, + bundled-Node remote verification/install/publish/launch, product/Beta/settings/fallback/tuple/ + publication/default behavior, Windows PowerShell path, or SignPath is included. Legacy remains the + sole production/default path. + +### E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-RED-001 — missing production boundary fails as designed + +- Date/owner/base: 2026-07-15, Codex implementation owner; exact uncommitted test/workflow state on + base `4945645e81af971d8ba874d83ee5975abfd6e79d` after + `E-M6-POSIX-SYSTEM-SSH-FILE-AUDIT-001`. +- Command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs`. +- Result: expected RED, exit 1. The new purpose suite collects zero cases and fails solely with + `Cannot find module './ssh-relay-runtime-posix-file-destination'`; that production module does not + exist yet. The independent release-workflow oracle passes all 8/8 cases, proving the purpose suite + appears exactly once in each native command family before implementation. +- Resource evidence: Vitest duration 671 ms; `/usr/bin/time -l` reports 1.86 seconds real, + 132,153,344 bytes maximum resident set size, zero swaps, and zero block input/output operations. +- Disposition: RED is causally narrow and accepted. Implement only the audited injected-channel + destination, then require purpose GREEN plus focused, broad, static, isolation, and exact-head + native workflow evidence. No live SSH, tree transfer, product path, or default-path claim follows + from this evidence. + +### E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-TYPE-RED-001 — test-only type boundary corrected + +- Date/owner/state: 2026-07-15, Codex implementation owner; first static pass on the uncommitted + implementation atop `4945645e81af971d8ba874d83ee5975abfd6e79d`. +- Command/result: `/usr/bin/time -l pnpm typecheck` — expected gate RED after runtime tests were + already green, exit 1 in 3.23 seconds real with 1,032,896,512-byte maximum RSS. TypeScript reports + only three purpose-test errors: the intentionally invalid numeric mode crosses the production + literal union, and two zero-argument `vi.fn` channel factories expose no command tuple to the + assertions. No production line fails typechecking. +- Correction: constrain the deliberate runtime-invalid mode at the test call boundary and declare + the two mock command/signal parameters. The 25-case purpose suite and repeated typecheck below pass + after that test-only correction. No product, transport, or runtime behavior changed. + +### E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001 — isolated file channel boundary is locally green + +- Date/owner/environment/base: 2026-07-15, Codex implementation owner; local macOS arm64, Node + 26.0.0, pnpm 10.24.0, uncommitted package atop exact base + `4945645e81af971d8ba874d83ee5975abfd6e79d`. +- Implementation: the 318-line purpose-named module accepts one absolute normalized POSIX manifest + path, authenticated `0644`/`0755`, exact signal, and injected channel factory. Its exact command + uses only `umask 077`, `set -C`, byte-preserving `cat`, and `chmod`; single-quote escaping is + covered. It awaits every non-empty write callback, sends EOF once, requires successful remote + settlement, and bounds cancellation to a 250 ms graceful window plus a 2,000 ms total ceiling with + force-close escalation. Cancellation never sends EOF and does not release an active retained + chunk until channel settlement or the fail-closed ceiling. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts` — PASS, 25/25 in + 275 ms Vitest / 1.31 seconds real, 132,743,168-byte maximum RSS, zero swaps, and zero block I/O. + Cases cover both final modes; exact/forbidden command content; hostile paths; empty/write-error/ + retained chunks; concurrent operations; zero-byte EOF; nonzero remote exit; pre-open, mid-open, + mid-write, and mid-close cancellation; graceful/forced/timeout settlement; cleanup failure; and + idempotent abort/no later EOF or write. +- Focused command: purpose, source stream/scan/tree, and native-workflow oracle — PASS, five files, + 87 cases plus one declared platform skip in 7.49 seconds Vitest / 8.98 seconds real with + 144,556,032-byte maximum RSS. The workflow oracle passes 8/8 and pins the suite exactly once in + both POSIX and PowerShell native command families. +- Broad commands: `src/main/ssh/ssh-relay-*.test.ts` — PASS, 47 files plus four declared skipped, + 628 cases plus six declared skips in 25.04 seconds Vitest / 26.50 seconds real with + 244,596,736-byte maximum RSS; `config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files and + 281/281 cases in 22.34 seconds Vitest / 24.48 seconds real with 190,545,920-byte maximum RSS. Only + the established stale-lock diagnostic and local Node 26 module deprecation appear. +- Static/isolation commands: repeated `/usr/bin/time -l pnpm typecheck` passes in 3.32 seconds real + with 1,241,726,976-byte maximum RSS. Targeted `oxlint`, targeted `oxfmt --check`, standalone + max-lines ratchet, `git diff --check`, protected-resolver empty diff, and no non-test consumer pass. + Full `pnpm lint` passes in 18.10 seconds real with 2,052,751,360-byte maximum RSS: switch + exhaustiveness, 41 reliability gates, the 355-entry max-lines ratchet, bundled-skill verification, + 9,837 localization references, locale parity, and zero-candidate localization coverage pass; only + pre-existing unrelated warnings remain. Module/test sizes are 318/325 lines with no bypass. +- Residual: this is an injected callback-channel contract, not a real SSH process, `SshConnection` + adapter, tree/root staging owner, semantic primitive probe, tar optimization, `MaxSessions=1`, + full-size/live transfer, or Windows PowerShell path. It has no non-test consumer. Remote bundled- + Node verification/install/publish/launch, product/Beta/fallback/tuple/publication/default behavior, + and SignPath remain absent; legacy remains the sole production/default path. Exact-head all-six + native and adjacent Actions evidence is required before the next bounded package. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -17476,11 +17631,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Apply current-main commit `45a772cb4`'s exact scoped Windows compiler-test timeout patch, commit and -push this evidence checkpoint, and require a fresh exact-head computer-use pass. Then audit the next -smallest disconnected POSIX system-SSH/no-tar transfer package before changing code. Do not check -the broad Milestone 6 SFTP item yet: Windows remote, cross-client, high-RTT, `MaxSessions=1`, live -channel/handle, and fault-injection cells remain open. Do not add an Electron/startup/product +Commit and push the locally green isolated POSIX no-tar per-file boundary, then require exact-head +all-six native artifact jobs, both Linux supplements, the Windows x64 floor, PR Checks, Golden E2E, +and computer-use evidence before auditing the next system-SSH adapter/tree slice. Do not +check the broad Milestone 6 SFTP or POSIX system-SSH items yet. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, diff --git a/src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts b/src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts new file mode 100644 index 00000000000..5613151840c --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts @@ -0,0 +1,325 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { + openSshRelayRuntimePosixFileDestination, + SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS, + type SshRelayRuntimePosixFileChannel +} from './ssh-relay-runtime-posix-file-destination' + +type Callback = (error?: Error) => void + +function createChannel(): { + channel: SshRelayRuntimePosixFileChannel + resolve: () => void + reject: (error: Error) => void +} { + let resolveSettled: () => void = () => {} + let rejectSettled: (error: Error) => void = () => {} + const settled = new Promise((resolve, reject) => { + resolveSettled = resolve + rejectSettled = reject + }) + const channel: SshRelayRuntimePosixFileChannel = { + write: vi.fn((_chunk: Buffer, callback: Callback) => callback()), + end: vi.fn(), + settled, + requestClose: vi.fn(), + forceClose: vi.fn() + } + return { channel, resolve: resolveSettled, reject: rejectSettled } +} + +function openDestination( + channel: SshRelayRuntimePosixFileChannel, + options: { + remotePath?: string + mode?: number + signal?: AbortSignal + openChannel?: (command: string, signal: AbortSignal) => Promise + } = {} +) { + return openSshRelayRuntimePosixFileDestination({ + remotePath: options.remotePath ?? "/owned stage/bin/no'de", + mode: (options.mode ?? 0o755) as 0o644 | 0o755, + signal: options.signal ?? new AbortController().signal, + openChannel: options.openChannel ?? vi.fn(async () => channel) + }) +} + +afterEach(() => { + vi.useRealTimers() +}) + +describe('SSH relay runtime POSIX no-tar file destination', () => { + it('builds one exclusive restrictive command with exact quoting and final mode order', async () => { + const { channel, resolve } = createChannel() + const controller = new AbortController() + const openChannel = vi.fn(async (_command: string, _signal: AbortSignal) => channel) + const destination = await openDestination(channel, { + signal: controller.signal, + openChannel + }) + + expect(openChannel).toHaveBeenCalledWith( + "umask 077; set -C; cat > '/owned stage/bin/no'\\''de' && chmod 0755 '/owned stage/bin/no'\\''de'", + controller.signal + ) + const command = openChannel.mock.calls[0]?.[0] ?? '' + for (const forbidden of ['node ', 'python', 'perl', 'tar ', 'base64', 'sha256sum', 'shasum']) { + expect(command.toLowerCase()).not.toContain(forbidden) + } + + const closing = destination.close() + expect(channel.end).toHaveBeenCalledOnce() + resolve() + await closing + }) + + it('authenticates a non-executable final mode in the same command', async () => { + const { channel, resolve } = createChannel() + const openChannel = vi.fn(async (_command: string, _signal: AbortSignal) => channel) + const destination = await openDestination(channel, { + remotePath: '/owned-stage/目录/manifest.json', + mode: 0o644, + openChannel + }) + + expect(openChannel.mock.calls[0]?.[0]).toBe( + "umask 077; set -C; cat > '/owned-stage/目录/manifest.json' && chmod 0644 '/owned-stage/目录/manifest.json'" + ) + const closing = destination.close() + resolve() + await closing + }) + + it.each([ + ['', 0o755], + ['relative/file', 0o755], + ['/', 0o755], + ['/owned//file', 0o755], + ['/owned/../file', 0o755], + ['/owned/./file', 0o755], + ['/owned/file/', 0o755], + ['/owned/file\nnext', 0o755], + ['/owned/file\0next', 0o755], + ['/owned/file', 0o600] + ])('rejects hostile path %j or mode %d before opening a channel', async (remotePath, mode) => { + const { channel } = createChannel() + const openChannel = vi.fn(async () => channel) + + await expect(openDestination(channel, { remotePath, mode, openChannel })).rejects.toThrow( + /invalid/i + ) + expect(openChannel).not.toHaveBeenCalled() + }) + + it('sends EOF once for a zero-byte file and awaits remote settlement', async () => { + const { channel, resolve } = createChannel() + const destination = await openDestination(channel) + let settled = false + const closing = destination.close().then(() => { + settled = true + }) + + await Promise.resolve() + expect(channel.end).toHaveBeenCalledOnce() + expect(settled).toBe(false) + resolve() + await closing + await expect(destination.close()).rejects.toThrow(/closed/i) + expect(channel.end).toHaveBeenCalledOnce() + }) + + it('awaits exact chunk callbacks and rejects concurrent operations', async () => { + const { channel, resolve } = createChannel() + let firstCallback: Callback | undefined + vi.mocked(channel.write).mockImplementationOnce((_chunk, callback) => { + firstCallback = callback + }) + const destination = await openDestination(channel) + const first = Buffer.from('first') + let settled = false + const writing = destination.write(first).then(() => { + settled = true + }) + + await Promise.resolve() + expect(channel.write).toHaveBeenCalledWith(first, expect.any(Function)) + expect(settled).toBe(false) + await expect(destination.write(Buffer.from('second'))).rejects.toThrow(/concurrent/i) + await expect(destination.close()).rejects.toThrow(/concurrent/i) + firstCallback?.() + await writing + await destination.write(Buffer.from('second')) + const closing = destination.close() + resolve() + await closing + }) + + it('rejects empty chunks and propagates channel write failures', async () => { + const { channel, resolve } = createChannel() + vi.mocked(channel.write).mockImplementationOnce((_chunk, callback) => { + callback(new Error('channel write failed')) + }) + const destination = await openDestination(channel) + + await expect(destination.write(Buffer.alloc(0))).rejects.toThrow(/empty/i) + await expect(destination.write(Buffer.from('payload'))).rejects.toThrow('channel write failed') + const closing = destination.close() + resolve() + await closing + }) + + it('propagates remote nonzero settlement on close', async () => { + const { channel, reject } = createChannel() + const destination = await openDestination(channel) + const closing = destination.close() + reject(new Error('remote cat failed (exit 1)')) + + await expect(closing).rejects.toThrow('remote cat failed') + }) + + it('rejects pre-open cancellation without opening a channel', async () => { + const { channel } = createChannel() + const controller = new AbortController() + const reason = new Error('cancelled before open') + controller.abort(reason) + const openChannel = vi.fn(async () => channel) + + await expect(openDestination(channel, { signal: controller.signal, openChannel })).rejects.toBe( + reason + ) + expect(openChannel).not.toHaveBeenCalled() + }) + + it('settles a channel returned after mid-open cancellation', async () => { + const { channel, resolve } = createChannel() + const controller = new AbortController() + const reason = new Error('cancelled during open') + vi.mocked(channel.requestClose).mockImplementation(() => resolve()) + const openChannel = vi.fn(async () => { + controller.abort(reason) + return channel + }) + + await expect(openDestination(channel, { signal: controller.signal, openChannel })).rejects.toBe( + reason + ) + expect(channel.requestClose).toHaveBeenCalledOnce() + expect(channel.forceClose).not.toHaveBeenCalled() + }) + + it('holds a retained write buffer until cancellation settles the channel', async () => { + const { channel, resolve } = createChannel() + const controller = new AbortController() + const reason = new Error('cancelled during write') + vi.mocked(channel.write).mockImplementation(() => {}) + vi.mocked(channel.requestClose).mockImplementation(() => resolve()) + const destination = await openDestination(channel, { signal: controller.signal }) + const chunk = Buffer.alloc(64 * 1024, 7) + let settled = false + const writing = destination.write(chunk).finally(() => { + settled = true + }) + + controller.abort(reason) + await Promise.resolve() + expect(channel.requestClose).toHaveBeenCalledOnce() + await expect(writing).rejects.toBe(reason) + expect(settled).toBe(true) + await expect(destination.abort(reason)).resolves.toBeUndefined() + await expect(destination.write(Buffer.from('later'))).rejects.toThrow(/closed/i) + expect(channel.write).toHaveBeenCalledOnce() + }) + + it('cancels a close only after the command channel settles', async () => { + const { channel, resolve } = createChannel() + const controller = new AbortController() + const reason = new Error('cancelled during close') + vi.mocked(channel.requestClose).mockImplementation(() => resolve()) + const destination = await openDestination(channel, { signal: controller.signal }) + const closing = destination.close() + + controller.abort(reason) + await expect(destination.abort(reason)).resolves.toBeUndefined() + await expect(closing).rejects.toBe(reason) + expect(channel.end).toHaveBeenCalledOnce() + expect(channel.requestClose).toHaveBeenCalledOnce() + }) + + it('settles ordinary cancellation during the graceful window', async () => { + const { channel, resolve } = createChannel() + const controller = new AbortController() + vi.mocked(channel.requestClose).mockImplementation(() => resolve()) + const destination = await openDestination(channel, { signal: controller.signal }) + + controller.abort(new Error('cancelled')) + await expect(destination.abort(controller.signal.reason)).resolves.toBeUndefined() + expect(channel.requestClose).toHaveBeenCalledOnce() + expect(channel.forceClose).not.toHaveBeenCalled() + }) + + it('forces a channel that does not settle during the graceful window', async () => { + vi.useFakeTimers() + const { channel, resolve } = createChannel() + const controller = new AbortController() + vi.mocked(channel.forceClose).mockImplementation(() => resolve()) + const destination = await openDestination(channel, { signal: controller.signal }) + + controller.abort(new Error('cancelled')) + const aborting = destination.abort(controller.signal.reason) + await vi.advanceTimersByTimeAsync( + SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS.gracefulCloseMs + ) + await expect(aborting).resolves.toBeUndefined() + expect(channel.requestClose).toHaveBeenCalledOnce() + expect(channel.forceClose).toHaveBeenCalledOnce() + }) + + it('fails closed when forced channel settlement reaches the total ceiling', async () => { + vi.useFakeTimers() + const { channel } = createChannel() + const controller = new AbortController() + const destination = await openDestination(channel, { signal: controller.signal }) + + controller.abort(new Error('cancelled')) + const aborting = destination.abort(controller.signal.reason) + await vi.advanceTimersByTimeAsync(SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS.totalCloseMs) + + await expect(aborting).rejects.toThrow(/settlement timed out/i) + expect(channel.requestClose).toHaveBeenCalledOnce() + expect(channel.forceClose).toHaveBeenCalledOnce() + }) + + it('propagates cleanup request failures after the channel settles', async () => { + const { channel, resolve } = createChannel() + const controller = new AbortController() + vi.mocked(channel.requestClose).mockImplementation(() => { + resolve() + throw new Error('graceful close failed') + }) + const destination = await openDestination(channel, { signal: controller.signal }) + + controller.abort(new Error('cancelled')) + await expect(destination.abort(controller.signal.reason)).rejects.toThrow( + 'graceful close failed' + ) + }) + + it('makes abort idempotent and never sends EOF after cancellation', async () => { + const { channel, resolve } = createChannel() + const controller = new AbortController() + vi.mocked(channel.requestClose).mockImplementation(() => resolve()) + const destination = await openDestination(channel, { signal: controller.signal }) + const reason = new Error('cancelled') + + controller.abort(reason) + const first = destination.abort(reason) + const second = destination.abort(reason) + expect(first).toBe(second) + await first + expect(channel.requestClose).toHaveBeenCalledOnce() + expect(channel.end).not.toHaveBeenCalled() + await expect(destination.close()).rejects.toThrow(/closed/i) + }) +}) diff --git a/src/main/ssh/ssh-relay-runtime-posix-file-destination.ts b/src/main/ssh/ssh-relay-runtime-posix-file-destination.ts new file mode 100644 index 00000000000..1bf7858351b --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-posix-file-destination.ts @@ -0,0 +1,318 @@ +import type { SshRelayRuntimeSourceDestination } from './ssh-relay-runtime-source-stream' + +type ChannelWriteCallback = (error?: Error) => void + +export type SshRelayRuntimePosixFileChannel = Readonly<{ + write: (chunk: Buffer, callback: ChannelWriteCallback) => void + end: () => void + settled: Promise + requestClose: () => void + forceClose: () => void +}> + +export type OpenSshRelayRuntimePosixFileDestinationOptions = Readonly<{ + remotePath: string + mode: 0o644 | 0o755 + signal: AbortSignal + openChannel: (command: string, signal: AbortSignal) => Promise +}> + +export const SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS = Object.freeze({ + gracefulCloseMs: 250, + totalCloseMs: 2_000 +}) + +type DestinationState = 'open' | 'closing' | 'complete' | 'aborting' | 'aborted' + +type ChannelSettlement = Readonly<{ ok: true } | { ok: false; error: unknown }> + +type AbortOutcome = Readonly<{ reason: unknown }> + +function validateRemotePath(remotePath: string): void { + const segments = remotePath.split('/') + if ( + remotePath === '/' || + !remotePath.startsWith('/') || + remotePath.includes('\0') || + remotePath.includes('\n') || + remotePath.includes('\r') || + segments.slice(1).some((segment) => segment === '' || segment === '.' || segment === '..') + ) { + throw new Error('SSH relay runtime POSIX file destination path is invalid') + } +} + +function validateOptions(options: OpenSshRelayRuntimePosixFileDestinationOptions): void { + if ( + !options || + typeof options.remotePath !== 'string' || + typeof options.openChannel !== 'function' || + !options.signal + ) { + throw new Error('SSH relay runtime POSIX file destination input is invalid') + } + validateRemotePath(options.remotePath) + if (options.mode !== 0o644 && options.mode !== 0o755) { + throw new Error('SSH relay runtime POSIX file destination mode is invalid') + } +} + +function validateChannel(channel: SshRelayRuntimePosixFileChannel): void { + if ( + !channel || + typeof channel.write !== 'function' || + typeof channel.end !== 'function' || + typeof channel.settled?.then !== 'function' || + typeof channel.requestClose !== 'function' || + typeof channel.forceClose !== 'function' + ) { + throw new Error('SSH relay runtime POSIX file channel is invalid') + } +} + +function quotePosixShellArgument(value: string): string { + return `'${value.replaceAll("'", "'\\''")}'` +} + +function buildCommand(remotePath: string, mode: 0o644 | 0o755): string { + const quotedPath = quotePosixShellArgument(remotePath) + const finalMode = mode.toString(8).padStart(4, '0') + // Why: noclobber plus umask makes authenticated staging exclusive and non-readable until EOF. + return `umask 077; set -C; cat > ${quotedPath} && chmod ${finalMode} ${quotedPath}` +} + +function cleanupFailure(failures: readonly unknown[]): unknown | undefined { + if (failures.length === 0) { + return undefined + } + return failures.length === 1 + ? failures[0] + : new AggregateError(failures, 'SSH relay runtime POSIX file channel cleanup failed') +} + +function waitForSettlement( + settlement: Promise, + timeoutMs: number +): Promise { + return new Promise((resolve) => { + const timeout = setTimeout(() => resolve(false), timeoutMs) + void settlement.then(() => { + clearTimeout(timeout) + resolve(true) + }) + }) +} + +async function settleCancelledChannel( + channel: SshRelayRuntimePosixFileChannel, + settlement: Promise +): Promise { + const failures: unknown[] = [] + try { + channel.requestClose() + } catch (error) { + failures.push(error) + } + + const graceful = await waitForSettlement( + settlement, + SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS.gracefulCloseMs + ) + if (!graceful) { + try { + channel.forceClose() + } catch (error) { + failures.push(error) + } + const forced = await waitForSettlement( + settlement, + SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS.totalCloseMs - + SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS.gracefulCloseMs + ) + if (!forced) { + failures.unshift(new Error('SSH relay runtime POSIX file channel settlement timed out')) + } + } + + const failure = cleanupFailure(failures) + if (failure !== undefined) { + throw failure + } +} + +function createDestination( + options: OpenSshRelayRuntimePosixFileDestinationOptions, + channel: SshRelayRuntimePosixFileChannel +): SshRelayRuntimeSourceDestination { + const { signal } = options + const channelSettlement: Promise = channel.settled.then( + () => ({ ok: true }), + (error: unknown) => ({ ok: false, error }) + ) + let state: DestinationState = 'open' + let activeOperation: Promise | undefined + let abortPromise: Promise | undefined + let abortReason: unknown + let resolveAbortOutcome: (outcome: AbortOutcome) => void = () => {} + const abortOutcome = new Promise((resolve) => { + resolveAbortOutcome = resolve + }) + + const assertWritable = (): void => { + if (state !== 'open') { + throw new Error('SSH relay runtime POSIX file destination is closed') + } + if (activeOperation) { + throw new Error('SSH relay runtime POSIX file destination has a concurrent operation') + } + } + + const runExclusive = (operation: () => Promise): Promise => { + const pending = operation() + activeOperation = pending + void pending + .finally(() => { + if (activeOperation === pending) { + activeOperation = undefined + } + }) + .catch(() => {}) + return pending + } + + const waitForOperation = async (operation: Promise): Promise => { + const result = await Promise.race([ + operation.then( + () => ({ kind: 'complete' as const }), + (error: unknown) => ({ kind: 'failed' as const, error }) + ), + abortOutcome.then((outcome) => ({ kind: 'aborted' as const, outcome })) + ]) + if (result.kind === 'aborted') { + throw result.outcome.reason + } + if (state === 'aborting' || state === 'aborted') { + const outcome = await abortOutcome + throw outcome.reason + } + if (result.kind === 'failed') { + throw result.error + } + } + + const performAbort = async (): Promise => { + const reason = abortReason + let failure: unknown + try { + await settleCancelledChannel(channel, channelSettlement) + } catch (error) { + failure = error + } finally { + state = 'aborted' + signal.removeEventListener('abort', onSignalAbort) + // Why: retained transport buffers are released only after the channel has settled or timed out. + resolveAbortOutcome({ reason }) + } + if (failure !== undefined) { + throw failure + } + } + + const abort = (reason: unknown): Promise => { + if (abortPromise) { + return abortPromise + } + if (state === 'complete' || state === 'aborted') { + abortPromise = Promise.resolve() + return abortPromise + } + state = 'aborting' + abortReason = reason + abortPromise = performAbort() + return abortPromise + } + + function onSignalAbort(): void { + void abort(signal.reason).catch(() => {}) + } + + const write = (chunk: Buffer): Promise => { + try { + assertWritable() + } catch (error) { + return Promise.reject(error) + } + if (!Buffer.isBuffer(chunk) || chunk.length === 0) { + return Promise.reject(new Error('SSH relay runtime POSIX file destination chunk is empty')) + } + return runExclusive(async () => { + signal.throwIfAborted() + // Why: the callback is the byte-retention boundary before a source worker reuses its buffer. + const accepted = new Promise((resolve, reject) => { + channel.write(chunk, (error) => (error ? reject(error) : resolve())) + }) + await waitForOperation(accepted) + signal.throwIfAborted() + }) + } + + const close = (): Promise => { + try { + assertWritable() + } catch (error) { + return Promise.reject(error) + } + state = 'closing' + return runExclusive(async () => { + signal.throwIfAborted() + channel.end() + const settled = await Promise.race([ + channelSettlement, + abortOutcome.then((outcome) => ({ ok: false as const, error: outcome.reason })) + ]) + if (state === 'aborting' || state === 'aborted') { + const outcome = await abortOutcome + throw outcome.reason + } + if (!settled.ok) { + throw settled.error + } + signal.throwIfAborted() + state = 'complete' + signal.removeEventListener('abort', onSignalAbort) + }) + } + + signal.addEventListener('abort', onSignalAbort, { once: true }) + if (signal.aborted) { + onSignalAbort() + } + return Object.freeze({ write, close, abort }) +} + +export async function openSshRelayRuntimePosixFileDestination( + options: OpenSshRelayRuntimePosixFileDestinationOptions +): Promise { + validateOptions(options) + options.signal.throwIfAborted() + const channel = await options.openChannel( + buildCommand(options.remotePath, options.mode), + options.signal + ) + validateChannel(channel) + const destination = createDestination(options, channel) + try { + options.signal.throwIfAborted() + return destination + } catch (error) { + try { + await destination.abort(error) + } catch (cleanupError) { + throw new AggregateError( + [error, cleanupError], + 'SSH relay runtime POSIX file open cancellation cleanup failed' + ) + } + throw error + } +} From 62ffaec98df827a5a502f55a2c4a7e1cb7d03d03 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 12:59:59 -0400 Subject: [PATCH 225/267] docs(ssh): record POSIX file destination evidence Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 15 +++-- ...github-release-implementation-checklist.md | 67 ++++++++++++++++--- 2 files changed, 65 insertions(+), 17 deletions(-) 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 a5c86899c41..68ee7a29113 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 @@ -10,13 +10,14 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected POSIX -system-SSH/no-tar per-file boundary is locally green and exact-head CI is next, 2026-07-15, Codex -implementation owner.** `E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001` records 25/25 purpose cases, 87 focused -cases with one declared skip, 628 broad relay cases with six declared skips, 281/281 release -contracts, typecheck, full lint, formatting, max-lines, diff, protected-resolver, and no-product- -consumer gates. The command uses only the four declared POSIX primitives and cancellation is bounded -to a 250 ms graceful / 2,000 ms total ceiling. Exact-head all-six native and adjacent Actions proof -is required before the next package. +system-SSH/no-tar per-file boundary is exact-head green; command-channel adapter audit is next, +2026-07-15, Codex implementation owner.** `E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001` records 25/25 purpose, +628 broad relay, 281/281 release, and all static/isolation gates. +`E-M6-POSIX-SYSTEM-SSH-FILE-CI-001` proves the 25 cases on all six primary native clients; both Linux +supplements, Windows x64 floor, PR Checks, computer-use Windows/Ubuntu, and Golden E2E macOS/Linux +pass. Windows arm64 retains only hosted build 26200 versus required 26100 after complete runtime +smoke. The module still has no real SSH/tree/product consumer. Audit the next smallest disconnected +exec-channel adapter; legacy remains the only production/default path and SignPath remains deferred. `E-M6-SFTP-LIVE-COMPOSITION-CI-001` records complete exact-tree/mode/exclusive-root/cancellation/RSS proof against stock OpenSSH on native Linux x64 and arm64. The repeated unrelated Windows compiler timeout is closed at exact head `4945645e8` by computer-use run `29515352845` under diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md index 72d7b245f9d..241907c6a85 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected POSIX system-SSH/no-tar per-file command and destination contract**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **POSIX NO-TAR FILE BOUNDARY LOCALLY GREEN / EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001` proves the isolated destination across 25 purpose cases, 628 broad relay cases, 281 release contracts, typecheck, lint, formatting, max-lines, diff, protected-resolver, and no-product-consumer gates. The module still accepts only an injected command channel and has no real SSH/tree/product caller, so exact-head all-six native and adjacent Actions evidence is required before the next package. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Session checkpoint: **POSIX NO-TAR FILE BOUNDARY EXACT-HEAD GREEN / COMMAND-CHANNEL ADAPTER AUDIT NEXT — 2026-07-15, Codex implementation owner** — `E-M6-POSIX-SYSTEM-SSH-FILE-CI-001` proves the 25-case purpose suite on all six primary native clients; both Linux supplements, Windows x64 floor, PR Checks, computer-use, and Golden E2E pass. Windows arm64 retains only the declared hosted build-26200 versus required-26100 rejection after full runtime smoke. The completed module has no real SSH/tree/product caller. Audit the next smallest disconnected command-channel adapter slice before adding RED tests; do not connect a product/default path. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -150,12 +150,13 @@ same change as the work it records. - Completed package: disconnected full host-evidence composition is green locally and on every primary native client under E-M5-HOST-EVIDENCE-COMPOSITION-LOCAL-001 and E-M5-HOST-EVIDENCE-COMPOSITION-CI-001. It has no product caller. -- Active package: **Local implementation complete / exact-head CI pending — 2026-07-15, Codex - implementation owner** — the disconnected POSIX no-tar per-file command/destination boundary is - green under E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001. It consumes the existing source-stream - destination interface and an injected command-channel factory only. Do not import or modify the - legacy tar/base64 directory uploader, wire `SshConnection`, create a remote tree, add a semantic - probe, or add product/settings/fallback/tuple/publication/default behavior. +- Completed package: the disconnected POSIX no-tar per-file command/destination boundary is green + locally and on every primary native client under E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001 and + E-M6-POSIX-SYSTEM-SSH-FILE-CI-001. It has no real SSH or product caller. +- Active package: **Audit next — 2026-07-15, Codex implementation owner** — establish the smallest + disconnected adapter between one authenticated system-SSH exec channel and the proven per-file + destination. Do not import or modify the legacy tar/base64 directory uploader, create a remote + tree, add a semantic probe, or add product/settings/fallback/tuple/publication/default behavior. - 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 @@ -17573,6 +17574,52 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs`. and SignPath remain absent; legacy remains the sole production/default path. Exact-head all-six native and adjacent Actions evidence is required before the next bounded package. +### E-M6-POSIX-SYSTEM-SSH-FILE-CI-001 — all-six native clients prove the isolated boundary + +- Date/owner/head: 2026-07-15, Codex implementation owner; exact implementation commit + `fac052b7714e2c93c5c84b41b784869d4b3fe125` on draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). +- Native workflow: SSH Relay Runtime Artifacts run + [29516746498](https://github.com/stablyai/orca/actions/runs/29516746498). The exact 25-case purpose + suite passes on all six primary Node 24.18.0 clients: Linux x64 job + [87683577951](https://github.com/stablyai/orca/actions/runs/29516746498/job/87683577951) in 40 ms, + macOS arm64 job [87683577985](https://github.com/stablyai/orca/actions/runs/29516746498/job/87683577985) + in 23 ms, Windows arm64 job + [87683577998](https://github.com/stablyai/orca/actions/runs/29516746498/job/87683577998) in 30 ms, + Windows x64 job [87683578029](https://github.com/stablyai/orca/actions/runs/29516746498/job/87683578029) + in 24 ms, macOS x64 job + [87683578046](https://github.com/stablyai/orca/actions/runs/29516746498/job/87683578046) in 69 ms, + and Linux arm64 job + [87683578064](https://github.com/stablyai/orca/actions/runs/29516746498/job/87683578064) in 27 ms. + POSIX jobs pass 93/93 runtime contract files and Windows jobs pass 94/94; all six then pass native + deterministic build/smoke and full-size extraction/cache evidence. Linux x64/arm64 additionally + retain their independent exact-runtime live SFTP proof. +- Supplemental/baseline result: Linux x64 job 87684783571 and Linux arm64 job 87684783588 pass the + oldest-userland supplements. Windows x64 floor job + [87685969814](https://github.com/stablyai/orca/actions/runs/29516746498/job/87685969814) passes. Only + Windows arm64 floor job + [87685969791](https://github.com/stablyai/orca/actions/runs/29516746498/job/87685969791) rejects the + hosted runner's build 26200 versus the immutable required build 26100 after complete + 85,213,511-byte / 42-file runtime, Node 24.18.0 ABI 137, native PTY, watcher, and two-second resource- + settlement smoke. Platform and architecture checks pass; only `osBuild` is false. This is the + declared runner gap, not a boundary regression. +- Adjacent Actions: Computer-use run + [29516746536](https://github.com/stablyai/orca/actions/runs/29516746536) passes Windows job + [87683577620](https://github.com/stablyai/orca/actions/runs/29516746536/job/87683577620) in 4 minutes + 46 seconds and Ubuntu job + [87683577703](https://github.com/stablyai/orca/actions/runs/29516746536/job/87683577703) in 2 minutes + 53 seconds. PR Checks run + [29516748161](https://github.com/stablyai/orca/actions/runs/29516748161), job 87683582762, passes + lint, typecheck, Git compatibility, tests, unpacked Electron build, and packaged CLI smoke in + 11 minutes 55 seconds. Golden E2E run + [29516747231](https://github.com/stablyai/orca/actions/runs/29516747231) passes macOS job 87683580560 + in 3 minutes 59 seconds and Linux job 87683580641 in 4 minutes 22 seconds. +- Disposition/residual: the isolated file destination package is exact-head green and may close. + This evidence does not prove a real POSIX system-SSH exec adapter, remote tree/root staging, + primitive semantics, full-size system-SSH transfer, `MaxSessions=1`, or a product path. No setting, + Beta mode, fallback, tuple, publication, default behavior, or SignPath work exists; legacy remains + the only production path. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -17631,9 +17678,9 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the locally green isolated POSIX no-tar per-file boundary, then require exact-head -all-six native artifact jobs, both Linux supplements, the Windows x64 floor, PR Checks, Golden E2E, -and computer-use evidence before auditing the next system-SSH adapter/tree slice. Do not +Audit the smallest disconnected authenticated system-SSH exec-channel adapter that can satisfy the +proven POSIX per-file boundary, record its exact ownership/cancellation/residual contract, then add +the purpose-named RED suite and native workflow oracle. Do not check the broad Milestone 6 SFTP or POSIX system-SSH items yet. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets From f1d6d92690d63744f7b2d9d1497ea8a3fcc82329 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 13:12:26 -0400 Subject: [PATCH 226/267] feat(ssh): adapt system SSH relay file channels Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 19 +- ...github-release-implementation-checklist.md | 137 +++++++- ...ay-runtime-system-ssh-file-channel.test.ts | 329 ++++++++++++++++++ ...h-relay-runtime-system-ssh-file-channel.ts | 192 ++++++++++ 6 files changed, 668 insertions(+), 12 deletions(-) create mode 100644 src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts create mode 100644 src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 83d675d4ac0..3ee2724d17c 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -287,6 +287,7 @@ jobs: src/main/ssh/ssh-relay-runtime-source-scan.test.ts \ src/main/ssh/ssh-relay-runtime-source-stream.test.ts \ src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts \ + src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-session.test.ts \ @@ -933,6 +934,7 @@ jobs: src/main/ssh/ssh-relay-runtime-source-scan.test.ts ` src/main/ssh/ssh-relay-runtime-source-stream.test.ts ` src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts ` + src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-session.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 673b8990b4d..c93fb9e2158 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -165,6 +165,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-runtime-source-scan.test.ts', 'ssh-relay-runtime-source-stream.test.ts', 'ssh-relay-runtime-posix-file-destination.test.ts', + 'ssh-relay-runtime-system-ssh-file-channel.test.ts', 'ssh-relay-runtime-sftp-file-destination.test.ts', 'ssh-relay-runtime-sftp-connection-transfer.test.ts', 'ssh-relay-runtime-sftp-session.test.ts', 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 68ee7a29113..5922a3a34ed 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 @@ -9,10 +9,21 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected POSIX -system-SSH/no-tar per-file boundary is exact-head green; command-channel adapter audit is next, -2026-07-15, Codex implementation owner.** `E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001` records 25/25 purpose, -628 broad relay, 281/281 release, and all static/isolation gates. +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — authenticated system-SSH +command-channel adapter is locally green and exact-head CI is next, 2026-07-15, Codex implementation +owner.** `E-M6-POSIX-SYSTEM-SSH-CHANNEL-LOCAL-001` records 13/13 purpose, 150/150 focused, 641 broad +relay cases with six declared skips, 281/281 release contracts, typecheck, full lint, formatting, +max-lines, diff, protected-resolver, and no-product-consumer gates. It retains a single cancellation +owner, drains stdout, caps copied stderr at 16 KiB, and exposes idempotent SIGTERM/SIGKILL hooks. It +has no real tree/live/product consumer; exact-head all-six and adjacent Actions evidence is next. +`E-M6-POSIX-SYSTEM-SSH-CHANNEL-AUDIT-001` limits this slice to one already-authenticated system-SSH +`SshConnection.exec()` channel, bounded copied stderr, drained stdout, exact stdin callback/EOF/ +settlement, and idempotent SIGTERM/SIGKILL hooks consumed by the proven single per-file cancellation +owner. The adapter prechecks but does not forward that signal into `exec`, avoiding a duplicate +immediate SIGTERM listener. +No direct spawn, legacy uploader change, real tree/live claim, or product path is included. +`E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001` records 25/25 purpose, 628 broad relay, 281/281 release, and all +static/isolation gates. `E-M6-POSIX-SYSTEM-SSH-FILE-CI-001` proves the 25 cases on all six primary native clients; both Linux supplements, Windows x64 floor, PR Checks, computer-use Windows/Ubuntu, and Golden E2E macOS/Linux pass. Windows arm64 retains only hosted build 26200 versus required 26100 after complete runtime 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 241907c6a85..44732f12823 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected POSIX system-SSH/no-tar per-file command and destination contract**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **POSIX NO-TAR FILE BOUNDARY EXACT-HEAD GREEN / COMMAND-CHANNEL ADAPTER AUDIT NEXT — 2026-07-15, Codex implementation owner** — `E-M6-POSIX-SYSTEM-SSH-FILE-CI-001` proves the 25-case purpose suite on all six primary native clients; both Linux supplements, Windows x64 floor, PR Checks, computer-use, and Golden E2E pass. Windows arm64 retains only the declared hosted build-26200 versus required-26100 rejection after full runtime smoke. The completed module has no real SSH/tree/product caller. Audit the next smallest disconnected command-channel adapter slice before adding RED tests; do not connect a product/default path. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Session checkpoint: **SYSTEM-SSH COMMAND-CHANNEL ADAPTER LOCALLY GREEN / EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-POSIX-SYSTEM-SSH-CHANNEL-LOCAL-001` proves the adapter and per-file composition across 13 purpose cases, 150 focused cases, 641 broad relay cases, 281 release contracts, typecheck, lint, formatting, max-lines, diff, protected-resolver, and no-product-consumer gates. It remains disconnected and has no tree or live system-SSH claim. Commit/push and require all-six native plus adjacent exact-head evidence before the next package. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -153,10 +153,12 @@ same change as the work it records. - Completed package: the disconnected POSIX no-tar per-file command/destination boundary is green locally and on every primary native client under E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001 and E-M6-POSIX-SYSTEM-SSH-FILE-CI-001. It has no real SSH or product caller. -- Active package: **Audit next — 2026-07-15, Codex implementation owner** — establish the smallest - disconnected adapter between one authenticated system-SSH exec channel and the proven per-file - destination. Do not import or modify the legacy tar/base64 directory uploader, create a remote - tree, add a semantic probe, or add product/settings/fallback/tuple/publication/default behavior. +- Active package: **Local implementation complete / exact-head CI pending — 2026-07-15, Codex + implementation owner** — the purpose-named adapter fixed by + E-M6-POSIX-SYSTEM-SSH-CHANNEL-AUDIT-001 is green under + E-M6-POSIX-SYSTEM-SSH-CHANNEL-LOCAL-001. Do not import or modify the legacy tar/base64 directory + uploader, create a remote tree, add a semantic probe, or add product/settings/fallback/tuple/ + publication/default behavior. - 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 @@ -17620,6 +17622,125 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs`. Beta mode, fallback, tuple, publication, default behavior, or SignPath work exists; legacy remains the only production path. +### E-M6-POSIX-SYSTEM-SSH-CHANNEL-AUDIT-001 — authenticated exec-channel adapter boundary + +- Date/owner/base: 2026-07-15, Codex implementation owner; clean exact base + `62ffaec98df827a5a502f55a2c4a7e1cb7d03d03` after recording the completed file-boundary evidence. +- Existing-code audit: `SshConnection.exec(command)` already preserves the connected + target, resolved OpenSSH configuration, host-scoped ControlMaster disable/retry state, command + tracking, and disconnect/reconnect teardown. Guarding `usesSystemSshTransport()` before that call + prevents accidental ssh2/SFTP-family use. `spawnSystemSshCommand` then wraps the local OpenSSH + child as a `ClientChannel`, exposes stdin/stderr plus its owned child process, forwards close/error, + and keeps stdout backpressure bounded when a consumer drains it. +- Rejected reuse: `system-ssh-file-binary-transfer.ts` is a proven legacy/default whole-buffer/local- + file uploader and must remain unchanged. `waitForChannelClose` accumulates arbitrary stderr, while + `awaitWithSystemSshAbort` deliberately returns immediately after SIGTERM instead of awaiting + process settlement. Those contracts are correct for legacy callers but do not meet the new + bootstrap's capped-memory and pre-execution cancellation boundary; do not import or modify them. +- New boundary: add one purpose-named disconnected adapter module that accepts only a connection, + exact command, and exact signal. It rejects a non-system transport or pre-aborted signal before + exec, calls `connection.exec(command)` exactly once, drains unused stdout immediately, + and returns `SshRelayRuntimePosixFileChannel`. Writes forward the exact borrowed chunk and callback + to child stdin; EOF ends child stdin once; settlement listens to child error/close, requires exit + zero, and includes at most a small fixed copied stderr prefix plus a truncation marker without + retaining an attacker-sized chunk. +- Single cancellation owner: do not pass the signal into `SshConnection.exec`. The proven per-file + destination already owns that exact signal and its bounded SIGTERM/SIGKILL sequence; passing it to + the connection would install a second immediate SIGTERM listener on the same child. Connection- + level disconnect/reconnect still closes every tracked command independently. +- Cancellation ownership: the adapter exposes idempotent graceful close through the tracked + channel's SIGTERM `close()` and idempotent forced close through its owned child process's + `kill('SIGKILL')`. Missing child-process ownership or a thrown kill fails closed. The proven + `ssh-relay-runtime-posix-file-destination.ts` owner retains the 250 ms graceful / 2,000 ms total + settlement timers and decides when to escalate; the adapter must not add a second timer or return + success before the process emits close. +- Layer-A RED/green proof: purpose cases must cover system-transport/pre-abort rejection with zero + exec; exact command, signal precheck, and one exec; stdout drain; exact non-empty chunk/callback and + stdin EOF; + zero/nonzero/signal/error settlement; bounded copied stderr/truncation; listener cleanup; duplicate + graceful/forced calls; SIGTERM before SIGKILL; exited-child force no-op; missing process and thrown + close/kill failures; and composition with the per-file owner for retained-write cancellation and + forced/timeout settlement. Pin the purpose suite exactly once in both native workflow families + before implementation. +- Explicit residual boundary: no direct spawn, SshConnection modification, ssh2/SFTP path, directory + or tree/root staging, primitive semantic probe, tar optimization, `MaxSessions=1` concurrency + policy, full-size/live system-SSH transfer, Windows PowerShell path, remote verification/install/ + publish/launch, product/Beta/settings/fallback/tuple/publication/default behavior, or SignPath is + included. Legacy remains the sole production/default path. + +### E-M6-POSIX-SYSTEM-SSH-CHANNEL-LOCAL-RED-001 — missing adapter fails as designed + +- Date/owner/base: 2026-07-15, Codex implementation owner; exact uncommitted test/workflow state on + base `62ffaec98df827a5a502f55a2c4a7e1cb7d03d03` after + E-M6-POSIX-SYSTEM-SSH-CHANNEL-AUDIT-001. +- Command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs`. +- Result: expected RED, exit 1. The new 311-line purpose suite collects zero cases and fails solely + with `Cannot find module './ssh-relay-runtime-system-ssh-file-channel'`; that production module does + not exist yet. The independent release-workflow oracle passes all 8/8 cases and proves the suite is + pinned exactly once in both native command families. +- Resource evidence: Vitest duration 539 ms; `/usr/bin/time -l` reports 1.69 seconds real, + 133,103,616 bytes maximum resident set size, zero swaps, and zero block I/O. +- Disposition: RED is causally narrow and accepted. Implement only the audited adapter, then require + purpose composition GREEN plus focused, broad, static, isolation, and exact-head native evidence. + No live system-SSH, tree, product, or default-path claim follows from this evidence. + +### E-M6-POSIX-SYSTEM-SSH-CHANNEL-LOCAL-TYPE-RED-001 — typed stream boundary corrected + +- Date/owner/state: 2026-07-15, Codex implementation owner; first static pass on the uncommitted + adapter atop `62ffaec98df827a5a502f55a2c4a7e1cb7d03d03` after runtime purpose GREEN. +- Command/result: `/usr/bin/time -l pnpm typecheck` — gate RED, exit 1 in 1.93 seconds real with + 1,044,365,312-byte maximum RSS. TypeScript reports fake `ClientChannel`/stream intersections, + deliberate rejected-promise narrowing, a readonly fake exit field, and the overloaded Node stdin + callback signature; no runtime assertion fails. +- Correction: keep the fake structural until the `ClientChannel` return boundary, narrow the already- + validated child stdin to the exact Buffer/callback/end shape, explicitly narrow the caught error, + and retain mutable fake process state. The repeated 13-case purpose suite and typecheck below pass. + No behavior, lifecycle timer, product path, or legacy module changed. + +### E-M6-POSIX-SYSTEM-SSH-CHANNEL-LOCAL-001 — authenticated child-channel adapter is locally green + +- Date/owner/environment/base: 2026-07-15, Codex implementation owner; local macOS arm64, Node + 26.0.0, pnpm 10.24.0, uncommitted package atop exact base + `62ffaec98df827a5a502f55a2c4a7e1cb7d03d03`. +- Implementation: the 192-line adapter rejects non-system transport and pre-abort before exec, calls + the already-authenticated/tracked `connection.exec(command)` exactly once, and deliberately keeps + cancellation single-owned by the per-file destination. It drains unused stdout, forwards the exact + borrowed Buffer/callback and EOF to child stdin, requires exit zero, copies/caps stderr at 16 KiB, + cleans all diagnostic/settlement listeners, and exposes idempotent SIGTERM/SIGKILL hooks. Missing + process ownership, false/throwing force signals, and termination failures fail closed. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts` — PASS, 13/13 in + 346 ms Vitest / 1.33 seconds real, 131,727,360-byte maximum RSS, zero swaps, and zero block I/O. + Cases cover transport/pre-abort rejection, exact one exec, stdout drain, retained chunk callback, + EOF, successful/nonzero/signaled/error settlement, copied capped diagnostic, listener cleanup, + idempotent ordered termination, exited/missing/unsignaled/thrown process cases, graceful retained- + write composition, and forced composition using only the file owner's timer. +- Focused command: adapter, per-file destination, existing system-SSH fallback, `SshConnection`, and + native workflow oracle — PASS, five files and 150/150 cases in 2.95 seconds Vitest / 4.31 seconds + real with 156,680,192-byte maximum RSS. The workflow oracle passes 8/8 and pins the purpose suite + exactly once in POSIX and PowerShell native command families. +- Broad commands: `src/main/ssh/ssh-relay-*.test.ts` — PASS, 48 files plus four declared skipped, + 641 cases plus six declared skips in 32.50 seconds Vitest / 34.59 seconds real with + 248,201,216-byte maximum RSS; `config/scripts/ssh-relay-runtime-*.test.mjs` — PASS, 50 files and + 281/281 cases in 21.88 seconds Vitest / 23.87 seconds real with 190,169,088-byte maximum RSS. Only + the established stale-lock diagnostic and local Node 26 module deprecation appear. +- Static/isolation commands: repeated `/usr/bin/time -l pnpm typecheck` passes in 3.11 seconds real + with 1,209,237,504-byte maximum RSS. Targeted `oxlint`, targeted `oxfmt --check`, standalone + max-lines ratchet, `git diff --check`, protected-resolver empty diff, and no non-test consumer pass. + Full `pnpm lint` passes in 19.61 seconds real with 2,067,415,040-byte maximum RSS: switch + exhaustiveness, 41 reliability gates, the 355-entry max-lines ratchet, bundled-skill verification, + 9,837 localization references, locale parity, and zero-candidate localization coverage pass; only + pre-existing unrelated warnings remain. Module/test sizes are 192/329 lines with no bypass. +- Residual: this proves an authenticated connection/child callback contract and its file-owner + composition, not a real local OpenSSH process, remote shell, directory/tree/root staging, + primitive semantics, full-size system-SSH transfer, `MaxSessions=1`, or Windows PowerShell path. + It has no non-test consumer. Remote verification/install/publish/launch, product/Beta/settings/ + fallback/tuple/publication/default behavior, and SignPath remain absent; legacy remains the only + production path. Exact-head all-six native and adjacent Actions evidence is required before the + next package. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -17678,9 +17799,9 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Audit the smallest disconnected authenticated system-SSH exec-channel adapter that can satisfy the -proven POSIX per-file boundary, record its exact ownership/cancellation/residual contract, then add -the purpose-named RED suite and native workflow oracle. Do not +Commit and push the locally green authenticated system-SSH exec-channel adapter, then require exact- +head all-six native artifact jobs, both Linux supplements, Windows x64 floor, PR Checks, computer- +use, and Golden E2E before auditing the next tree/root composition package. Do not check the broad Milestone 6 SFTP or POSIX system-SSH items yet. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets diff --git a/src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts b/src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts new file mode 100644 index 00000000000..567c40cd488 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts @@ -0,0 +1,329 @@ +import { EventEmitter } from 'node:events' +import type { ChildProcess } from 'node:child_process' +import type { ClientChannel } from 'ssh2' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { + openSshRelayRuntimePosixFileDestination, + SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS +} from './ssh-relay-runtime-posix-file-destination' +import { + openSshRelayRuntimeSystemSshFileChannel, + SSH_RELAY_RUNTIME_SYSTEM_SSH_FILE_CHANNEL_LIMITS, + type SshRelayRuntimeSystemSshConnection +} from './ssh-relay-runtime-system-ssh-file-channel' + +type WriteCallback = (error?: Error) => void + +type FakeChannel = EventEmitter & { + stdin: EventEmitter & { + write: ReturnType boolean>> + end: ReturnType void>> + } + stderr: EventEmitter + resume: ReturnType FakeChannel>> + close: ReturnType void>> + _process?: ChildProcess +} + +function createChannel(options: { process?: boolean } = {}): { + channel: FakeChannel + kill: ReturnType boolean>> + process: { exitCode: number | null; signalCode: NodeJS.Signals | null } +} { + const emitter = new EventEmitter() as FakeChannel + const stdin = Object.assign(new EventEmitter(), { + write: vi.fn((_chunk: Buffer, callback: WriteCallback) => { + callback() + return true + }), + end: vi.fn() + }) + const kill = vi.fn(() => true) + emitter.stdin = stdin + emitter.stderr = new EventEmitter() + emitter.resume = vi.fn(() => emitter) + emitter.close = vi.fn() + const process = { exitCode: null, signalCode: null, kill } + if (options.process !== false) { + emitter._process = process as unknown as ChildProcess + } + return { channel: emitter, kill, process } +} + +function createConnection( + channel: FakeChannel, + systemTransport = true +): SshRelayRuntimeSystemSshConnection & { + exec: ReturnType> +} { + return { + usesSystemSshTransport: vi.fn(() => systemTransport), + exec: vi.fn(async () => channel as unknown as ClientChannel) + } +} + +afterEach(() => { + vi.useRealTimers() +}) + +describe('SSH relay runtime system-SSH file channel', () => { + it('rejects non-system transport and pre-abort before exec', async () => { + const { channel } = createChannel() + const nonSystem = createConnection(channel, false) + + await expect( + openSshRelayRuntimeSystemSshFileChannel( + nonSystem, + 'cat command', + new AbortController().signal + ) + ).rejects.toThrow(/system ssh/i) + expect(nonSystem.exec).not.toHaveBeenCalled() + + const cancelled = createConnection(channel) + const controller = new AbortController() + const reason = new Error('cancelled before exec') + controller.abort(reason) + await expect( + openSshRelayRuntimeSystemSshFileChannel(cancelled, 'cat command', controller.signal) + ).rejects.toBe(reason) + expect(cancelled.exec).not.toHaveBeenCalled() + }) + + it('opens once with the exact command and keeps cancellation single-owned', async () => { + const { channel } = createChannel() + const connection = createConnection(channel) + const controller = new AbortController() + const adapted = await openSshRelayRuntimeSystemSshFileChannel( + connection, + 'exact remote command', + controller.signal + ) + + expect(connection.exec).toHaveBeenCalledOnce() + expect(connection.exec).toHaveBeenCalledWith('exact remote command') + expect(channel.resume).toHaveBeenCalledOnce() + channel.emit('close', 0, null) + await expect(adapted.settled).resolves.toBeUndefined() + expect(channel.listenerCount('error')).toBe(0) + expect(channel.listenerCount('close')).toBe(0) + expect(channel.stderr.listenerCount('data')).toBe(0) + expect(channel.stderr.listenerCount('error')).toBe(0) + }) + + it('forwards the exact borrowed chunk callback and EOF to child stdin', async () => { + const { channel } = createChannel() + let retainedCallback: WriteCallback | undefined + channel.stdin.write.mockImplementationOnce((_chunk, callback) => { + retainedCallback = callback + return false + }) + const adapted = await openSshRelayRuntimeSystemSshFileChannel( + createConnection(channel), + 'cat command', + new AbortController().signal + ) + const chunk = Buffer.alloc(64 * 1024, 3) + const callback = vi.fn() + + adapted.write(chunk, callback) + expect(channel.stdin.write).toHaveBeenCalledWith(chunk, callback) + expect(callback).not.toHaveBeenCalled() + retainedCallback?.() + expect(callback).toHaveBeenCalledOnce() + adapted.end() + expect(channel.stdin.end).toHaveBeenCalledOnce() + channel.emit('close', 0, null) + await adapted.settled + }) + + it.each([ + [1, null, 'exit 1'], + [null, 'SIGTERM', 'signal SIGTERM'] + ] as const)('rejects nonzero settlement %j/%j', async (code, signal, expected) => { + const { channel } = createChannel() + const adapted = await openSshRelayRuntimeSystemSshFileChannel( + createConnection(channel), + 'cat command', + new AbortController().signal + ) + + channel.stderr.emit('data', Buffer.from('remote cat failed')) + channel.emit('close', code, signal) + await expect(adapted.settled).rejects.toThrow(expected) + await expect(adapted.settled).rejects.toThrow('remote cat failed') + }) + + it.each(['channel', 'stderr'] as const)( + 'propagates %s errors and removes listeners', + async (from) => { + const { channel } = createChannel() + const adapted = await openSshRelayRuntimeSystemSshFileChannel( + createConnection(channel), + 'cat command', + new AbortController().signal + ) + const error = new Error(`${from} failed`) + + if (from === 'channel') { + channel.emit('error', error) + } else { + channel.stderr.emit('error', error) + } + await expect(adapted.settled).rejects.toBe(error) + expect(channel.listenerCount('error')).toBe(0) + expect(channel.listenerCount('close')).toBe(0) + expect(channel.stderr.listenerCount('data')).toBe(0) + expect(channel.stderr.listenerCount('error')).toBe(0) + } + ) + + it('copies and caps attacker-sized stderr with an explicit marker', async () => { + const { channel } = createChannel() + const adapted = await openSshRelayRuntimeSystemSshFileChannel( + createConnection(channel), + 'cat command', + new AbortController().signal + ) + const diagnostic = Buffer.alloc( + SSH_RELAY_RUNTIME_SYSTEM_SSH_FILE_CHANNEL_LIMITS.diagnosticBytes * 4, + 's' + ) + + channel.stderr.emit('data', diagnostic) + diagnostic.fill('x') + channel.emit('close', 9, null) + const error = await adapted.settled.catch((reason: Error) => reason) + expect(error).toBeInstanceOf(Error) + if (!(error instanceof Error)) { + throw new Error('expected system SSH settlement failure') + } + expect(error.message).toContain('sss') + expect(error.message).not.toContain('xxx') + expect(error.message).toContain('truncated') + expect(Buffer.byteLength(error.message)).toBeLessThanOrEqual( + SSH_RELAY_RUNTIME_SYSTEM_SSH_FILE_CHANNEL_LIMITS.diagnosticBytes + 200 + ) + }) + + it('makes graceful and forced close idempotent and ordered', async () => { + const { channel, kill } = createChannel() + const adapted = await openSshRelayRuntimeSystemSshFileChannel( + createConnection(channel), + 'cat command', + new AbortController().signal + ) + void adapted.settled.catch(() => {}) + + adapted.requestClose() + adapted.requestClose() + adapted.forceClose() + adapted.forceClose() + expect(channel.close).toHaveBeenCalledOnce() + expect(kill).toHaveBeenCalledOnce() + expect(kill).toHaveBeenCalledWith('SIGKILL') + expect(channel.close.mock.invocationCallOrder[0]).toBeLessThan(kill.mock.invocationCallOrder[0]) + }) + + it('does not force an exited child and fails closed without process ownership or signaling', async () => { + const exited = createChannel() + exited.process.exitCode = 0 + const exitedAdapter = await openSshRelayRuntimeSystemSshFileChannel( + createConnection(exited.channel), + 'cat command', + new AbortController().signal + ) + void exitedAdapter.settled.catch(() => {}) + exitedAdapter.forceClose() + expect(exited.kill).not.toHaveBeenCalled() + + const missing = createChannel({ process: false }) + const missingAdapter = await openSshRelayRuntimeSystemSshFileChannel( + createConnection(missing.channel), + 'cat command', + new AbortController().signal + ) + void missingAdapter.settled.catch(() => {}) + expect(() => missingAdapter.forceClose()).toThrow(/process ownership/i) + + const unsignaled = createChannel() + unsignaled.kill.mockReturnValue(false) + const unsignaledAdapter = await openSshRelayRuntimeSystemSshFileChannel( + createConnection(unsignaled.channel), + 'cat command', + new AbortController().signal + ) + void unsignaledAdapter.settled.catch(() => {}) + expect(() => unsignaledAdapter.forceClose()).toThrow(/forced termination/i) + }) + + it('propagates thrown graceful and forced termination failures once', async () => { + const { channel, kill } = createChannel() + channel.close.mockImplementation(() => { + throw new Error('SIGTERM failed') + }) + kill.mockImplementation(() => { + throw new Error('SIGKILL failed') + }) + const adapted = await openSshRelayRuntimeSystemSshFileChannel( + createConnection(channel), + 'cat command', + new AbortController().signal + ) + void adapted.settled.catch(() => {}) + + expect(() => adapted.requestClose()).toThrow('SIGTERM failed') + expect(() => adapted.requestClose()).not.toThrow() + expect(() => adapted.forceClose()).toThrow('SIGKILL failed') + expect(() => adapted.forceClose()).not.toThrow() + }) + + it('composes retained-write cancellation through graceful process settlement', async () => { + const { channel, kill } = createChannel() + channel.stdin.write.mockImplementation(() => true) + channel.close.mockImplementation(() => channel.emit('close', null, 'SIGTERM')) + const connection = createConnection(channel) + const controller = new AbortController() + const reason = new Error('cancelled retained system SSH write') + const destination = await openSshRelayRuntimePosixFileDestination({ + remotePath: '/owned-stage/bin/node', + mode: 0o755, + signal: controller.signal, + openChannel: (command, signal) => + openSshRelayRuntimeSystemSshFileChannel(connection, command, signal) + }) + const writing = destination.write(Buffer.alloc(64 * 1024, 5)) + + controller.abort(reason) + await expect(writing).rejects.toBe(reason) + await expect(destination.abort(reason)).resolves.toBeUndefined() + expect(channel.close).toHaveBeenCalled() + expect(kill).not.toHaveBeenCalled() + }) + + it('composes forced settlement without adding a second timer', async () => { + vi.useFakeTimers() + const { channel, kill } = createChannel() + kill.mockImplementation(() => { + channel.emit('close', null, 'SIGKILL') + return true + }) + const controller = new AbortController() + const destination = await openSshRelayRuntimePosixFileDestination({ + remotePath: '/owned-stage/bin/node', + mode: 0o755, + signal: controller.signal, + openChannel: (command, signal) => + openSshRelayRuntimeSystemSshFileChannel(createConnection(channel), command, signal) + }) + + controller.abort(new Error('cancelled')) + const aborting = destination.abort(controller.signal.reason) + await vi.advanceTimersByTimeAsync( + SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS.gracefulCloseMs + ) + await expect(aborting).resolves.toBeUndefined() + expect(channel.close).toHaveBeenCalledOnce() + expect(kill).toHaveBeenCalledWith('SIGKILL') + }) +}) diff --git a/src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.ts b/src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.ts new file mode 100644 index 00000000000..ace3381340d --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.ts @@ -0,0 +1,192 @@ +import type { ClientChannel } from 'ssh2' +import type { SshRelayRuntimePosixFileChannel } from './ssh-relay-runtime-posix-file-destination' +import type { SystemSshCommandChannel } from './system-ssh-command' + +export type SshRelayRuntimeSystemSshConnection = Readonly<{ + usesSystemSshTransport: () => boolean + exec: (command: string) => Promise +}> + +export const SSH_RELAY_RUNTIME_SYSTEM_SSH_FILE_CHANNEL_LIMITS = Object.freeze({ + diagnosticBytes: 16 * 1024 +}) + +type ChannelCloseCode = number | null +type ChannelCloseSignal = NodeJS.Signals | null | undefined + +function validateInput( + connection: SshRelayRuntimeSystemSshConnection, + command: string, + signal: AbortSignal +): void { + if ( + !connection || + typeof connection.usesSystemSshTransport !== 'function' || + typeof connection.exec !== 'function' || + typeof command !== 'string' || + command === '' || + !signal + ) { + throw new Error('SSH relay runtime system SSH file channel input is invalid') + } + if (!connection.usesSystemSshTransport()) { + throw new Error('SSH relay runtime system SSH file channel requires system SSH transport') + } + signal.throwIfAborted() +} + +function validateChannel(channel: SystemSshCommandChannel): void { + if ( + !channel || + typeof channel.on !== 'function' || + typeof channel.off !== 'function' || + typeof channel.resume !== 'function' || + typeof channel.close !== 'function' || + !channel.stdin || + typeof channel.stdin.write !== 'function' || + typeof channel.stdin.end !== 'function' || + !channel.stderr || + typeof channel.stderr.on !== 'function' || + typeof channel.stderr.off !== 'function' + ) { + throw new Error('SSH relay runtime system SSH command channel is invalid') + } +} + +function appendDiagnostic( + chunks: Buffer[], + size: number, + data: Buffer | string +): { size: number; truncated: boolean } { + const bytes = Buffer.isBuffer(data) ? data : Buffer.from(data) + const remaining = SSH_RELAY_RUNTIME_SYSTEM_SSH_FILE_CHANNEL_LIMITS.diagnosticBytes - size + if (remaining > 0) { + // Why: copying avoids retaining an attacker-sized parent buffer for a small diagnostic prefix. + const copied = Buffer.from(bytes.subarray(0, remaining)) + chunks.push(copied) + size += copied.length + } + return { size, truncated: bytes.length > remaining } +} + +function diagnosticText(chunks: readonly Buffer[], truncated: boolean): string { + const text = Buffer.concat(chunks).toString('utf8').trim() + if (!truncated) { + return text + } + return `${text}${text ? ' ' : ''}[truncated]` +} + +function settlementError( + code: ChannelCloseCode, + signal: ChannelCloseSignal, + diagnostic: string +): Error { + const detail = code === null ? `signal ${signal ?? 'unknown'}` : `exit ${code}` + return new Error( + `SSH relay runtime system SSH file command failed (${detail})${diagnostic ? `: ${diagnostic}` : ''}` + ) +} + +function waitForSettlement(channel: SystemSshCommandChannel): Promise { + return new Promise((resolve, reject) => { + const diagnosticChunks: Buffer[] = [] + let diagnosticSize = 0 + let diagnosticTruncated = false + let complete = false + + const cleanup = (): void => { + channel.off('error', onError) + channel.off('close', onClose) + channel.stderr.off('data', onStderrData) + channel.stderr.off('error', onError) + } + const settle = (callback: () => void): void => { + if (complete) { + return + } + complete = true + cleanup() + callback() + } + const onError = (error: Error): void => { + settle(() => reject(error)) + } + const onStderrData = (data: Buffer | string): void => { + const appended = appendDiagnostic(diagnosticChunks, diagnosticSize, data) + diagnosticSize = appended.size + diagnosticTruncated ||= appended.truncated + } + const onClose = (code: ChannelCloseCode, signal?: ChannelCloseSignal): void => { + settle(() => { + if (code === 0) { + resolve() + return + } + reject(settlementError(code, signal, diagnosticText(diagnosticChunks, diagnosticTruncated))) + }) + } + + channel.on('error', onError) + channel.on('close', onClose) + channel.stderr.on('data', onStderrData) + channel.stderr.on('error', onError) + }) +} + +function adaptChannel(channel: SystemSshCommandChannel): SshRelayRuntimePosixFileChannel { + validateChannel(channel) + const settled = waitForSettlement(channel) + const input = channel.stdin as unknown as { + write: (chunk: Buffer, callback: (error?: Error) => void) => void + end: () => void + } + let closeRequested = false + let forceRequested = false + + // Why: the remote command has no stdout contract; draining prevents login noise from deadlocking. + channel.resume() + + const write = (chunk: Buffer, callback: (error?: Error) => void): void => { + input.write(chunk, callback) + } + const end = (): void => { + input.end() + } + const requestClose = (): void => { + if (closeRequested) { + return + } + closeRequested = true + channel.close() + } + const forceClose = (): void => { + if (forceRequested) { + return + } + forceRequested = true + const process = channel._process + if (!process) { + throw new Error('SSH relay runtime system SSH file channel process ownership is missing') + } + if (process.exitCode !== null || process.signalCode !== null) { + return + } + if (!process.kill('SIGKILL')) { + throw new Error('SSH relay runtime system SSH file channel forced termination failed') + } + } + + return Object.freeze({ write, end, settled, requestClose, forceClose }) +} + +export async function openSshRelayRuntimeSystemSshFileChannel( + connection: SshRelayRuntimeSystemSshConnection, + command: string, + signal: AbortSignal +): Promise { + validateInput(connection, command, signal) + // Why: the file destination owns this signal's bounded teardown; forwarding it would double-kill. + const channel = (await connection.exec(command)) as SystemSshCommandChannel + return adaptChannel(channel) +} From 3cf9bae8f1ac8dd3d0ede4fdbb5c80e90cfe636b Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 13:31:21 -0400 Subject: [PATCH 227/267] docs(ssh): record system SSH channel evidence Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 20 +++--- ...github-release-implementation-checklist.md | 67 +++++++++++++++++-- 2 files changed, 73 insertions(+), 14 deletions(-) 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 5922a3a34ed..54f48290c78 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 @@ -10,25 +10,27 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — authenticated system-SSH -command-channel adapter is locally green and exact-head CI is next, 2026-07-15, Codex implementation -owner.** `E-M6-POSIX-SYSTEM-SSH-CHANNEL-LOCAL-001` records 13/13 purpose, 150/150 focused, 641 broad -relay cases with six declared skips, 281/281 release contracts, typecheck, full lint, formatting, -max-lines, diff, protected-resolver, and no-product-consumer gates. It retains a single cancellation -owner, drains stdout, caps copied stderr at 16 KiB, and exposes idempotent SIGTERM/SIGKILL hooks. It -has no real tree/live/product consumer; exact-head all-six and adjacent Actions evidence is next. +command-channel adapter is exact-head green; disconnected tree/root composition audit is next, +2026-07-15, Codex implementation owner.** `E-M6-POSIX-SYSTEM-SSH-CHANNEL-LOCAL-001` records 13/13 +purpose, 150/150 focused, 641 broad relay cases with six declared skips, 281/281 release contracts, +and all static/isolation gates. `E-M6-POSIX-SYSTEM-SSH-CHANNEL-CI-001` proves the 13 cases on all six +primary native clients; both Linux supplements, Windows x64 floor, PR Checks, computer-use Windows/ +Ubuntu, and Golden E2E macOS/Linux pass. Windows arm64 retains only the declared hosted build 26200 +versus required 26100 rejection after the complete 85,213,511-byte/42-file runtime smoke. `E-M6-POSIX-SYSTEM-SSH-CHANNEL-AUDIT-001` limits this slice to one already-authenticated system-SSH `SshConnection.exec()` channel, bounded copied stderr, drained stdout, exact stdin callback/EOF/ settlement, and idempotent SIGTERM/SIGKILL hooks consumed by the proven single per-file cancellation owner. The adapter prechecks but does not forward that signal into `exec`, avoiding a duplicate immediate SIGTERM listener. -No direct spawn, legacy uploader change, real tree/live claim, or product path is included. +No direct spawn, legacy uploader change, real tree/live claim, or product path is included. Audit the +next smallest disconnected tree/root composition package; legacy remains the only production/default +path and SignPath remains deferred. `E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001` records 25/25 purpose, 628 broad relay, 281/281 release, and all static/isolation gates. `E-M6-POSIX-SYSTEM-SSH-FILE-CI-001` proves the 25 cases on all six primary native clients; both Linux supplements, Windows x64 floor, PR Checks, computer-use Windows/Ubuntu, and Golden E2E macOS/Linux pass. Windows arm64 retains only hosted build 26200 versus required 26100 after complete runtime -smoke. The module still has no real SSH/tree/product consumer. Audit the next smallest disconnected -exec-channel adapter; legacy remains the only production/default path and SignPath remains deferred. +smoke. The module still has no real SSH/tree/product consumer. `E-M6-SFTP-LIVE-COMPOSITION-CI-001` records complete exact-tree/mode/exclusive-root/cancellation/RSS proof against stock OpenSSH on native Linux x64 and arm64. The repeated unrelated Windows compiler timeout is closed at exact head `4945645e8` by computer-use run `29515352845` under diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md index 44732f12823..d35d3eb026a 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected POSIX system-SSH/no-tar per-file command and destination contract**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **SYSTEM-SSH COMMAND-CHANNEL ADAPTER LOCALLY GREEN / EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-POSIX-SYSTEM-SSH-CHANNEL-LOCAL-001` proves the adapter and per-file composition across 13 purpose cases, 150 focused cases, 641 broad relay cases, 281 release contracts, typecheck, lint, formatting, max-lines, diff, protected-resolver, and no-product-consumer gates. It remains disconnected and has no tree or live system-SSH claim. Commit/push and require all-six native plus adjacent exact-head evidence before the next package. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected POSIX system-SSH/no-tar transfer composition**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **SYSTEM-SSH COMMAND-CHANNEL ADAPTER EXACT-HEAD GREEN / TREE-ROOT COMPOSITION AUDIT NEXT — 2026-07-15, Codex implementation owner** — `E-M6-POSIX-SYSTEM-SSH-CHANNEL-LOCAL-001` and `E-M6-POSIX-SYSTEM-SSH-CHANNEL-CI-001` prove the adapter and per-file composition locally and on all six primary native clients, both Linux supplements, Windows x64 floor, PR Checks, computer-use, and Golden E2E. Windows arm64 retains only the declared hosted build-26200 versus required-26100 gap after complete runtime smoke. The package remains disconnected and has no tree or live system-SSH claim. Audit the next smallest disconnected tree/root composition package before adding RED tests. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -17741,6 +17741,62 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs`. production path. Exact-head all-six native and adjacent Actions evidence is required before the next package. +### E-M6-POSIX-SYSTEM-SSH-CHANNEL-CI-001 — all-six native clients prove the channel adapter + +- Date/owner/head: 2026-07-15, Codex implementation owner; exact implementation commit + `f1d6d92690d63744f7b2d9d1497ea8a3fcc82329` on draft PR + [#8741](https://github.com/stablyai/orca/pull/8741). +- Native workflow: SSH Relay Runtime Artifacts run + [29518835627](https://github.com/stablyai/orca/actions/runs/29518835627). The exact 13-case purpose + suite passes on all six primary Node 24.18.0 clients: Linux x64 job + [87690584214](https://github.com/stablyai/orca/actions/runs/29518835627/job/87690584214) in 269 ms, + Linux arm64 job + [87690584277](https://github.com/stablyai/orca/actions/runs/29518835627/job/87690584277) in 209 ms, + macOS x64 job + [87690584250](https://github.com/stablyai/orca/actions/runs/29518835627/job/87690584250) in 503 ms, + macOS arm64 job + [87690584270](https://github.com/stablyai/orca/actions/runs/29518835627/job/87690584270) in 119 ms, + Windows x64 job + [87690584285](https://github.com/stablyai/orca/actions/runs/29518835627/job/87690584285) in 393 ms, + and Windows arm64 job + [87690584235](https://github.com/stablyai/orca/actions/runs/29518835627/job/87690584235) in 343 ms. + Linux jobs pass 94 files and 810 cases with one declared skip, macOS jobs pass 94 files and 809 + cases with two declared skips, and Windows jobs pass 95 files and 797 cases with 18 declared skips. + All six then pass deterministic runtime build/smoke and full-size extraction/cache evidence; Linux + x64/arm64 also retain their independent exact-runtime live SFTP proof. +- Supplemental/baseline result: Linux arm64 job + [87692000755](https://github.com/stablyai/orca/actions/runs/29518835627/job/87692000755) and Linux + x64 job [87692000791](https://github.com/stablyai/orca/actions/runs/29518835627/job/87692000791) + pass the oldest-userland supplements. Windows x64 floor job + [87693390041](https://github.com/stablyai/orca/actions/runs/29518835627/job/87693390041) passes on + build 20348. Only Windows arm64 floor job + [87693390043](https://github.com/stablyai/orca/actions/runs/29518835627/job/87693390043) rejects the + hosted runner's build 26200 versus immutable required build 26100 after complete 85,213,511-byte / + 42-file runtime, Node 24.18.0 ABI 137, native PTY, watcher, and two-second resource-settlement smoke. + Platform and architecture checks pass; only `osBuild` is false. This is the declared runner gap, + not an adapter regression. +- Adjacent Actions: Computer-use run + [29518834854](https://github.com/stablyai/orca/actions/runs/29518834854) passes Windows job + [87690552717](https://github.com/stablyai/orca/actions/runs/29518834854/job/87690552717) in 5 minutes + 1 second and Ubuntu job + [87690552721](https://github.com/stablyai/orca/actions/runs/29518834854/job/87690552721) in 3 minutes + 14 seconds. Golden E2E run + [29518834911](https://github.com/stablyai/orca/actions/runs/29518834911) passes Linux job + [87690552742](https://github.com/stablyai/orca/actions/runs/29518834911/job/87690552742) in 4 minutes + 29 seconds and macOS job + [87690552777](https://github.com/stablyai/orca/actions/runs/29518834911/job/87690552777) in 3 minutes + 8 seconds. PR Checks run + [29518835161](https://github.com/stablyai/orca/actions/runs/29518835161), job + [87690553577](https://github.com/stablyai/orca/actions/runs/29518835161/job/87690553577), passes + lint, typecheck, Git compatibility, tests, unpacked Electron build, and packaged CLI smoke in + 12 minutes 45 seconds. +- Disposition/residual: the authenticated system-SSH command-channel adapter package is exact-head + green and may close. This evidence proves an already-authenticated child-channel/per-file contract, + not a real remote tree/root transfer, semantic primitive probe, full-size system-SSH transfer, + `MaxSessions=1`, Windows PowerShell transfer, remote verification/install/publish/launch, or a + product path. No setting, Beta mode, fallback, tuple, publication, default behavior, or SignPath + work exists; legacy remains the only production path. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -17799,9 +17855,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the locally green authenticated system-SSH exec-channel adapter, then require exact- -head all-six native artifact jobs, both Linux supplements, Windows x64 floor, PR Checks, computer- -use, and Golden E2E before auditing the next tree/root composition package. Do not +Audit the smallest disconnected POSIX system-SSH tree/root composition package that can combine the +proven authenticated exec-channel adapter, per-file destination, and authenticated source stream. +Record exact ownership, exclusivity, cancellation, memory, concurrency, and residual boundaries +before adding a purpose-named RED suite. Do not check the broad Milestone 6 SFTP or POSIX system-SSH items yet. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets From ec51e36b77d8f3ca1c3505e509e0c7f909dcd3cf Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 13:56:25 -0400 Subject: [PATCH 228/267] feat(ssh): compose POSIX relay tree transfer Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 4 + .../ssh-relay-runtime-workflow.test.mjs | 2 + ...elease-implementation-checklist-summary.md | 23 +- ...github-release-implementation-checklist.md | 153 ++++++- ...elay-runtime-posix-control-command.test.ts | 196 +++++++++ ...ssh-relay-runtime-posix-control-command.ts | 159 ++++++++ ...-relay-runtime-posix-tree-transfer.test.ts | 375 ++++++++++++++++++ .../ssh-relay-runtime-posix-tree-transfer.ts | 193 +++++++++ 8 files changed, 1088 insertions(+), 17 deletions(-) create mode 100644 src/main/ssh/ssh-relay-runtime-posix-control-command.test.ts create mode 100644 src/main/ssh/ssh-relay-runtime-posix-control-command.ts create mode 100644 src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts create mode 100644 src/main/ssh/ssh-relay-runtime-posix-tree-transfer.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 3ee2724d17c..eda2274aacd 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -286,7 +286,9 @@ jobs: src/main/ssh/ssh-relay-runtime-source-tree.test.ts \ src/main/ssh/ssh-relay-runtime-source-scan.test.ts \ src/main/ssh/ssh-relay-runtime-source-stream.test.ts \ + src/main/ssh/ssh-relay-runtime-posix-control-command.test.ts \ src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts \ + src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts \ src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.test.ts \ @@ -933,7 +935,9 @@ jobs: src/main/ssh/ssh-relay-runtime-source-tree.test.ts ` src/main/ssh/ssh-relay-runtime-source-scan.test.ts ` src/main/ssh/ssh-relay-runtime-source-stream.test.ts ` + src/main/ssh/ssh-relay-runtime-posix-control-command.test.ts ` src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts ` + src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts ` src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-connection-transfer.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index c93fb9e2158..32d559a9ff0 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -164,7 +164,9 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-runtime-source-tree.test.ts', 'ssh-relay-runtime-source-scan.test.ts', 'ssh-relay-runtime-source-stream.test.ts', + 'ssh-relay-runtime-posix-control-command.test.ts', 'ssh-relay-runtime-posix-file-destination.test.ts', + 'ssh-relay-runtime-posix-tree-transfer.test.ts', 'ssh-relay-runtime-system-ssh-file-channel.test.ts', 'ssh-relay-runtime-sftp-file-destination.test.ts', 'ssh-relay-runtime-sftp-connection-transfer.test.ts', 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 54f48290c78..94c6a58cc6c 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 @@ -9,9 +9,21 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — authenticated system-SSH -command-channel adapter is exact-head green; disconnected tree/root composition audit is next, -2026-07-15, Codex implementation owner.** `E-M6-POSIX-SYSTEM-SSH-CHANNEL-LOCAL-001` records 13/13 +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected POSIX +system-SSH tree/root composition is locally green and exact-head native evidence is next, +2026-07-15, Codex implementation owner.** +`E-M6-POSIX-SYSTEM-SSH-TREE-AUDIT-001` requires a bounded no-input control-command owner rather than +the legacy unbounded-output command helper, exclusive `0700` root creation, parent-first declared +directories, exact per-file source-stream composition, owned-root cleanup, and a worst-case +cancellation/cleanup join below 10 seconds. It remains disconnected and defaults to one channel. +`E-M6-POSIX-SYSTEM-SSH-TREE-LOCAL-RED-001` proves both purpose suites fail solely because the audited +production modules are absent while the native workflow oracle passes 8/8. +`E-M6-POSIX-SYSTEM-SSH-TREE-LOCAL-001` records 15/15 purpose, 219 focused cases with one declared +skip, 656 broad relay cases with six declared skips, 281/281 release contracts, full lint/typecheck, +and static/isolation proof. The new tree owner has no production consumer and the protected legacy +Node/npm resolver diff remains untouched. Live/full-size/high-RTT OpenSSH and every product/default +path remain explicit gaps until later packages. +`E-M6-POSIX-SYSTEM-SSH-CHANNEL-LOCAL-001` records 13/13 purpose, 150/150 focused, 641 broad relay cases with six declared skips, 281/281 release contracts, and all static/isolation gates. `E-M6-POSIX-SYSTEM-SSH-CHANNEL-CI-001` proves the 13 cases on all six primary native clients; both Linux supplements, Windows x64 floor, PR Checks, computer-use Windows/ @@ -22,9 +34,8 @@ versus required 26100 rejection after the complete 85,213,511-byte/42-file runti settlement, and idempotent SIGTERM/SIGKILL hooks consumed by the proven single per-file cancellation owner. The adapter prechecks but does not forward that signal into `exec`, avoiding a duplicate immediate SIGTERM listener. -No direct spawn, legacy uploader change, real tree/live claim, or product path is included. Audit the -next smallest disconnected tree/root composition package; legacy remains the only production/default -path and SignPath remains deferred. +No direct spawn, legacy uploader change, live claim, or product path is included. Collect exact-head +native evidence next; legacy remains the only production/default path and SignPath remains deferred. `E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001` records 25/25 purpose, 628 broad relay, 281/281 release, and all static/isolation gates. `E-M6-POSIX-SYSTEM-SSH-FILE-CI-001` proves the 25 cases on all six primary native clients; both Linux 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 d35d3eb026a..3703eda661e 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected POSIX system-SSH/no-tar transfer composition**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **SYSTEM-SSH COMMAND-CHANNEL ADAPTER EXACT-HEAD GREEN / TREE-ROOT COMPOSITION AUDIT NEXT — 2026-07-15, Codex implementation owner** — `E-M6-POSIX-SYSTEM-SSH-CHANNEL-LOCAL-001` and `E-M6-POSIX-SYSTEM-SSH-CHANNEL-CI-001` prove the adapter and per-file composition locally and on all six primary native clients, both Linux supplements, Windows x64 floor, PR Checks, computer-use, and Golden E2E. Windows arm64 retains only the declared hosted build-26200 versus required-26100 gap after complete runtime smoke. The package remains disconnected and has no tree or live system-SSH claim. Audit the next smallest disconnected tree/root composition package before adding RED tests. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Session checkpoint: **POSIX SYSTEM-SSH TREE/ROOT LOCAL GREEN / EXACT-HEAD NATIVE EVIDENCE NEXT — 2026-07-15, Codex implementation owner** — `E-M6-POSIX-SYSTEM-SSH-TREE-LOCAL-001` proves the disconnected bounded control-command and tree-transfer owners with 15/15 purpose, 219 focused cases with one declared skip, 656 broad relay cases with six declared skips, 281/281 release contracts, full lint/typecheck, and static/isolation gates. Commit and push only this package, then require exact-head native evidence before selecting another package. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -153,12 +153,15 @@ same change as the work it records. - Completed package: the disconnected POSIX no-tar per-file command/destination boundary is green locally and on every primary native client under E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001 and E-M6-POSIX-SYSTEM-SSH-FILE-CI-001. It has no real SSH or product caller. -- Active package: **Local implementation complete / exact-head CI pending — 2026-07-15, Codex - implementation owner** — the purpose-named adapter fixed by - E-M6-POSIX-SYSTEM-SSH-CHANNEL-AUDIT-001 is green under - E-M6-POSIX-SYSTEM-SSH-CHANNEL-LOCAL-001. Do not import or modify the legacy tar/base64 directory - uploader, create a remote tree, add a semantic probe, or add product/settings/fallback/tuple/ - publication/default behavior. +- Completed package: the disconnected authenticated system-SSH command-channel adapter is green + locally and on every primary native client under E-M6-POSIX-SYSTEM-SSH-CHANNEL-LOCAL-001 and + E-M6-POSIX-SYSTEM-SSH-CHANNEL-CI-001. It has no tree/live/product caller. +- Active package: **Local green / exact-head native evidence pending — 2026-07-15, Codex + implementation owner** — the smallest disconnected POSIX system-SSH tree/root composition combines + only the proven authenticated source stream, exclusive per-file destination, and command-channel + adapter under E-M6-POSIX-SYSTEM-SSH-TREE-LOCAL-001. Do not import or modify the legacy tar/base64 + directory uploader, add a semantic probe, or add product/settings/fallback/tuple/publication/ + default behavior. - 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 @@ -17797,6 +17800,135 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs`. product path. No setting, Beta mode, fallback, tuple, publication, default behavior, or SignPath work exists; legacy remains the only production path. +### E-M6-POSIX-SYSTEM-SSH-TREE-AUDIT-001 — disconnected POSIX system-SSH staging-tree composition + +- Date/owner/base: 2026-07-15, Codex implementation owner; exact clean evidence base + `3cf9bae8f1ac8dd3d0ede4fdbb5c80e90cfe636b` after closing the authenticated channel adapter. +- Existing boundaries: compose only the scanned lease-backed source tree, + `streamSshRelayRuntimeSourceTree`, `openSshRelayRuntimePosixFileDestination`, and + `openSshRelayRuntimeSystemSshFileChannel`. Reject `execCommand`, `waitForChannelClose`, the legacy + tar uploader, and the legacy buffer/file uploader: they respectively retain unbounded output, + settle timeout/abort before forced child settlement, require remote tar, or implement a different + whole-buffer/default-path contract. +- Control-command prerequisite: add one purpose-named no-input POSIX control-command owner over the + proven channel adapter. It drains stdout and inherits the adapter's copied 16 KiB stderr bound, + ends stdin exactly once, requires exit zero, has a 30-second command ceiling, and on cancellation + or timeout requests SIGTERM, waits 250 ms, requests SIGKILL, and fails closed if settlement has + not completed by 2,000 ms. It preserves the primary abort/timeout/nonzero failure and joins close, + force, and settlement failures without putting the command or remote path in orchestrator-authored + diagnostics. +- Path/ownership contract: accept only a Linux/Darwin scanned tree, one normalized absolute non-root + POSIX staging path with no empty, dot, traversal, NUL, CR, or LF segment, one already-authenticated + system-SSH connection, the exact caller signal, and explicit one-to-four concurrency defaulting to + one. Use only POSIX `sh`, `mkdir`, `chmod`, `cat`, and `rm`: exclusively create the root under + `umask 077`, mark it owned only after successful command settlement, create signed directories in + parent-before-child order, and build child paths only from safe slash-delimited manifest segments. +- Stream/cleanup contract: open every file through the proven exclusive per-file destination and + authenticated command-channel adapter using the same caller signal. Preserve the source stream's + 64 KiB borrowed-buffer boundary, local SHA-256/source-mutation proof, exact 0644/0755 repair, + progress, counts, and one-to-four ceiling. On any post-ownership failure, wait for every active + destination to settle, then remove only the exclusively owned root with `rm -rf` and join cleanup + failure. A cleanup controller is deliberately distinct because the caller signal has already + requested cleanup; it aborts after 5 seconds and its control channel still has the 2-second forced + settlement ceiling. Together with the active per-file ceiling, worst-case cancellation and cleanup + join remains below the plan's 10-second budget. +- Required purpose RED/green: control success/nonzero/error, exact EOF, pre-abort, mid-command abort, + timeout, SIGTERM/SIGKILL order, missing process/false kill/settlement ceiling, and listener/timer + cleanup; tree OS/path/concurrency rejection before exec; exact quoted primitive-only commands; + root collision without cleanup; parent-first directories; exact file bytes/modes/result/progress; + one/four peak channels; source mutation/write failure/cancellation with no later writes; owned-root + cleanup after all active files settle; cleanup timeout/failure joining; lease assertions; and exact + inclusion of both purpose suites once in both native workflow command families before production. +- Explicit residual boundary: no semantic primitive probe/cache or compatibility classification, + optional tar fast path, dynamic `MaxSessions=1` policy, live/full-size/high-RTT system-SSH remote, + Windows PowerShell path, remote bundled-Node tree verification/install/publish/launch, product/ + Beta/settings/fallback/tuple/publication/default behavior, or SignPath is included. Legacy remains + the only production/default path and every tuple stays disabled. + +### E-M6-POSIX-SYSTEM-SSH-TREE-LOCAL-RED-001 — audited control and tree owners are absent + +- Date/owner/base: 2026-07-15, Codex implementation owner; audit, test, and native-workflow-only diff + atop exact clean evidence base `3cf9bae8f1ac8dd3d0ede4fdbb5c80e90cfe636b`. +- Command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-runtime-posix-control-command.test.ts +src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs`. +- Result: expected RED, exit 1. Both new purpose suites collect zero cases and each fails solely with + `Cannot find module` for its audited production module. The independent workflow oracle passes all + 8/8 cases and proves both suites are pinned exactly once in both native command families. +- Resource evidence: Vitest duration 978 ms; `/usr/bin/time -l` reports 2.17 seconds real, + 141,410,304-byte maximum RSS, 96,310,792-byte peak memory footprint, zero swaps, and zero block I/O. +- Disposition: RED is causally narrow and accepted. Implement only the audited bounded control and + tree modules, then require purpose composition GREEN plus focused, broad, static, isolation, and + exact-head native evidence. No semantic probe, live/full-size system SSH, product path, fallback, + tuple, publication, default behavior, or SignPath claim follows from this evidence. + +### E-M6-POSIX-SYSTEM-SSH-TREE-LOCAL-001 — disconnected bounded staging-tree composition is green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted package atop exact base + `3cf9bae8f1ac8dd3d0ede4fdbb5c80e90cfe636b` on Apple arm64, macOS 26.2 build 25C56, Node 26.0.0, + pnpm 10.24.0. The local Node version is newer than the repository's Node 24 contract; exact-head + native workflow jobs remain required before closing the package. +- Implementation: `ssh-relay-runtime-posix-control-command.ts` owns exact EOF/exit settlement, a + 30-second ceiling, 250 ms graceful close, and 2,000 ms forced-settlement ceiling over the proven + copied-diagnostic/drained-output adapter. `ssh-relay-runtime-posix-tree-transfer.ts` validates an + absolute normalized POSIX staging root, exclusively creates it under `umask 077`, creates signed + directories parent-first with mode `0755`, streams every signed file through the exclusive + destination with one-to-four channels defaulting to one, and cleans only its owned root with a + separate five-second abort controller. Runtime-invalid staging-root input was corrected before + final GREEN so validation cannot throw an incidental `.split` type error. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts + --maxWorkers=1 src/main/ssh/ssh-relay-runtime-posix-control-command.test.ts + src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts`. Result: 2 files and 15/15 cases pass; + Vitest 1.95 seconds, 2.96 seconds real, 139,640,832-byte maximum RSS, 95,851,848-byte peak memory + footprint, zero swaps and block I/O. Cases cover exact/no-input success, nonzero/error, pre/mid + abort, timeout, graceful/forced settlement and joined failures; invalid OS/path/concurrency before + exec; exact quoted primitive-only root/directory/file commands, parent-first order, bytes/modes/ + progress, default-one/four-channel bounds, collision ownership, cancellation settlement, and + cleanup failure/timeout joining. +- Focused composition command: `/usr/bin/time -l pnpm exec vitest run --config + config/vitest.config.ts --maxWorkers=1` with the two new purpose suites plus + `ssh-relay-runtime-posix-file-destination.test.ts`, + `ssh-relay-runtime-system-ssh-file-channel.test.ts`, the source-tree/scan/stream suites, + `ssh-system-fallback.test.ts`, `ssh-connection.test.ts`, and + `config/scripts/ssh-relay-runtime-workflow.test.mjs`. Result: 10 files, 219 passed and one declared + platform skip; Vitest 9.02 seconds, 10.51 seconds real, 156,794,880-byte maximum RSS, + 97,457,504-byte peak memory footprint, zero swaps and block I/O. This includes source mutation, + lease, retained-buffer/no-later-write, missing process/false kill, diagnostic, child settlement, + and exact native-workflow-family contracts inherited from the composed boundaries. +- Broad relay command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts + --maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts`. Result: 50 files passed, four files skipped; + 656 passed and six declared skips out of 662. Vitest 55.58 seconds, 58.05 seconds real, + 254,115,840-byte maximum RSS, 96,458,248-byte peak memory footprint, zero swaps and block I/O. + Skips remain the declared case-fold/full-size/live SSH cells and are not treated as this package's + evidence. +- Release-contract command: `/usr/bin/time -l pnpm exec vitest run --config + config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs`. Result: 50 + files and 281/281 cases pass; Vitest 37.74 seconds, 41.85 seconds real, 190,676,992-byte maximum + RSS, 97,195,384-byte peak memory footprint, zero swaps and block I/O. +- Static commands/results: `/usr/bin/time -l pnpm typecheck` passes in 8.38 seconds real with + 1,225,637,888-byte maximum RSS and 97,588,552-byte peak footprint. `/usr/bin/time -l pnpm lint` + passes in 61.60 seconds real with 2,049,687,552-byte maximum RSS and 96,621,872-byte peak + footprint; its warnings are pre-existing outside this diff. The lint aggregate proves 41 + reliability gates, the 355-entry max-lines ratchet with no new bypass, bundled guides, and all + localization checks. Targeted `oxlint` and `oxfmt --check` pass. The obsolete attempted + `pnpm lint:max-lines` spelling was replaced by the repository's current + `pnpm check:max-lines-ratchet` command; no checklist box was credited to the failed spelling. +- Isolation evidence: `git diff --check` and no-index whitespace checks for all four new files are + clean. `git diff --exit-code HEAD -- src/main/ssh/ssh-remote-node-resolution.ts + src/main/ssh/ssh-remote-node-resolution.test.ts` is empty, preserving the protected Milestone 0 + resolver. The only implementation references to the exported tree owner are its definition and + purpose test; the control owner is consumed only by that disconnected tree owner and its test. + No product, settings, legacy uploader, fallback, tuple, publication, or default-path importer + exists. The four implementation/test files are 159/196/193/375 lines and contain no max-lines + bypass. +- Residual boundary: this evidence uses a realistic fake `ClientChannel` over the real adapter, not + a live OpenSSH daemon or full-size runtime. Semantic primitive probe/cache, optional tar fast path, + dynamic `MaxSessions=1`, live/full-size/high-RTT POSIX system SSH, Windows transfer, remote bundled- + Node verification/install/publish/launch, Beta/product/fallback/default wiring, tuple enablement, + release publication, and SignPath remain absent. Exact-head native workflow, PR Checks, + computer-use, and Golden E2E run IDs are still required before this package is closed. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -17855,10 +17987,9 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Audit the smallest disconnected POSIX system-SSH tree/root composition package that can combine the -proven authenticated exec-channel adapter, per-file destination, and authenticated source stream. -Record exact ownership, exclusivity, cancellation, memory, concurrency, and residual boundaries -before adding a purpose-named RED suite. Do not +Commit and push only the locally green bounded control-command/tree composition package proven by +E-M6-POSIX-SYSTEM-SSH-TREE-LOCAL-001, then require exact-head native workflow, PR Checks, +computer-use, and Golden E2E evidence. Do not check the broad Milestone 6 SFTP or POSIX system-SSH items yet. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets diff --git a/src/main/ssh/ssh-relay-runtime-posix-control-command.test.ts b/src/main/ssh/ssh-relay-runtime-posix-control-command.test.ts new file mode 100644 index 00000000000..36dc5532868 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-posix-control-command.test.ts @@ -0,0 +1,196 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import type { SshRelayRuntimePosixFileChannel } from './ssh-relay-runtime-posix-file-destination' +import { + runSshRelayRuntimePosixControlCommand, + SSH_RELAY_RUNTIME_POSIX_CONTROL_COMMAND_LIMITS +} from './ssh-relay-runtime-posix-control-command' + +function createChannel(): { + channel: SshRelayRuntimePosixFileChannel + settle: (error?: unknown) => void + end: ReturnType void>> + requestClose: ReturnType void>> + forceClose: ReturnType void>> +} { + let resolveSettlement: (() => void) | undefined + let rejectSettlement: ((error: unknown) => void) | undefined + const settled = new Promise((resolve, reject) => { + resolveSettlement = resolve + rejectSettlement = reject + }) + const end = vi.fn() + const requestClose = vi.fn() + const forceClose = vi.fn() + return { + channel: Object.freeze({ + write: vi.fn(), + end, + settled, + requestClose, + forceClose + }), + settle: (error?: unknown) => + error === undefined ? resolveSettlement?.() : rejectSettlement?.(error), + end, + requestClose, + forceClose + } +} + +afterEach(() => { + vi.useRealTimers() +}) + +describe('SSH relay runtime POSIX control command', () => { + it('rejects invalid input and pre-abort before opening a channel', async () => { + const openChannel = vi.fn() + const controller = new AbortController() + const reason = new Error('cancelled before control command') + controller.abort(reason) + + await expect( + runSshRelayRuntimePosixControlCommand({ + command: 'mkdir stage', + signal: controller.signal, + openChannel + }) + ).rejects.toBe(reason) + expect(openChannel).not.toHaveBeenCalled() + + await expect( + runSshRelayRuntimePosixControlCommand({ + command: '', + signal: new AbortController().signal, + openChannel + }) + ).rejects.toThrow(/input/i) + }) + + it('opens the exact command, ends stdin once, and awaits successful settlement', async () => { + const fixture = createChannel() + const controller = new AbortController() + const removeAbortListener = vi.spyOn(controller.signal, 'removeEventListener') + const openChannel = vi.fn(async () => fixture.channel) + const running = runSshRelayRuntimePosixControlCommand({ + command: "umask 077; mkdir '/owned-stage'", + signal: controller.signal, + openChannel + }) + + await vi.waitFor(() => expect(fixture.end).toHaveBeenCalledOnce()) + expect(openChannel).toHaveBeenCalledWith("umask 077; mkdir '/owned-stage'", controller.signal) + expect(await Promise.race([running.then(() => 'settled'), Promise.resolve('pending')])).toBe( + 'pending' + ) + fixture.settle() + await expect(running).resolves.toBeUndefined() + expect(fixture.requestClose).not.toHaveBeenCalled() + expect(fixture.forceClose).not.toHaveBeenCalled() + expect(removeAbortListener).toHaveBeenCalledWith('abort', expect.any(Function)) + }) + + it('propagates nonzero and channel errors without replacing them', async () => { + for (const error of [new Error('exit 7'), new Error('channel failed')]) { + const fixture = createChannel() + const running = runSshRelayRuntimePosixControlCommand({ + command: 'mkdir stage', + signal: new AbortController().signal, + openChannel: async () => fixture.channel + }) + await vi.waitFor(() => expect(fixture.end).toHaveBeenCalledOnce()) + fixture.settle(error) + await expect(running).rejects.toBe(error) + } + }) + + it('settles a mid-command abort gracefully before returning the exact reason', async () => { + const fixture = createChannel() + const controller = new AbortController() + const reason = new Error('cancelled control command') + fixture.requestClose.mockImplementation(() => fixture.settle()) + const running = runSshRelayRuntimePosixControlCommand({ + command: 'mkdir stage', + signal: controller.signal, + openChannel: async () => fixture.channel + }) + + await vi.waitFor(() => expect(fixture.end).toHaveBeenCalledOnce()) + controller.abort(reason) + await expect(running).rejects.toBe(reason) + expect(fixture.requestClose).toHaveBeenCalledOnce() + expect(fixture.forceClose).not.toHaveBeenCalled() + }) + + it('forces an unsettled abort after the graceful window', async () => { + vi.useFakeTimers() + const fixture = createChannel() + const controller = new AbortController() + const reason = new Error('force cancelled control command') + fixture.forceClose.mockImplementation(() => fixture.settle()) + const running = runSshRelayRuntimePosixControlCommand({ + command: 'mkdir stage', + signal: controller.signal, + openChannel: async () => fixture.channel + }) + + await vi.waitFor(() => expect(fixture.end).toHaveBeenCalledOnce()) + const rejection = expect(running).rejects.toBe(reason) + controller.abort(reason) + await vi.advanceTimersByTimeAsync( + SSH_RELAY_RUNTIME_POSIX_CONTROL_COMMAND_LIMITS.gracefulCloseMs + ) + await rejection + expect(fixture.requestClose).toHaveBeenCalledOnce() + expect(fixture.forceClose).toHaveBeenCalledOnce() + }) + + it('turns the command ceiling into bounded graceful cancellation', async () => { + vi.useFakeTimers() + const fixture = createChannel() + fixture.requestClose.mockImplementation(() => fixture.settle()) + const running = runSshRelayRuntimePosixControlCommand({ + command: 'mkdir stage', + signal: new AbortController().signal, + openChannel: async () => fixture.channel + }) + + const rejection = expect(running).rejects.toThrow(/timed out/i) + await vi.advanceTimersByTimeAsync( + SSH_RELAY_RUNTIME_POSIX_CONTROL_COMMAND_LIMITS.commandTimeoutMs + ) + await rejection + expect(fixture.requestClose).toHaveBeenCalledOnce() + expect(fixture.forceClose).not.toHaveBeenCalled() + }) + + it('fails closed at the forced-settlement ceiling and joins termination failures', async () => { + vi.useFakeTimers() + const fixture = createChannel() + fixture.requestClose.mockImplementation(() => { + throw new Error('graceful close failed') + }) + fixture.forceClose.mockImplementation(() => { + throw new Error('forced close failed') + }) + const controller = new AbortController() + const reason = new Error('cancelled forever') + const running = runSshRelayRuntimePosixControlCommand({ + command: 'mkdir stage', + signal: controller.signal, + openChannel: async () => fixture.channel + }) + + controller.abort(reason) + const rejection = expect(running).rejects.toMatchObject({ + errors: expect.arrayContaining([ + reason, + expect.objectContaining({ message: 'graceful close failed' }), + expect.objectContaining({ message: 'forced close failed' }), + expect.objectContaining({ message: expect.stringMatching(/settlement timed out/i) }) + ]) + }) + await vi.advanceTimersByTimeAsync(SSH_RELAY_RUNTIME_POSIX_CONTROL_COMMAND_LIMITS.totalCloseMs) + await rejection + }) +}) diff --git a/src/main/ssh/ssh-relay-runtime-posix-control-command.ts b/src/main/ssh/ssh-relay-runtime-posix-control-command.ts new file mode 100644 index 00000000000..37efa0a5da0 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-posix-control-command.ts @@ -0,0 +1,159 @@ +import type { SshRelayRuntimePosixFileChannel } from './ssh-relay-runtime-posix-file-destination' + +export type RunSshRelayRuntimePosixControlCommandOptions = Readonly<{ + command: string + signal: AbortSignal + openChannel: (command: string, signal: AbortSignal) => Promise +}> + +const COMMAND_TIMEOUT_MS = 30_000 +const GRACEFUL_CLOSE_MS = 250 +const TOTAL_CLOSE_MS = 2_000 + +type ChannelSettlement = Readonly<{ ok: true } | { ok: false; error: unknown }> + +function validateOptions(options: RunSshRelayRuntimePosixControlCommandOptions): void { + if ( + !options || + typeof options.command !== 'string' || + options.command === '' || + options.command.includes('\0') || + options.command.includes('\r') || + options.command.includes('\n') || + !options.signal || + typeof options.openChannel !== 'function' + ) { + throw new Error('SSH relay runtime POSIX control command input is invalid') + } +} + +function validateChannel(channel: SshRelayRuntimePosixFileChannel): void { + if ( + !channel || + typeof channel.end !== 'function' || + typeof channel.settled?.then !== 'function' || + typeof channel.requestClose !== 'function' || + typeof channel.forceClose !== 'function' + ) { + throw new Error('SSH relay runtime POSIX control command channel is invalid') + } +} + +function waitForSettlement( + settlement: Promise, + timeoutMs: number +): Promise { + return new Promise((resolve) => { + const timeout = setTimeout(() => resolve(false), timeoutMs) + void settlement.then(() => { + clearTimeout(timeout) + resolve(true) + }) + }) +} + +async function terminateChannel( + channel: SshRelayRuntimePosixFileChannel, + settlement: Promise +): Promise { + const failures: unknown[] = [] + try { + channel.requestClose() + } catch (error) { + failures.push(error) + } + const graceful = await waitForSettlement(settlement, GRACEFUL_CLOSE_MS) + if (graceful) { + return failures + } + try { + channel.forceClose() + } catch (error) { + failures.push(error) + } + const forced = await waitForSettlement(settlement, TOTAL_CLOSE_MS - GRACEFUL_CLOSE_MS) + if (!forced) { + failures.push(new Error('SSH relay runtime POSIX control command settlement timed out')) + } + return failures +} + +function joinedFailure(primary: unknown, cleanupFailures: readonly unknown[]): unknown { + if (cleanupFailures.length === 0) { + return primary + } + return new AggregateError( + [primary, ...cleanupFailures], + 'SSH relay runtime POSIX control command termination failed' + ) +} + +export async function runSshRelayRuntimePosixControlCommand( + options: RunSshRelayRuntimePosixControlCommandOptions +): Promise { + validateOptions(options) + const { command, signal, openChannel } = options + signal.throwIfAborted() + const channel = await openChannel(command, signal) + validateChannel(channel) + const settlement: Promise = channel.settled.then( + () => ({ ok: true }), + (error: unknown) => ({ ok: false, error }) + ) + let interrupted = false + let interruptionReason: unknown + let resolveInterruption: (() => void) | undefined + const interruption = new Promise((resolve) => { + resolveInterruption = resolve + }) + let termination = Promise.resolve([]) + const interrupt = (reason: unknown): void => { + if (interrupted) { + return + } + interrupted = true + interruptionReason = reason + // Why: callers may reuse a session slot only after the local OpenSSH child has settled. + termination = terminateChannel(channel, settlement) + resolveInterruption?.() + } + const onAbort = (): void => interrupt(signal.reason) + const commandTimeout = setTimeout( + () => interrupt(new Error('SSH relay runtime POSIX control command timed out')), + COMMAND_TIMEOUT_MS + ) + signal.addEventListener('abort', onAbort, { once: true }) + if (signal.aborted) { + onAbort() + } + + try { + if (!interrupted) { + try { + channel.end() + } catch (error) { + interrupt(error) + } + } + const outcome = await Promise.race([ + settlement.then((value) => ({ kind: 'settled' as const, value })), + interruption.then(() => ({ kind: 'interrupted' as const })) + ]) + if (outcome.kind === 'interrupted') { + const cleanupFailures = await termination + throw joinedFailure(interruptionReason, cleanupFailures) + } + if (!outcome.value.ok) { + throw outcome.value.error + } + } finally { + clearTimeout(commandTimeout) + signal.removeEventListener('abort', onAbort) + } +} + +export const SSH_RELAY_RUNTIME_POSIX_CONTROL_COMMAND_LIMITS = Object.freeze({ + commandTimeoutMs: COMMAND_TIMEOUT_MS, + gracefulCloseMs: GRACEFUL_CLOSE_MS, + totalCloseMs: TOTAL_CLOSE_MS +}) diff --git a/src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts b/src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts new file mode 100644 index 00000000000..cb7c5543221 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts @@ -0,0 +1,375 @@ +import type { ChildProcess } from 'node:child_process' +import { EventEmitter } from 'node:events' +import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import type { ClientChannel } from 'ssh2' +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { publishSshRelayArtifactCacheEntry } from './ssh-relay-artifact-cache-entry' +import { createSshRelayArtifactCacheEntryFixture } from './ssh-relay-artifact-cache-entry-fixture' +import { + acquireSshRelayArtifactCacheInUseLease, + type SshRelayArtifactCacheInUseLease +} from './ssh-relay-artifact-cache-in-use-lease' +import { scanSshRelayRuntimeSourceTree } from './ssh-relay-runtime-source-scan' +import { + transferSshRelayRuntimeTreeViaPosixSystemSsh, + SSH_RELAY_RUNTIME_POSIX_TREE_TRANSFER_LIMITS +} from './ssh-relay-runtime-posix-tree-transfer' +import { createSshRelayRuntimeSourceTree } from './ssh-relay-runtime-source-tree' +import type { SshRelayRuntimeSystemSshConnection } from './ssh-relay-runtime-system-ssh-file-channel' + +type WriteCallback = (error?: Error) => void +type FakeChannel = EventEmitter & { + stdin: { write: (chunk: Buffer, callback: WriteCallback) => boolean; end: () => void } + stderr: EventEmitter + resume: () => FakeChannel + close: () => void + _process: ChildProcess +} + +type ConnectionBehavior = { + rootExit?: number + cleanupExit?: number + writeError?: Error + retainFirstWrite?: boolean + hangCleanup?: boolean +} + +const cleanupRoots = new Set() +const cleanupLeases = new Set() + +async function treeFixture(os: 'linux' | 'win32' = 'linux') { + const root = await mkdtemp(join(tmpdir(), 'orca-relay-posix-tree-')) + cleanupRoots.add(root) + const inputRoot = join(root, 'input') + await mkdir(inputRoot) + const fixture = await createSshRelayArtifactCacheEntryFixture({ root: inputRoot, os }) + const cacheRoot = join(root, 'cache') + const entry = await publishSshRelayArtifactCacheEntry({ + cacheRoot, + artifact: fixture.artifact, + archivePath: fixture.archivePath + }) + const lease = await acquireSshRelayArtifactCacheInUseLease({ cacheRoot, entry }) + cleanupLeases.add(lease) + const source = createSshRelayRuntimeSourceTree({ + kind: 'ready', + source: 'cache', + artifact: fixture.artifact, + entry, + lease + }) + return scanSshRelayRuntimeSourceTree(source, new AbortController().signal) +} + +function quote(value: string): string { + return `'${value.replaceAll("'", "'\\''")}'` +} + +function createConnection(behavior: ConnectionBehavior = {}): { + connection: SshRelayRuntimeSystemSshConnection + commands: string[] + fileBytes: Map + writes: () => number + peakFiles: () => number + releaseWrites: () => void +} { + const commands: string[] = [] + const fileBytes = new Map() + const retainedWrites: WriteCallback[] = [] + let writeCount = 0 + let activeFiles = 0 + let peakFiles = 0 + let writeFailed = false + let writeRetained = false + + const exec = vi.fn(async (command: string) => { + commands.push(command) + const isFile = command.includes('; cat > ') + const isCleanup = command.startsWith('rm -rf ') + const isRoot = command.startsWith('umask 077; mkdir ') && !command.includes(' && chmod ') + const channel = new EventEmitter() as FakeChannel + let closed = false + const emitClose = (code: number | null, signal?: NodeJS.Signals | null): void => { + if (closed) { + return + } + closed = true + if (isFile) { + activeFiles -= 1 + } + queueMicrotask(() => channel.emit('close', code, signal)) + } + if (isFile) { + activeFiles += 1 + peakFiles = Math.max(peakFiles, activeFiles) + fileBytes.set(command, Buffer.alloc(0)) + } + channel.stderr = new EventEmitter() + channel.resume = () => channel + channel.stdin = { + write: (chunk, callback) => { + writeCount += 1 + if (behavior.writeError && !writeFailed) { + writeFailed = true + callback(behavior.writeError) + return true + } + const current = fileBytes.get(command) ?? Buffer.alloc(0) + fileBytes.set(command, Buffer.concat([current, Buffer.from(chunk)])) + if (behavior.retainFirstWrite && !writeRetained) { + writeRetained = true + retainedWrites.push(callback) + return true + } + callback() + return true + }, + end: () => { + if (isCleanup && behavior.hangCleanup) { + return + } + const code = isCleanup ? (behavior.cleanupExit ?? 0) : isRoot ? (behavior.rootExit ?? 0) : 0 + if (code !== 0) { + channel.stderr.emit('data', Buffer.from('remote command rejected')) + } + emitClose(code, null) + } + } + channel.close = () => { + if (isCleanup && behavior.hangCleanup) { + return + } + emitClose(null, 'SIGTERM') + } + channel._process = { + exitCode: null, + signalCode: null, + kill: vi.fn(() => { + if (!isCleanup || !behavior.hangCleanup) { + emitClose(null, 'SIGKILL') + } + return true + }) + } as unknown as ChildProcess + return channel as unknown as ClientChannel + }) + return { + connection: { usesSystemSshTransport: () => true, exec }, + commands, + fileBytes, + writes: () => writeCount, + peakFiles: () => peakFiles, + releaseWrites: () => retainedWrites.splice(0).forEach((callback) => callback()) + } +} + +afterEach(async () => { + vi.useRealTimers() + await Promise.all([...cleanupLeases].map((lease) => lease.release().catch(() => {}))) + cleanupLeases.clear() + await Promise.all([...cleanupRoots].map((root) => rm(root, { recursive: true, force: true }))) + cleanupRoots.clear() +}) + +describe('SSH relay runtime POSIX system-SSH tree transfer', () => { + it('rejects Windows, unsafe roots, and invalid concurrency before exec', async () => { + const windows = await treeFixture('win32') + const linux = await treeFixture() + const fixture = createConnection() + const base = { + connection: fixture.connection, + signal: new AbortController().signal + } + + await expect( + transferSshRelayRuntimeTreeViaPosixSystemSsh({ + ...base, + tree: windows, + remoteStagingRoot: '/stage/content' + }) + ).rejects.toThrow(/POSIX tree/i) + await expect( + transferSshRelayRuntimeTreeViaPosixSystemSsh({ + ...base, + tree: linux, + remoteStagingRoot: '../stage' + }) + ).rejects.toThrow(/staging root/i) + await expect( + transferSshRelayRuntimeTreeViaPosixSystemSsh({ + ...base, + tree: linux, + remoteStagingRoot: undefined as unknown as string + }) + ).rejects.toThrow(/staging root/i) + await expect( + transferSshRelayRuntimeTreeViaPosixSystemSsh({ + ...base, + tree: linux, + remoteStagingRoot: '/stage/content', + maximumConcurrency: 5 + }) + ).rejects.toThrow(/concurrency/i) + expect(fixture.commands).toEqual([]) + }) + + it('creates an exclusive tree, streams exact bytes/modes, and returns progress', async () => { + const tree = await treeFixture() + const fixture = createConnection() + const root = "/tmp/orca user's/content" + const progress: unknown[] = [] + const result = await transferSshRelayRuntimeTreeViaPosixSystemSsh({ + tree, + connection: fixture.connection, + remoteStagingRoot: root, + maximumConcurrency: 2, + signal: new AbortController().signal, + onProgress: (value) => progress.push(value) + }) + + expect(result).toMatchObject({ + remoteStagingRoot: root, + filesCompleted: tree.fileCount, + bytesTransferred: tree.expandedBytes + }) + expect(fixture.commands[0]).toBe(`umask 077; mkdir ${quote(root)}`) + const firstFile = fixture.commands.findIndex((command) => command.includes('; cat > ')) + const lastDirectory = fixture.commands.findLastIndex( + (command) => command.includes(' && chmod ') && !command.includes('; cat > ') + ) + expect(firstFile).toBeGreaterThan(lastDirectory) + const expectedDirectoryCommands = [...tree.directories] + .sort( + (left, right) => + left.path.split('/').length - right.path.split('/').length || + (left.path < right.path ? -1 : left.path > right.path ? 1 : 0) + ) + .map((directory) => { + const remotePath = `${root}/${directory.path}` + return `umask 077; mkdir ${quote(remotePath)} && chmod 0755 ${quote(remotePath)}` + }) + expect(fixture.commands.slice(1, firstFile)).toEqual(expectedDirectoryCommands) + for (const forbidden of ['node ', 'python', 'perl', 'tar ', 'base64', 'sha256sum', 'shasum']) { + expect(fixture.commands.join('\n')).not.toContain(forbidden) + } + for (const file of tree.files) { + const remotePath = `${root}/${file.path}` + const command = fixture.commands.find( + (candidate) => + candidate.includes(`cat > ${quote(remotePath)}`) && + candidate.includes(`chmod ${file.mode.toString(8).padStart(4, '0')} ${quote(remotePath)}`) + ) + expect(command).toBeDefined() + expect(fixture.fileBytes.get(command as string)).toEqual(await readFile(file.localPath)) + } + expect(progress.length).toBeGreaterThan(0) + }) + + it('does not clean a pre-existing root when exclusive creation fails', async () => { + const tree = await treeFixture() + const fixture = createConnection({ rootExit: 1 }) + await expect( + transferSshRelayRuntimeTreeViaPosixSystemSsh({ + tree, + connection: fixture.connection, + remoteStagingRoot: '/existing/content', + signal: new AbortController().signal + }) + ).rejects.toThrow(/exit 1/i) + expect(fixture.commands.some((command) => command.startsWith('rm -rf '))).toBe(false) + }) + + it('permits at most four active file channels', async () => { + const tree = await treeFixture() + const fixture = createConnection({ retainFirstWrite: true }) + const transfer = transferSshRelayRuntimeTreeViaPosixSystemSsh({ + tree, + connection: fixture.connection, + remoteStagingRoot: '/stage/content', + maximumConcurrency: 4, + signal: new AbortController().signal + }) + + await vi.waitFor(() => expect(fixture.peakFiles()).toBe(Math.min(4, tree.fileCount))) + fixture.releaseWrites() + await transfer + expect(fixture.peakFiles()).toBeLessThanOrEqual(4) + }) + + it('defaults to one active file channel', async () => { + const tree = await treeFixture() + const fixture = createConnection({ retainFirstWrite: true }) + const transfer = transferSshRelayRuntimeTreeViaPosixSystemSsh({ + tree, + connection: fixture.connection, + remoteStagingRoot: '/stage/content', + signal: new AbortController().signal + }) + + await vi.waitFor(() => expect(fixture.peakFiles()).toBe(1)) + fixture.releaseWrites() + await transfer + expect(fixture.peakFiles()).toBe(1) + }) + + it('settles retained writes before cleaning its owned root on cancellation', async () => { + const tree = await treeFixture() + const fixture = createConnection({ retainFirstWrite: true }) + const controller = new AbortController() + const reason = new Error('cancelled tree transfer') + const transfer = transferSshRelayRuntimeTreeViaPosixSystemSsh({ + tree, + connection: fixture.connection, + remoteStagingRoot: '/stage/content', + signal: controller.signal + }) + + await vi.waitFor(() => expect(fixture.writes()).toBe(1)) + controller.abort(reason) + await expect(transfer).rejects.toBe(reason) + expect(fixture.commands.at(-1)).toBe("rm -rf '/stage/content'") + expect(fixture.writes()).toBe(1) + }) + + it('joins transfer and owned-root cleanup failures', async () => { + const tree = await treeFixture() + const primary = new Error('remote disk full') + const fixture = createConnection({ writeError: primary, cleanupExit: 9 }) + const transfer = transferSshRelayRuntimeTreeViaPosixSystemSsh({ + tree, + connection: fixture.connection, + remoteStagingRoot: '/private/content', + signal: new AbortController().signal + }) + + await expect(transfer).rejects.toMatchObject({ + errors: expect.arrayContaining([ + primary, + expect.objectContaining({ message: expect.stringMatching(/exit 9/i) }) + ]) + }) + expect(fixture.commands.at(-1)).toBe("rm -rf '/private/content'") + }) + + it('bounds an unresponsive owned-root cleanup after cancellation', async () => { + vi.useFakeTimers() + const tree = await treeFixture() + const fixture = createConnection({ writeError: new Error('write failed'), hangCleanup: true }) + const transfer = transferSshRelayRuntimeTreeViaPosixSystemSsh({ + tree, + connection: fixture.connection, + remoteStagingRoot: '/stage/content', + signal: new AbortController().signal + }) + const rejection = expect(transfer).rejects.toThrow(/cleanup|settlement timed out/i) + + await vi.waitFor(() => expect(fixture.commands.at(-1)).toBe("rm -rf '/stage/content'")) + await vi.advanceTimersByTimeAsync( + SSH_RELAY_RUNTIME_POSIX_TREE_TRANSFER_LIMITS.cleanupTimeoutMs + 2_000 + ) + await rejection + }) +}) diff --git a/src/main/ssh/ssh-relay-runtime-posix-tree-transfer.ts b/src/main/ssh/ssh-relay-runtime-posix-tree-transfer.ts new file mode 100644 index 00000000000..9e451c72e78 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-posix-tree-transfer.ts @@ -0,0 +1,193 @@ +import { assertSafeRemotePathSegment } from './ssh-remote-platform' +import { + runSshRelayRuntimePosixControlCommand, + type RunSshRelayRuntimePosixControlCommandOptions +} from './ssh-relay-runtime-posix-control-command' +import { openSshRelayRuntimePosixFileDestination } from './ssh-relay-runtime-posix-file-destination' +import type { SshRelayRuntimeScannedSourceTree } from './ssh-relay-runtime-source-scan' +import { + streamSshRelayRuntimeSourceTree, + type SshRelayRuntimeSourceStreamOptions, + type SshRelayRuntimeSourceStreamResult +} from './ssh-relay-runtime-source-stream' +import { + openSshRelayRuntimeSystemSshFileChannel, + type SshRelayRuntimeSystemSshConnection +} from './ssh-relay-runtime-system-ssh-file-channel' + +export type SshRelayRuntimePosixTreeTransferOptions = Readonly<{ + tree: SshRelayRuntimeScannedSourceTree + connection: SshRelayRuntimeSystemSshConnection + remoteStagingRoot: string + signal: AbortSignal + maximumConcurrency?: number + onProgress?: SshRelayRuntimeSourceStreamOptions['onProgress'] +}> + +export type SshRelayRuntimePosixTreeTransferResult = Readonly< + SshRelayRuntimeSourceStreamResult & { remoteStagingRoot: string } +> + +const MAXIMUM_CONCURRENT_FILES = 4 +const CLEANUP_TIMEOUT_MS = 5_000 + +type OpenChannel = RunSshRelayRuntimePosixControlCommandOptions['openChannel'] + +function normalizeStagingRoot(value: string): string { + if (typeof value !== 'string') { + throw new Error('SSH relay runtime POSIX staging root is invalid') + } + const segments = value.split('/') + if ( + value === '/' || + !value.startsWith('/') || + value.includes('\0') || + value.includes('\r') || + value.includes('\n') || + segments.slice(1).some((segment) => segment === '' || segment === '.' || segment === '..') + ) { + throw new Error('SSH relay runtime POSIX staging root is invalid') + } + return value +} + +function validateOptions(options: SshRelayRuntimePosixTreeTransferOptions): number { + const concurrency = options?.maximumConcurrency ?? 1 + if ( + !options?.tree || + !options.connection || + typeof options.connection.usesSystemSshTransport !== 'function' || + typeof options.connection.exec !== 'function' || + !options.signal || + !Number.isInteger(concurrency) || + concurrency < 1 || + concurrency > MAXIMUM_CONCURRENT_FILES + ) { + throw new Error('SSH relay runtime POSIX tree transfer input or concurrency is invalid') + } + if (options.tree.os === 'win32') { + throw new Error('SSH relay runtime POSIX tree transfer requires a POSIX tree') + } + if (!options.connection.usesSystemSshTransport()) { + throw new Error('SSH relay runtime POSIX tree transfer requires system SSH transport') + } + return concurrency +} + +function quotePosixShellArgument(value: string): string { + return `'${value.replaceAll("'", "'\\''")}'` +} + +function remoteManifestPath(remoteStagingRoot: string, manifestPath: string): string { + const segments = manifestPath.split('/') + for (const segment of segments) { + assertSafeRemotePathSegment(segment, 'posix') + } + // Why: manifest paths are remote slash paths; client-native path utilities would corrupt them. + return `${remoteStagingRoot}/${segments.join('/')}` +} + +function rootCommand(remoteStagingRoot: string): string { + return `umask 077; mkdir ${quotePosixShellArgument(remoteStagingRoot)}` +} + +function directoryCommand(remotePath: string): string { + const quoted = quotePosixShellArgument(remotePath) + return `umask 077; mkdir ${quoted} && chmod 0755 ${quoted}` +} + +function cleanupCommand(remoteStagingRoot: string): string { + return `rm -rf ${quotePosixShellArgument(remoteStagingRoot)}` +} + +function joinedFailure(primary: unknown, cleanupFailure: unknown | undefined): unknown { + return cleanupFailure === undefined + ? primary + : new AggregateError( + [primary, cleanupFailure], + 'SSH relay runtime POSIX tree transfer cleanup failed' + ) +} + +async function cleanupOwnedRoot( + remoteStagingRoot: string, + openChannel: OpenChannel +): Promise { + const controller = new AbortController() + // Why: caller cancellation initiates cleanup; a separate bounded signal lets cleanup still run. + const timeout = setTimeout( + () => controller.abort(new Error('SSH relay runtime POSIX tree cleanup timed out')), + CLEANUP_TIMEOUT_MS + ) + try { + await runSshRelayRuntimePosixControlCommand({ + command: cleanupCommand(remoteStagingRoot), + signal: controller.signal, + openChannel + }) + } finally { + clearTimeout(timeout) + } +} + +export async function transferSshRelayRuntimeTreeViaPosixSystemSsh( + options: SshRelayRuntimePosixTreeTransferOptions +): Promise { + const maximumConcurrency = validateOptions(options) + const { tree, connection, signal, onProgress } = options + const remoteStagingRoot = normalizeStagingRoot(options.remoteStagingRoot) + const openChannel: OpenChannel = (command, exactSignal) => + openSshRelayRuntimeSystemSshFileChannel(connection, command, exactSignal) + const runControl = (command: string): Promise => + runSshRelayRuntimePosixControlCommand({ command, signal, openChannel }) + signal.throwIfAborted() + let rootOwned = false + + try { + await runControl(rootCommand(remoteStagingRoot)) + rootOwned = true + signal.throwIfAborted() + + const directories = [...tree.directories].sort( + (left, right) => + left.path.split('/').length - right.path.split('/').length || + (left.path < right.path ? -1 : left.path > right.path ? 1 : 0) + ) + for (const directory of directories) { + signal.throwIfAborted() + await runControl(directoryCommand(remoteManifestPath(remoteStagingRoot, directory.path))) + signal.throwIfAborted() + } + + const result = await streamSshRelayRuntimeSourceTree({ + tree, + signal, + maximumConcurrency, + onProgress, + openDestination: (file, exactSignal) => + openSshRelayRuntimePosixFileDestination({ + remotePath: remoteManifestPath(remoteStagingRoot, file.path), + mode: file.mode, + signal: exactSignal, + openChannel + }) + }) + signal.throwIfAborted() + return Object.freeze({ remoteStagingRoot, ...result }) + } catch (error) { + let cleanupFailure: unknown + if (rootOwned) { + try { + await cleanupOwnedRoot(remoteStagingRoot, openChannel) + } catch (cleanupError) { + cleanupFailure = cleanupError + } + } + throw joinedFailure(error, cleanupFailure) + } +} + +export const SSH_RELAY_RUNTIME_POSIX_TREE_TRANSFER_LIMITS = Object.freeze({ + maximumConcurrentFiles: MAXIMUM_CONCURRENT_FILES, + cleanupTimeoutMs: CLEANUP_TIMEOUT_MS +}) From 3fbf2339badb7bab0308a6daadb80e7d5444e280 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 14:16:19 -0400 Subject: [PATCH 229/267] docs(ssh): record POSIX tree transfer evidence Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 15 +++-- ...github-release-implementation-checklist.md | 61 ++++++++++++++++--- 2 files changed, 62 insertions(+), 14 deletions(-) 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 94c6a58cc6c..4b93ae7ff33 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 @@ -10,8 +10,8 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected POSIX -system-SSH tree/root composition is locally green and exact-head native evidence is next, -2026-07-15, Codex implementation owner.** +system-SSH tree/root composition is exact-head green; audit a live/full-size loopback OpenSSH proof +next, 2026-07-15, Codex implementation owner.** `E-M6-POSIX-SYSTEM-SSH-TREE-AUDIT-001` requires a bounded no-input control-command owner rather than the legacy unbounded-output command helper, exclusive `0700` root creation, parent-first declared directories, exact per-file source-stream composition, owned-root cleanup, and a worst-case @@ -23,6 +23,12 @@ skip, 656 broad relay cases with six declared skips, 281/281 release contracts, and static/isolation proof. The new tree owner has no production consumer and the protected legacy Node/npm resolver diff remains untouched. Live/full-size/high-RTT OpenSSH and every product/default path remain explicit gaps until later packages. +`E-M6-POSIX-SYSTEM-SSH-TREE-CI-001` proves all 15 purpose cases on all six primary Node 24 native +clients at exact head `ec51e36b7`; both Linux supplements, Windows x64 floor, PR Checks, +computer-use Windows/Ubuntu, and Golden E2E macOS/Linux pass. The artifact run is red only because +the Windows arm64 hosted floor runner is build 26200 instead of required 26100; it first verified the +complete 85,213,511-byte/42-file runtime, ABI 137, PTY, watcher, and two-second settlement. No live +POSIX system-SSH claim follows from these adapter-backed native cases. `E-M6-POSIX-SYSTEM-SSH-CHANNEL-LOCAL-001` records 13/13 purpose, 150/150 focused, 641 broad relay cases with six declared skips, 281/281 release contracts, and all static/isolation gates. `E-M6-POSIX-SYSTEM-SSH-CHANNEL-CI-001` proves the 13 cases on all six @@ -34,8 +40,9 @@ versus required 26100 rejection after the complete 85,213,511-byte/42-file runti settlement, and idempotent SIGTERM/SIGKILL hooks consumed by the proven single per-file cancellation owner. The adapter prechecks but does not forward that signal into `exec`, avoiding a duplicate immediate SIGTERM listener. -No direct spawn, legacy uploader change, live claim, or product path is included. Collect exact-head -native evidence next; legacy remains the only production/default path and SignPath remains deferred. +No direct spawn, legacy uploader change, live claim, or product path is included. Audit only a +disconnected live/full-size POSIX system-SSH proof next; legacy remains the only production/default +path and SignPath remains deferred. `E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001` records 25/25 purpose, 628 broad relay, 281/281 release, and all static/isolation gates. `E-M6-POSIX-SYSTEM-SSH-FILE-CI-001` proves the 25 cases on all six primary native clients; both Linux 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 3703eda661e..27f16c28f3e 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected POSIX system-SSH/no-tar transfer composition**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **POSIX SYSTEM-SSH TREE/ROOT LOCAL GREEN / EXACT-HEAD NATIVE EVIDENCE NEXT — 2026-07-15, Codex implementation owner** — `E-M6-POSIX-SYSTEM-SSH-TREE-LOCAL-001` proves the disconnected bounded control-command and tree-transfer owners with 15/15 purpose, 219 focused cases with one declared skip, 656 broad relay cases with six declared skips, 281/281 release contracts, full lint/typecheck, and static/isolation gates. Commit and push only this package, then require exact-head native evidence before selecting another package. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Session checkpoint: **POSIX SYSTEM-SSH TREE/ROOT EXACT-HEAD GREEN / LIVE FULL-SIZE AUDIT NEXT — 2026-07-15, Codex implementation owner** — `E-M6-POSIX-SYSTEM-SSH-TREE-CI-001` proves the 15-case disconnected composition on all six primary Node 24 native clients at exact head `ec51e36b7`; Linux supplements, Windows x64 floor, PR Checks, computer-use, and Golden E2E pass. The artifact run remains red only for the declared Windows arm64 hosted build 26200 versus required 26100 floor mismatch after complete runtime verification. Audit only a purpose-named live/full-size loopback OpenSSH proof next; do not connect production behavior. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -156,12 +156,13 @@ same change as the work it records. - Completed package: the disconnected authenticated system-SSH command-channel adapter is green locally and on every primary native client under E-M6-POSIX-SYSTEM-SSH-CHANNEL-LOCAL-001 and E-M6-POSIX-SYSTEM-SSH-CHANNEL-CI-001. It has no tree/live/product caller. -- Active package: **Local green / exact-head native evidence pending — 2026-07-15, Codex - implementation owner** — the smallest disconnected POSIX system-SSH tree/root composition combines - only the proven authenticated source stream, exclusive per-file destination, and command-channel - adapter under E-M6-POSIX-SYSTEM-SSH-TREE-LOCAL-001. Do not import or modify the legacy tar/base64 - directory uploader, add a semantic probe, or add product/settings/fallback/tuple/publication/ - default behavior. +- Completed package: the disconnected POSIX system-SSH tree/root composition is green locally and + on all six primary Node 24 native clients under E-M6-POSIX-SYSTEM-SSH-TREE-LOCAL-001 and + E-M6-POSIX-SYSTEM-SSH-TREE-CI-001. It has no live/full-size SSH or product caller. +- Active package: **Audit in progress — 2026-07-15, Codex implementation owner** — establish the + smallest purpose-named live/full-size loopback OpenSSH proof for the exact disconnected POSIX + system-SSH tree owner, initially on target-native Linux x64/arm64 runners. Do not add product, + settings, fallback, tuple, publication, default behavior, a semantic probe, or a tar fast path. - 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 @@ -17929,6 +17930,47 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs`. release publication, and SignPath remain absent. Exact-head native workflow, PR Checks, computer-use, and Golden E2E run IDs are still required before this package is closed. +### E-M6-POSIX-SYSTEM-SSH-TREE-CI-001 — exact-head native composition proof + +- Date/owner/head: 2026-07-15, Codex implementation owner; exact implementation commit + `ec51e36b77d8f3ca1c3505e509e0c7f909dcd3cf` on draft PR #8741. All run/job IDs below report this + exact head and use Node 24.18.0 where the relay artifact workflow installs Node. +- Artifact run: `29521889962`. All six primary target-native build jobs pass the complete runtime + contract command with both new purpose suites present exactly once: + - Linux x64 `87700712386` (`ubuntu-24.04`): tree 8/8 in 1,035 ms and control 7/7 in 272 ms; + aggregate 96 files, 825 passed/1 declared skip, 16.09 seconds. + - Linux arm64 `87700712339` (`ubuntu-24.04-arm`): tree 8/8 in 1,429 ms and control 7/7 in 276 ms; + aggregate 96 files, 825 passed/1 declared skip, 15.00 seconds. + - macOS x64 `87700712350` (`macos-15-intel`): tree 8/8 in 2,416 ms and control 7/7 in 287 ms; + aggregate 96 files, 824 passed/2 declared skips, 35.45 seconds. + - macOS arm64 `87700712336` (`macos-15`): tree 8/8 in 918 ms and control 7/7 in 272 ms; + aggregate 96 files, 824 passed/2 declared skips, 22.18 seconds. + - Windows x64 `87700712271` (`windows-2022`): tree 8/8 in 3,278 ms and control 7/7 in 319 ms; + aggregate 97 files, 812 passed/18 declared skips, 27.88 seconds. + - Windows arm64 `87700712395` (`windows-11-arm`): tree 8/8 in 2,866 ms and control 7/7 in 334 ms; + aggregate 97 files, 812 passed/18 declared skips, 30.77 seconds. +- Adjacent artifact cells: Linux x64 oldest-userland supplement `87701645841`, Linux arm64 + supplement `87701645883`, and Windows x64 build-20348 floor `87703087692` pass. The workflow's + overall conclusion is failure solely because Windows arm64 floor job `87703087689` correctly + rejects hosted Windows 10.0.26200 against the required build 26100. Before that floor comparison, + it verifies the exact downloaded artifact digest, 60 entries/42 files/85,213,511 expanded bytes, + Node 24.18.0, modules ABI 137, resized PTY exit 23, watcher create/update/delete/rename events, + 2,000 ms resource settlement, 48,619,520-byte RSS, and 5,889 ms smoke duration. Platform and + architecture checks pass; only `osBuild` is false. This is the same declared runner-availability + gap as the preceding exact-head packages, not a regression from this change. +- Repository/adjacent runs: PR Checks run `29521889985`, job `87700711608`, passes lint, + reliability/max-lines, typecheck, Git compatibility, full tests, unpacked build, and packaged CLI + smoke. Computer-use run `29521890044` passes Ubuntu job `87700711776` and Windows job + `87700711824`. Golden E2E run `29521890004` passes Linux job `87700711722` and macOS job + `87700711721`. +- Disposition/residual boundary: the disconnected tree/root package is exact-head green on all six + primary native clients, with the known floor-runner mismatch retained explicitly. These native + cases still use the realistic fake command channel over the production adapter; they do not prove + a live or full-size POSIX system-SSH transfer. Semantic primitive probe/cache, optional tar fast + path, dynamic `MaxSessions=1`, high-RTT remotes, Windows transfer, remote bundled-Node install/ + publish/launch, product/Beta/fallback/default wiring, tuple enablement, release publication, and + SignPath remain absent. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -17987,9 +18029,8 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the locally green bounded control-command/tree composition package proven by -E-M6-POSIX-SYSTEM-SSH-TREE-LOCAL-001, then require exact-head native workflow, PR Checks, -computer-use, and Golden E2E evidence. Do not +Audit and RED-test only the smallest purpose-named live/full-size loopback OpenSSH proof that calls +the exact disconnected POSIX system-SSH tree owner on target-native Linux x64/arm64 CI. Do not check the broad Milestone 6 SFTP or POSIX system-SSH items yet. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets From a56dded6ff0b10046e58e8ff2a3a99b8d6578291 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 14:33:23 -0400 Subject: [PATCH 230/267] test(ssh): prove full-size POSIX system transfer Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 90 +++++ .../ssh-relay-runtime-workflow.test.mjs | 36 ++ ...elease-implementation-checklist-summary.md | 13 + ...github-release-implementation-checklist.md | 123 +++++- ...posix-system-ssh-openssh-full-size.test.ts | 377 ++++++++++++++++++ 5 files changed, 632 insertions(+), 7 deletions(-) create mode 100644 src/main/ssh/ssh-relay-runtime-posix-system-ssh-openssh-full-size.test.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index eda2274aacd..fe859cd0b6d 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -632,6 +632,96 @@ jobs: pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 --reporter=verbose \ src/main/ssh/ssh-relay-runtime-sftp-openssh-full-size.test.ts + - name: Start restricted loopback OpenSSH POSIX system-SSH fixture + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + fixture="$RUNNER_TEMP/orca-live-sftp" + test -x /usr/sbin/sshd + test -s "$fixture/client-key" + test -s "$fixture/host-key" + system_fixture="$fixture/system-ssh" + primitives="$system_fixture/primitives" + mkdir -p "$primitives" + ln -s "$(command -v mkdir)" "$primitives/mkdir" + ln -s "$(command -v chmod)" "$primitives/chmod" + ln -s "$(command -v cat)" "$primitives/cat" + ln -s "$(command -v rm)" "$primitives/rm" + wrapper="$system_fixture/restricted-command" + printf '%s\n' \ + '#!/bin/sh' \ + 'PATH="${0%/*}/primitives"' \ + 'export PATH' \ + 'exec /bin/sh -c "$SSH_ORIGINAL_COMMAND"' > "$wrapper" + chmod 755 "$wrapper" + port=22223 + known_hosts="$HOME/.ssh/known_hosts" + printf '[127.0.0.1]:%s %s\n' "$port" "$(cat "$fixture/host-key.pub")" >> "$known_hosts" + chmod 600 "$known_hosts" + config="$system_fixture/sshd_config" + printf '%s\n' \ + "Port $port" \ + 'ListenAddress 127.0.0.1' \ + "HostKey $fixture/host-key" \ + "PidFile $system_fixture/sshd.pid" \ + "AuthorizedKeysFile $HOME/.ssh/authorized_keys" \ + "AllowUsers $USER" \ + 'PasswordAuthentication no' \ + 'KbdInteractiveAuthentication no' \ + 'ChallengeResponseAuthentication no' \ + 'UsePAM no' \ + 'StrictModes yes' \ + 'LogLevel VERBOSE' \ + "ForceCommand $wrapper" > "$config" + sudo /usr/sbin/sshd -f "$config" -E "$system_fixture/sshd.log" + for attempt in {1..20}; do + if ssh -F /dev/null -i "$fixture/client-key" -p "$port" \ + -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes \ + -o UserKnownHostsFile="$known_hosts" "$USER@127.0.0.1" true; then + break + fi + if test "$attempt" = 20; then + sudo tail -200 "$system_fixture/sshd.log" + exit 1 + fi + sleep 0.25 + done + server_version=$(/usr/sbin/sshd -V 2>&1 | head -1) + { + printf 'ORCA_SSH_RELAY_LIVE_SYSTEM_SSH_HOST=127.0.0.1\n' + printf 'ORCA_SSH_RELAY_LIVE_SYSTEM_SSH_PORT=%s\n' "$port" + printf 'ORCA_SSH_RELAY_LIVE_SYSTEM_SSH_USER=%s\n' "$USER" + printf 'ORCA_SSH_RELAY_LIVE_SYSTEM_SSH_IDENTITY=%s\n' "$fixture/client-key" + printf 'ORCA_SSH_RELAY_LIVE_SYSTEM_SSH_REMOTE_ROOT=%s\n' "$fixture/remote" + printf 'ORCA_SSH_RELAY_LIVE_SYSTEM_SSH_SERVER_VERSION=%s\n' "$server_version" + } >> "$GITHUB_ENV" + + - name: Measure exact full-size runtime over POSIX system SSH + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + first_output="$RUNNER_TEMP/ssh-relay-runtime/${{ matrix.tuple }}/first" + ORCA_SSH_FORCE_SYSTEM_TRANSPORT=1 \ + ORCA_SSH_RELAY_FULL_SIZE_RUNTIME_ROOT="$first_output/runtime" \ + ORCA_SSH_RELAY_FULL_SIZE_IDENTITY="$first_output/orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json" \ + pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 --reporter=verbose \ + src/main/ssh/ssh-relay-runtime-posix-system-ssh-openssh-full-size.test.ts + + - name: Stop restricted loopback OpenSSH POSIX system-SSH fixture + if: always() && runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + system_fixture="$RUNNER_TEMP/orca-live-sftp/system-ssh" + if test -s "$system_fixture/sshd.pid"; then + sudo kill "$(cat "$system_fixture/sshd.pid")" || true + fi + if test -f "$system_fixture/sshd.log"; then + sudo tail -200 "$system_fixture/sshd.log" + fi + - name: Stop loopback OpenSSH SFTP fixture if: always() && runner.os == 'Linux' shell: bash diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 32d559a9ff0..4f7d8a2ec2c 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -104,6 +104,42 @@ describe('SSH relay runtime artifact workflow', () => { expect(stop.if).toBe("always() && runner.os == 'Linux'") }) + it('runs full-size POSIX system SSH against loopback OpenSSH with restricted primitives', async () => { + const workflow = parse(await readFile(workflowUrl, 'utf8')) + const steps = workflow.jobs['build-posix-runtime'].steps + const start = steps.find( + (step) => step.name === 'Start restricted loopback OpenSSH POSIX system-SSH fixture' + ) + const measure = steps.find( + (step) => step.name === 'Measure exact full-size runtime over POSIX system SSH' + ) + const stop = steps.find( + (step) => step.name === 'Stop restricted loopback OpenSSH POSIX system-SSH fixture' + ) + + expect(start).toBeDefined() + expect(measure).toBeDefined() + expect(stop).toBeDefined() + expect(start.if).toBe("runner.os == 'Linux'") + expect(start.run).toContain('ListenAddress 127.0.0.1') + expect(start.run).toContain('ForceCommand $wrapper') + expect(start.run).toContain('PasswordAuthentication no') + expect(start.run).toContain('StrictModes yes') + expect(start.run).toContain('known_hosts') + expect(start.run).toContain('StrictHostKeyChecking=yes') + for (const primitive of ['mkdir', 'chmod', 'cat', 'rm']) { + expect(start.run).toContain(`command -v ${primitive}`) + } + for (const forbidden of ['node', 'python', 'perl', 'tar', 'base64', 'sha256sum', 'shasum']) { + expect(start.run).not.toContain(`command -v ${forbidden}`) + } + expect(measure.if).toBe("runner.os == 'Linux'") + expect(measure.run).toContain('ORCA_SSH_FORCE_SYSTEM_TRANSPORT=1') + expect(measure.run).toContain('ssh-relay-runtime-posix-system-ssh-openssh-full-size.test.ts') + expect(measure.run).toContain('first_output/runtime') + expect(stop.if).toBe("always() && runner.os == 'Linux'") + }) + it('uploads only the first output after two clean builds verify and compare', async () => { const source = await readFile(workflowUrl, 'utf8') const workflow = parse(source) 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 4b93ae7ff33..497df6015d3 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 @@ -43,6 +43,19 @@ immediate SIGTERM listener. No direct spawn, legacy uploader change, live claim, or product path is included. Audit only a disconnected live/full-size POSIX system-SSH proof next; legacy remains the only production/default path and SignPath remains deferred. +`E-M6-POSIX-SYSTEM-SSH-TREE-LIVE-AUDIT-001` limits that proof to a second loopback-only stock +`sshd` on Linux x64/arm64 artifact runners, exact generated host-key trust, public-key auth, and a +forced remote `PATH` containing only `mkdir`, `chmod`, `cat`, and `rm` plus absolute `/bin/sh`. +It adds only a purpose-named full-size test and workflow/oracle wiring; no production module changes. +`E-M6-POSIX-SYSTEM-SSH-TREE-LIVE-LOCAL-RED-001` proves the live suite skips without its seven CI +inputs while the independent workflow oracle fails only because the audited start/measure/stop +steps are absent. That bounded fixture wiring now passes the local 9/9 workflow oracle while the +purpose suite remains honestly skipped without CI inputs; broad regression gates and exact-head +Linux x64/arm64 live evidence remain required. +`E-M6-POSIX-SYSTEM-SSH-TREE-LIVE-LOCAL-001` records that bounded package locally green: 9/9 workflow +oracle, 220 focused, 656 broad relay, 282 release-contract, typecheck, lint, 355-entry max-lines, +formatting, diff, protected-resolver, and no-product-consumer gates pass. The live case is explicitly +skipped locally and earns no proof; exact-head Linux x64/arm64 live evidence is still required. `E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001` records 25/25 purpose, 628 broad relay, 281/281 release, and all static/isolation gates. `E-M6-POSIX-SYSTEM-SSH-FILE-CI-001` proves the 25 cases on all six primary native clients; both Linux 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 27f16c28f3e..b666c057d44 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected POSIX system-SSH/no-tar transfer composition**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **POSIX SYSTEM-SSH TREE/ROOT EXACT-HEAD GREEN / LIVE FULL-SIZE AUDIT NEXT — 2026-07-15, Codex implementation owner** — `E-M6-POSIX-SYSTEM-SSH-TREE-CI-001` proves the 15-case disconnected composition on all six primary Node 24 native clients at exact head `ec51e36b7`; Linux supplements, Windows x64 floor, PR Checks, computer-use, and Golden E2E pass. The artifact run remains red only for the declared Windows arm64 hosted build 26200 versus required 26100 floor mismatch after complete runtime verification. Audit only a purpose-named live/full-size loopback OpenSSH proof next; do not connect production behavior. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Session checkpoint: **POSIX SYSTEM-SSH LIVE FULL-SIZE LOCAL PACKAGE GREEN / COMMIT AND EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-POSIX-SYSTEM-SSH-TREE-LIVE-LOCAL-001` records the bounded workflow oracle, focused/broad/release suites, typecheck, lint, max-lines, formatting, diff, protected-resolver, and no-product-consumer gates green. The purpose suite remains honestly skipped without its seven CI inputs and earns no live proof locally. Commit and push only this test/workflow/docs package, then require exact-head Linux x64/arm64 live metrics and all-six adjacent regression cells. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -17879,8 +17879,8 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs`. separate five-second abort controller. Runtime-invalid staging-root input was corrected before final GREEN so validation cannot throw an incidental `.split` type error. - Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts - --maxWorkers=1 src/main/ssh/ssh-relay-runtime-posix-control-command.test.ts - src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts`. Result: 2 files and 15/15 cases pass; +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-posix-control-command.test.ts +src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts`. Result: 2 files and 15/15 cases pass; Vitest 1.95 seconds, 2.96 seconds real, 139,640,832-byte maximum RSS, 95,851,848-byte peak memory footprint, zero swaps and block I/O. Cases cover exact/no-input success, nonzero/error, pre/mid abort, timeout, graceful/forced settlement and joined failures; invalid OS/path/concurrency before @@ -17888,7 +17888,7 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs`. progress, default-one/four-channel bounds, collision ownership, cancellation settlement, and cleanup failure/timeout joining. - Focused composition command: `/usr/bin/time -l pnpm exec vitest run --config - config/vitest.config.ts --maxWorkers=1` with the two new purpose suites plus +config/vitest.config.ts --maxWorkers=1` with the two new purpose suites plus `ssh-relay-runtime-posix-file-destination.test.ts`, `ssh-relay-runtime-system-ssh-file-channel.test.ts`, the source-tree/scan/stream suites, `ssh-system-fallback.test.ts`, `ssh-connection.test.ts`, and @@ -17898,13 +17898,13 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs`. lease, retained-buffer/no-later-write, missing process/false kill, diagnostic, child settlement, and exact native-workflow-family contracts inherited from the composed boundaries. - Broad relay command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts - --maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts`. Result: 50 files passed, four files skipped; +--maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts`. Result: 50 files passed, four files skipped; 656 passed and six declared skips out of 662. Vitest 55.58 seconds, 58.05 seconds real, 254,115,840-byte maximum RSS, 96,458,248-byte peak memory footprint, zero swaps and block I/O. Skips remain the declared case-fold/full-size/live SSH cells and are not treated as this package's evidence. - Release-contract command: `/usr/bin/time -l pnpm exec vitest run --config - config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs`. Result: 50 +config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs`. Result: 50 files and 281/281 cases pass; Vitest 37.74 seconds, 41.85 seconds real, 190,676,992-byte maximum RSS, 97,195,384-byte peak memory footprint, zero swaps and block I/O. - Static commands/results: `/usr/bin/time -l pnpm typecheck` passes in 8.38 seconds real with @@ -17917,7 +17917,7 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs`. `pnpm check:max-lines-ratchet` command; no checklist box was credited to the failed spelling. - Isolation evidence: `git diff --check` and no-index whitespace checks for all four new files are clean. `git diff --exit-code HEAD -- src/main/ssh/ssh-remote-node-resolution.ts - src/main/ssh/ssh-remote-node-resolution.test.ts` is empty, preserving the protected Milestone 0 +src/main/ssh/ssh-remote-node-resolution.test.ts` is empty, preserving the protected Milestone 0 resolver. The only implementation references to the exported tree owner are its definition and purpose test; the control owner is consumed only by that disconnected tree owner and its test. No product, settings, legacy uploader, fallback, tuple, publication, or default-path importer @@ -17971,6 +17971,115 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs`. publish/launch, product/Beta/fallback/default wiring, tuple enablement, release publication, and SignPath remain absent. +### E-M6-POSIX-SYSTEM-SSH-TREE-LIVE-AUDIT-001 — restricted-primitive full-size OpenSSH proof + +- Date/owner/base: 2026-07-15, Codex implementation owner; exact clean evidence base + `3fbf2339badb7bab0308a6daadb80e7d5444e280` after recording the disconnected tree package's + exact-head evidence. +- Existing-boundary audit: the exact tree owner already composes production `SshConnection.exec`, + the system-SSH child/channel adapter, exclusive POSIX destinations, the scanned lease-backed + source stream, and bounded owned-root cleanup. No production change is necessary for live proof. + The existing live SFTP fixture cannot force a restricted command environment because its + `internal-sftp` subsystem shares the daemon, so do not alter or weaken it. +- Live fixture: on only the Linux x64/arm64 target-native artifact jobs, start a second stock + loopback-only `sshd` on its own port/PID/log with the already-generated ephemeral Ed25519 client + and host keys. Pin the generated public host key into the runner user's `known_hosts`; retain + `StrictModes yes`, disable password/keyboard-interactive authentication, and never expose a + non-loopback listener. `ForceCommand` invokes a fixed fixture-owned POSIX wrapper whose remote + `PATH` contains symlinks only for `mkdir`, `chmod`, `cat`, and `rm`; `/bin/sh` is absolute. This + proves transfer without remote Node, Python, Perl, tar, base64, `sha256sum`, or `shasum`, while + preserving normal production system-SSH argument/channel behavior and authenticated transport. +- Purpose suite: build a scanned source tree from the exact full-size runtime identity produced in + the same native job, force the existing `SshConnection` into system transport only for the test, + and call `transferSshRelayRuntimeTreeViaPosixSystemSsh` directly. Measure serial/default-one and + explicit-four transfers; validate every remote path, byte, SHA-256, mode, file count, and total; + prove a root collision cannot alter the complete tree; cancel after the first transferred bytes, + require no later progress and owned-root removal after settlement; and prove the connection remains + usable. Record tuple/content/server/runner, bytes/files, elapsed time, peak progress concurrency, + incremental RSS, cancellation delay, and residual gaps. Enforce the existing 80 MiB incremental + RSS and 10-second cancellation ceilings under the workflow's 20-minute job budget. +- Evidence sequence: add only the skipped-without-input purpose suite plus workflow oracle first and + accept RED only when the oracle reports the three missing start/measure/stop steps. Then add the + bounded workflow fixture/wiring, require local skip/oracle GREEN plus broad/static/isolation gates, + commit, and require exact-head Linux x64/arm64 live metrics and all-six adjacent regression cells. +- Explicit residual boundary: no high-RTT host, `MaxSessions=1`, cross-client/remote family matrix, + network interruption/disk/quota/inode/noexec/AV fault injection, semantic probe/cache, optional tar + fast path, Windows transfer, remote bundled-Node verification/install/publish/launch, product/Beta/ + settings/fallback/default behavior, tuple enablement, publication, or SignPath is included. Legacy + remains the only production/default path. + +### E-M6-POSIX-SYSTEM-SSH-TREE-LIVE-LOCAL-RED-001 — audited workflow fixture is absent + +- Date/owner/base: 2026-07-15, Codex implementation owner; audit, purpose suite, and workflow-oracle- + only diff atop exact clean evidence base `3fbf2339badb7bab0308a6daadb80e7d5444e280`. +- Command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-runtime-posix-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs`. +- Result: expected RED, exit 1. The purpose-named live suite declares its single case skipped because + the seven full-size/OpenSSH inputs are absent locally and earns no transfer evidence. The workflow + oracle passes 8/9 existing cases and its only failure is `expected undefined to be defined` for + the audited restricted POSIX system-SSH start step; measure and stop are consequently absent too. +- Resource evidence: Vitest 1.50 seconds, 3.73 seconds real, 143,818,752-byte maximum RSS, + 96,146,808-byte peak memory footprint, zero swaps and block I/O. +- Disposition: RED is causal and accepted. Add only the second loopback daemon, restricted primitive + wrapper, exact known-host pin, purpose measurement, and always-run cleanup steps. No production, + product/default, fallback, tuple, publication, tar optimization, Windows transfer, or SignPath + change is authorized by this evidence. + +### E-M6-POSIX-SYSTEM-SSH-TREE-LIVE-LOCAL-001 — bounded live fixture package is locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted test/workflow/docs + package atop exact base `3fbf2339badb7bab0308a6daadb80e7d5444e280` on Apple arm64, macOS 26.2 + build 25C56, Node 26.0.0, pnpm 10.24.0. The local Node version is newer than the repository's Node + 24 contract, so exact-head native workflow evidence remains mandatory. +- Package: add one purpose-named live/full-size test and three audited Linux-only artifact-workflow + steps. The fixture starts a second stock loopback `sshd`, pins the generated host key, permits only + public-key authentication, and forces a wrapper whose `PATH` contains only `mkdir`, `chmod`, `cat`, + and `rm` while invoking absolute `/bin/sh`. The test uses the exact native runtime tree and existing + production system-SSH composition to measure default-one and explicit-four transfer, exact tree + bytes/hashes/modes/paths/counts, exclusive-root collision preservation, cancellation settlement, + no later progress, owned-root cleanup, post-cancellation connection usability, latency, RSS, and + active-file count. No production module changes. +- Targeted command: `pnpm exec oxfmt --check` for the new test, workflow oracle, and artifact workflow; + `pnpm exec oxlint` for the new test and oracle; then `/usr/bin/time -l pnpm exec vitest run --config + config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-runtime-posix-system-ssh-openssh-full-size.test.ts + config/scripts/ssh-relay-runtime-workflow.test.mjs`. Result: formatting/lint pass; the workflow + oracle passes 9/9 while the one live case is explicitly skipped because its seven CI inputs are + absent. Vitest 12.53 seconds; 25.90 seconds real; 131,907,584-byte maximum RSS; 96,146,904-byte + peak footprint; zero swaps and block I/O. The skip earns no live/full-size transfer evidence. +- Focused composition command: `/usr/bin/time -l pnpm exec vitest run --config + config/vitest.config.ts --maxWorkers=1` with the control/tree/file-destination/system-channel, + source-tree/scan/stream, system-fallback, connection, and workflow-oracle suites. Result: 10 files, + 220 passed and one declared platform skip; Vitest 13.07 seconds; 14.67 seconds real; + 178,061,312-byte maximum RSS; 96,277,904-byte peak footprint; zero swaps and block I/O. +- Broad relay command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts + --maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts`. Result: 50 files passed, five files skipped; 656 + passed and seven declared skips out of 663. Vitest 59.16 seconds; 64.28 seconds real; + 269,565,952-byte maximum RSS; 96,114,160-byte peak footprint; zero swaps and block I/O. The new + live case is one of the declared skips and is not counted as proof. +- Release-contract command: `/usr/bin/time -l pnpm exec vitest run --config + config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs`. Result: 50 files + and 282/282 cases pass; Vitest 35.69 seconds; 39.36 seconds real; 190,201,856-byte maximum RSS; + 96,638,376-byte peak footprint; zero swaps and block I/O. +- Static evidence: `/usr/bin/time -l pnpm typecheck` passes in 6.13 seconds real with + 1,241,317,376-byte maximum RSS and 96,163,192-byte peak footprint. `/usr/bin/time -l pnpm lint` + passes in 36.12 seconds real with 2,111,471,616-byte maximum RSS and 95,868,232-byte peak footprint; + its 26 warnings are pre-existing outside this diff. Full lint proves switch exhaustiveness, 41 + reliability gates, the 355-entry max-lines ratchet with no new bypass, bundled guides, and all + localization checks. Standalone max-lines and targeted formatting pass after formatting this + ledger entry; the new test is 377 lines with no suppression. +- Isolation evidence: `git diff --check` passes; `git diff --exit-code HEAD -- + src/main/ssh/ssh-remote-node-resolution.ts src/main/ssh/ssh-remote-node-resolution.test.ts` is empty. + An asserted non-test reference search finds the tree-transfer export only in its owning production + module; this package adds no product importer. The intended diff is limited to the live test, + artifact workflow, workflow oracle, and both checklist documents. No settings, Beta, legacy + uploader, fallback, tuple, publication, default-path, or signing behavior changes. +- Residual boundary: local execution does not supply a Linux OpenSSH daemon or the exact full-size + native runtime, so exact-head Linux x64/arm64 live metrics and all-six adjacent native regression + cells remain required. High RTT, `MaxSessions=1`, cross-family remotes, Windows transfer, primitive + probe/cache, remote bundled-Node verification/install/publish/launch, product/Beta/fallback/default + wiring, tuple enablement, publication, Apple signing, and SignPath remain absent. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/src/main/ssh/ssh-relay-runtime-posix-system-ssh-openssh-full-size.test.ts b/src/main/ssh/ssh-relay-runtime-posix-system-ssh-openssh-full-size.test.ts new file mode 100644 index 00000000000..586caede435 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-posix-system-ssh-openssh-full-size.test.ts @@ -0,0 +1,377 @@ +import { createHash } from 'node:crypto' +import { lstat, open, opendir, readFile, rm } from 'node:fs/promises' +import { join } from 'node:path' +import { performance } from 'node:perf_hooks' + +import { describe, expect, it, vi } from 'vitest' + +vi.mock('electron', () => ({ app: { getAppPath: () => process.cwd() } })) + +import type { SshTarget } from '../../shared/ssh-types' +import { SshConnection } from './ssh-connection' +import { runSshRelayRuntimePosixControlCommand } from './ssh-relay-runtime-posix-control-command' +import { transferSshRelayRuntimeTreeViaPosixSystemSsh } from './ssh-relay-runtime-posix-tree-transfer' +import { + scanSshRelayRuntimeSourceTree, + type SshRelayRuntimeScannedSourceTree +} from './ssh-relay-runtime-source-scan' +import type { SshRelayRuntimeSourceTree } from './ssh-relay-runtime-source-tree' +import { + SSH_RELAY_RUNTIME_SOURCE_STREAM_LIMITS, + type SshRelayRuntimeSourceStreamProgress +} from './ssh-relay-runtime-source-stream' +import { openSshRelayRuntimeSystemSshFileChannel } from './ssh-relay-runtime-system-ssh-file-channel' + +type MeasurementIdentity = Pick< + SshRelayRuntimeSourceTree, + 'tupleId' | 'contentId' | 'os' | 'architecture' +> & { + entries: ( + | { path: string; type: 'directory'; mode: 0o755 } + | { + path: string + type: 'file' + role: SshRelayRuntimeSourceTree['files'][number]['role'] + size: number + mode: 0o644 | 0o755 + sha256: SshRelayRuntimeSourceTree['files'][number]['sha256'] + } + )[] + archive: { expandedSize: number; fileCount: number } +} + +type Measurement = { + result: T + elapsedMs: number + baselineRss: number + peakRss: number + incrementalRssBytes: number +} + +const host = process.env.ORCA_SSH_RELAY_LIVE_SYSTEM_SSH_HOST +const user = process.env.ORCA_SSH_RELAY_LIVE_SYSTEM_SSH_USER +const identityFile = process.env.ORCA_SSH_RELAY_LIVE_SYSTEM_SSH_IDENTITY +const remoteRoot = process.env.ORCA_SSH_RELAY_LIVE_SYSTEM_SSH_REMOTE_ROOT +const runtimeRoot = process.env.ORCA_SSH_RELAY_FULL_SIZE_RUNTIME_ROOT +const identityPath = process.env.ORCA_SSH_RELAY_FULL_SIZE_IDENTITY +const port = Number.parseInt(process.env.ORCA_SSH_RELAY_LIVE_SYSTEM_SSH_PORT ?? '', 10) +const hasLiveInput = Boolean( + host && + user && + identityFile && + remoteRoot && + runtimeRoot && + identityPath && + Number.isInteger(port) +) + +function parseIdentity(value: unknown): MeasurementIdentity { + if ( + typeof value !== 'object' || + value === null || + !('entries' in value) || + !Array.isArray(value.entries) || + !('archive' in value) || + typeof value.archive !== 'object' || + value.archive === null + ) { + throw new Error('Live POSIX system-SSH measurement identity is incomplete') + } + return value as MeasurementIdentity +} + +function sourceTree(identity: MeasurementIdentity): SshRelayRuntimeSourceTree { + const directories = identity.entries + .filter((entry) => entry.type === 'directory') + .map((entry) => ({ + ...entry, + localPath: join(runtimeRoot as string, ...entry.path.split('/')) + })) + const files = identity.entries + .filter((entry) => entry.type === 'file') + .map((entry) => ({ + ...entry, + localPath: join(runtimeRoot as string, ...entry.path.split('/')) + })) + return Object.freeze({ + tupleId: identity.tupleId, + contentId: identity.contentId, + releaseTag: 'measurement-only', + os: identity.os, + architecture: identity.architecture, + runtimeRoot: runtimeRoot as string, + directories, + files, + fileCount: identity.archive.fileCount, + expandedBytes: identity.archive.expandedSize, + assertLeaseOwned: async () => {} + }) +} + +async function measure(operation: () => Promise): Promise> { + const baselineRss = process.memoryUsage().rss + let peakRss = baselineRss + const sample = (): void => { + peakRss = Math.max(peakRss, process.memoryUsage().rss) + } + const sampler = setInterval(sample, 1) + const startedAt = performance.now() + try { + const result = await operation() + sample() + return { + result, + elapsedMs: performance.now() - startedAt, + baselineRss, + peakRss, + incrementalRssBytes: Math.max(0, peakRss - baselineRss) + } + } finally { + clearInterval(sampler) + } +} + +async function hashFile(path: string): Promise { + const handle = await open(path, 'r') + const digest = createHash('sha256') + const buffer = Buffer.allocUnsafe(64 * 1024) + let position = 0 + try { + while (true) { + const { bytesRead } = await handle.read(buffer, 0, buffer.length, position) + if (bytesRead === 0) { + break + } + digest.update(buffer.subarray(0, bytesRead)) + position += bytesRead + } + } finally { + await handle.close() + } + return `sha256:${digest.digest('hex')}` +} + +async function collectRemotePaths(root: string): Promise { + const paths: string[] = [] + const pending = [{ localPath: root, relativePath: '' }] + while (pending.length > 0) { + const current = pending.pop()! + const directory = await opendir(current.localPath) + for await (const entry of directory) { + const relativePath = current.relativePath + ? `${current.relativePath}/${entry.name}` + : entry.name + paths.push(relativePath) + if (entry.isDirectory()) { + pending.push({ localPath: join(current.localPath, entry.name), relativePath }) + } + } + } + return paths.sort() +} + +async function validateTransferredTree( + tree: SshRelayRuntimeScannedSourceTree, + stage: string +): Promise { + const expectedPaths = [...tree.directories, ...tree.files].map((entry) => entry.path).sort() + // Why: the loopback runner can inspect sshd's filesystem without adding another proof channel. + expect(await collectRemotePaths(stage)).toEqual(expectedPaths) + for (const directory of tree.directories) { + const metadata = await lstat(join(stage, ...directory.path.split('/'))) + expect(metadata.isDirectory()).toBe(true) + expect(metadata.mode & 0o777).toBe(directory.mode) + } + for (const file of tree.files) { + const path = join(stage, ...file.path.split('/')) + const metadata = await lstat(path) + expect(metadata.isFile()).toBe(true) + expect(metadata.size).toBe(file.size) + expect(metadata.mode & 0o777).toBe(file.mode) + expect(await hashFile(path)).toBe(file.sha256) + } +} + +function stagePath(name: string, identity: MeasurementIdentity): string { + return join(remoteRoot as string, `${name}-${identity.contentId.slice('sha256:'.length, 23)}`) +} + +function createProgressRecorder(): { + record: (progress: SshRelayRuntimeSourceStreamProgress) => void + snapshot: () => { bytes: number; updates: number; peakActiveFiles: number } +} { + let bytes = 0 + let updates = 0 + let peakActiveFiles = 0 + return { + record: (progress) => { + bytes = progress.bytesTransferred + updates += 1 + peakActiveFiles = Math.max(peakActiveFiles, progress.activeFiles) + }, + snapshot: () => ({ bytes, updates, peakActiveFiles }) + } +} + +async function transfer( + connection: SshConnection, + tree: SshRelayRuntimeScannedSourceTree, + stage: string, + signal: AbortSignal, + onProgress: (progress: SshRelayRuntimeSourceStreamProgress) => void, + maximumConcurrency?: number +) { + return transferSshRelayRuntimeTreeViaPosixSystemSsh({ + connection, + tree, + remoteStagingRoot: stage, + signal, + onProgress, + ...(maximumConcurrency === undefined ? {} : { maximumConcurrency }) + }) +} + +describe.skipIf(!hasLiveInput)( + 'SSH relay full-size POSIX system-SSH transfer through restricted OpenSSH', + () => { + it( + 'preserves exact bytes and modes with bounded serial, concurrent, and cancellation behavior', + { timeout: 20 * 60_000 }, + async () => { + expect(process.platform).toBe('linux') + expect(process.env.ORCA_SSH_FORCE_SYSTEM_TRANSPORT).toBe('1') + const identity = parseIdentity(JSON.parse(await readFile(identityPath as string, 'utf8'))) + expect(identity.os).toBe('linux') + const tree = await scanSshRelayRuntimeSourceTree( + sourceTree(identity), + new AbortController().signal + ) + const target: SshTarget = { + id: 'live-posix-system-ssh-measurement', + label: 'live-posix-system-ssh-measurement', + host: host as string, + port, + username: user as string, + identityFile: identityFile as string, + identitiesOnly: true, + systemSshConnectionReuse: true, + source: 'manual' + } + const connection = new SshConnection(target, { onStateChange: () => {} }) + const serialStage = stagePath('system-serial', identity) + const concurrentStage = stagePath('system-concurrent', identity) + const cancelledStage = stagePath('system-cancelled', identity) + const stages = [serialStage, concurrentStage, cancelledStage] + await Promise.all(stages.map((stage) => rm(stage, { recursive: true, force: true }))) + + try { + await connection.connect() + expect(connection.usesSystemSshTransport()).toBe(true) + + const serialProgress = createProgressRecorder() + const serial = await measure(() => + transfer( + connection, + tree, + serialStage, + new AbortController().signal, + serialProgress.record + ) + ) + await validateTransferredTree(tree, serialStage) + expect(serialProgress.snapshot().peakActiveFiles).toBe(1) + + const concurrentProgress = createProgressRecorder() + const concurrent = await measure(() => + transfer( + connection, + tree, + concurrentStage, + new AbortController().signal, + concurrentProgress.record, + 4 + ) + ) + await validateTransferredTree(tree, concurrentStage) + expect(concurrentProgress.snapshot().peakActiveFiles).toBeGreaterThan(1) + expect(concurrentProgress.snapshot().peakActiveFiles).toBeLessThanOrEqual(4) + await expect( + transfer(connection, tree, concurrentStage, new AbortController().signal, () => {}) + ).rejects.toBeTruthy() + await validateTransferredTree(tree, concurrentStage) + + const controller = new AbortController() + const cancelledProgress = createProgressRecorder() + let abortRequestedAt = 0 + const cancelled = await measure(async () => { + const outcome = transfer( + connection, + tree, + cancelledStage, + controller.signal, + (progress) => { + cancelledProgress.record(progress) + if (progress.bytesTransferred > 0 && !controller.signal.aborted) { + abortRequestedAt = performance.now() + controller.abort(new Error('live POSIX system-SSH cancellation')) + } + }, + 4 + ) + await expect(outcome).rejects.toThrow('live POSIX system-SSH cancellation') + return performance.now() - abortRequestedAt + }) + const updatesAfterSettlement = cancelledProgress.snapshot().updates + await expect(lstat(cancelledStage)).rejects.toMatchObject({ code: 'ENOENT' }) + await new Promise((resolve) => setTimeout(resolve, 500)) + await expect(lstat(cancelledStage)).rejects.toMatchObject({ code: 'ENOENT' }) + expect(cancelledProgress.snapshot().updates).toBe(updatesAfterSettlement) + + await runSshRelayRuntimePosixControlCommand({ + command: 'true', + signal: new AbortController().signal, + openChannel: (command, signal) => + openSshRelayRuntimeSystemSshFileChannel(connection, command, signal) + }) + expect(connection.getState().status).toBe('connected') + + const metrics = { + tupleId: identity.tupleId, + contentId: identity.contentId, + files: tree.fileCount, + bytes: tree.expandedBytes, + serverVersion: process.env.ORCA_SSH_RELAY_LIVE_SYSTEM_SSH_SERVER_VERSION, + runnerImage: process.env.ImageOS, + runnerVersion: process.env.ImageVersion, + runnerArchitecture: process.env.RUNNER_ARCH, + remotePrimitives: ['/bin/sh', 'mkdir', 'chmod', 'cat', 'rm'], + serialElapsedMs: serial.elapsedMs, + serialIncrementalRssBytes: serial.incrementalRssBytes, + serialPeakActiveFiles: serialProgress.snapshot().peakActiveFiles, + concurrentElapsedMs: concurrent.elapsedMs, + concurrentIncrementalRssBytes: concurrent.incrementalRssBytes, + concurrentPeakActiveFiles: concurrentProgress.snapshot().peakActiveFiles, + cancellationSettlementMs: cancelled.result, + cancellationIncrementalRssBytes: cancelled.incrementalRssBytes, + cancellationProgressUpdates: cancelledProgress.snapshot().updates + } + console.log(`ssh_relay_live_posix_system_ssh=${JSON.stringify(metrics)}`) + expect(serial.result.bytesTransferred).toBe(tree.expandedBytes) + expect(concurrent.result.bytesTransferred).toBe(tree.expandedBytes) + expect(serialProgress.snapshot().bytes).toBe(tree.expandedBytes) + expect(concurrentProgress.snapshot().bytes).toBe(tree.expandedBytes) + expect(serial.incrementalRssBytes).toBeLessThanOrEqual( + SSH_RELAY_RUNTIME_SOURCE_STREAM_LIMITS.maximumIncrementalMemoryBytes + ) + expect(concurrent.incrementalRssBytes).toBeLessThanOrEqual( + SSH_RELAY_RUNTIME_SOURCE_STREAM_LIMITS.maximumIncrementalMemoryBytes + ) + expect(cancelled.result).toBeLessThan(10_000) + } finally { + await connection.disconnect() + await Promise.all(stages.map((stage) => rm(stage, { recursive: true, force: true }))) + } + } + ) + } +) From cdc25b2d1339fae9c4508b5b7453bb77fe9b9abc Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 14:34:52 -0400 Subject: [PATCH 231/267] docs(ssh): format live transfer evidence Co-authored-by: Orca --- ...-relay-github-release-implementation-checklist.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 b666c057d44..d37854d4c1d 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 @@ -18042,23 +18042,23 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs`. active-file count. No production module changes. - Targeted command: `pnpm exec oxfmt --check` for the new test, workflow oracle, and artifact workflow; `pnpm exec oxlint` for the new test and oracle; then `/usr/bin/time -l pnpm exec vitest run --config - config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-runtime-posix-system-ssh-openssh-full-size.test.ts - config/scripts/ssh-relay-runtime-workflow.test.mjs`. Result: formatting/lint pass; the workflow +config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-runtime-posix-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs`. Result: formatting/lint pass; the workflow oracle passes 9/9 while the one live case is explicitly skipped because its seven CI inputs are absent. Vitest 12.53 seconds; 25.90 seconds real; 131,907,584-byte maximum RSS; 96,146,904-byte peak footprint; zero swaps and block I/O. The skip earns no live/full-size transfer evidence. - Focused composition command: `/usr/bin/time -l pnpm exec vitest run --config - config/vitest.config.ts --maxWorkers=1` with the control/tree/file-destination/system-channel, +config/vitest.config.ts --maxWorkers=1` with the control/tree/file-destination/system-channel, source-tree/scan/stream, system-fallback, connection, and workflow-oracle suites. Result: 10 files, 220 passed and one declared platform skip; Vitest 13.07 seconds; 14.67 seconds real; 178,061,312-byte maximum RSS; 96,277,904-byte peak footprint; zero swaps and block I/O. - Broad relay command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts - --maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts`. Result: 50 files passed, five files skipped; 656 +--maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts`. Result: 50 files passed, five files skipped; 656 passed and seven declared skips out of 663. Vitest 59.16 seconds; 64.28 seconds real; 269,565,952-byte maximum RSS; 96,114,160-byte peak footprint; zero swaps and block I/O. The new live case is one of the declared skips and is not counted as proof. - Release-contract command: `/usr/bin/time -l pnpm exec vitest run --config - config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs`. Result: 50 files +config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs`. Result: 50 files and 282/282 cases pass; Vitest 35.69 seconds; 39.36 seconds real; 190,201,856-byte maximum RSS; 96,638,376-byte peak footprint; zero swaps and block I/O. - Static evidence: `/usr/bin/time -l pnpm typecheck` passes in 6.13 seconds real with @@ -18069,7 +18069,7 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs`. localization checks. Standalone max-lines and targeted formatting pass after formatting this ledger entry; the new test is 377 lines with no suppression. - Isolation evidence: `git diff --check` passes; `git diff --exit-code HEAD -- - src/main/ssh/ssh-remote-node-resolution.ts src/main/ssh/ssh-remote-node-resolution.test.ts` is empty. +src/main/ssh/ssh-remote-node-resolution.ts src/main/ssh/ssh-remote-node-resolution.test.ts` is empty. An asserted non-test reference search finds the tree-transfer export only in its owning production module; this package adds no product importer. The intended diff is limited to the live test, artifact workflow, workflow oracle, and both checklist documents. No settings, Beta, legacy From aba3f7c649b8412b40871d8c3a82938e0094739e Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 14:55:32 -0400 Subject: [PATCH 232/267] docs(ssh): record live system transfer evidence Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 15 ++++- ...github-release-implementation-checklist.md | 57 ++++++++++++++++++- 2 files changed, 68 insertions(+), 4 deletions(-) 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 497df6015d3..618c748228a 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 @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected POSIX -system-SSH tree/root composition is exact-head green; audit a live/full-size loopback OpenSSH proof -next, 2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — restricted-primitive +live/full-size POSIX system-SSH proof is exact-head green; record evidence and audit the next +disconnected transfer package, 2026-07-15, Codex implementation owner.** `E-M6-POSIX-SYSTEM-SSH-TREE-AUDIT-001` requires a bounded no-input control-command owner rather than the legacy unbounded-output command helper, exclusive `0700` root creation, parent-first declared directories, exact per-file source-stream composition, owned-root cleanup, and a worst-case @@ -56,6 +56,15 @@ Linux x64/arm64 live evidence remain required. oracle, 220 focused, 656 broad relay, 282 release-contract, typecheck, lint, 355-entry max-lines, formatting, diff, protected-resolver, and no-product-consumer gates pass. The live case is explicitly skipped locally and earns no proof; exact-head Linux x64/arm64 live evidence is still required. +`E-M6-POSIX-SYSTEM-SSH-TREE-LIVE-CI-001` closes that live package at exact head `cdc25b2d1`: native +Linux x64 transfers 124,846,430 bytes/34 files in 976 ms serial or 678 ms at four channels; native +Linux arm64 transfers 122,865,324 bytes/34 files in 796 ms serial or 568 ms at four channels. Peak +incremental RSS is 4,956,160 bytes and cancellation settles in 15.6–27.6 ms with no later progress; +both trees, modes, hashes, collision preservation, cleanup, and connection reuse pass against pinned +stock OpenSSH exposing only `/bin/sh`, `mkdir`, `chmod`, `cat`, and `rm`. All six primary native +jobs, both Linux supplements, Windows x64 floor, PR Checks, Golden E2E, and computer-use pass after +one unchanged Ubuntu UI-focus rerun. Windows arm64 retains only hosted build 26200 versus required +26100 after full runtime proof. No product/default path is connected. `E-M6-POSIX-SYSTEM-SSH-FILE-LOCAL-001` records 25/25 purpose, 628 broad relay, 281/281 release, and all static/isolation gates. `E-M6-POSIX-SYSTEM-SSH-FILE-CI-001` proves the 25 cases on all six primary native clients; both Linux 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 d37854d4c1d..ef759ff3693 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected POSIX system-SSH/no-tar transfer composition**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **POSIX SYSTEM-SSH LIVE FULL-SIZE LOCAL PACKAGE GREEN / COMMIT AND EXACT-HEAD CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-POSIX-SYSTEM-SSH-TREE-LIVE-LOCAL-001` records the bounded workflow oracle, focused/broad/release suites, typecheck, lint, max-lines, formatting, diff, protected-resolver, and no-product-consumer gates green. The purpose suite remains honestly skipped without its seven CI inputs and earns no live proof locally. Commit and push only this test/workflow/docs package, then require exact-head Linux x64/arm64 live metrics and all-six adjacent regression cells. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Session checkpoint: **POSIX SYSTEM-SSH LIVE FULL-SIZE EXACT-HEAD GREEN / EVIDENCE COMMIT NEXT — 2026-07-15, Codex implementation owner** — `E-M6-POSIX-SYSTEM-SSH-TREE-LIVE-CI-001` proves restricted-primitive exact full-size transfer, tree integrity, exclusive collision behavior, cancellation cleanup, connection reuse, latency, RSS, and one/four-channel bounds on native Linux x64 and arm64. All six primary native jobs, both Linux supplements, Windows x64 floor, PR Checks, Golden E2E, and computer-use pass; the artifact run is red only for the declared Windows arm64 hosted build 26200 versus required 26100 mismatch after complete runtime verification. Commit and push this evidence only, then audit the next disconnected transfer package. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -18080,6 +18080,61 @@ src/main/ssh/ssh-remote-node-resolution.ts src/main/ssh/ssh-remote-node-resoluti probe/cache, remote bundled-Node verification/install/publish/launch, product/Beta/fallback/default wiring, tuple enablement, publication, Apple signing, and SignPath remain absent. +### E-M6-POSIX-SYSTEM-SSH-TREE-LIVE-CI-001 — exact-head restricted OpenSSH proof + +- Date/owner/head: 2026-07-15, Codex implementation owner; exact head + `cdc25b2d1339fae9c4508b5b7453bb77fe9b9abc` on draft PR #8741. Implementation commit + `a56dded6ff0b10046e58e8ff2a3a99b8d6578291`; the exact-head follow-up + `cdc25b2d1339fae9c4508b5b7453bb77fe9b9abc` changes only Markdown formatting. Artifact run + `29524562239` uses Node 24.18.0 and stock `OpenSSH_9.6p1 Ubuntu-3ubuntu13.18` on both live cells. +- Linux x64 live job `87709667467`: Ubuntu 24 image `20260714.240.1`, X64; tuple + `linux-x64-glibc`, content `sha256:fc63ca342a5990f460ec6d72262a8542173dab20ce03c9b9cfb755b1c6057e6d`, + 34 files and 124,846,430 bytes. The default-one transfer takes 976.400 ms with 4,415,488 + incremental RSS bytes and peak active files 1. The explicit-four transfer takes 677.967 ms with + 1,777,664 incremental RSS bytes and peak active files 4. Cancellation settles in 27.553 ms with + 3,538,944 incremental RSS bytes and one progress update. The full purpose case passes in 3.84 + seconds. +- Linux arm64 live job `87709667417`: Ubuntu 24 arm64 image `20260714.61.1`, ARM64; tuple + `linux-arm64-glibc`, content `sha256:96f07f62af9b35304bb8ca0870ca4d8095e059bfa61dd1bc57e81b20f3fbca67`, + 34 files and 122,865,324 bytes. The default-one transfer takes 796.017 ms with 4,956,160 + incremental RSS bytes and peak active files 1. The explicit-four transfer takes 568.454 ms with + 1,048,576 incremental RSS bytes and peak active files 4. Cancellation settles in 15.614 ms with + 131,072 incremental RSS bytes and one progress update. The full purpose case passes in 3.23 + seconds. +- Boundary proved in both live jobs: the forced remote environment exposes only absolute `/bin/sh`, + `mkdir`, `chmod`, `cat`, and `rm`; host trust uses the generated pinned key and public-key-only + loopback OpenSSH. Each job verifies every exact path, byte, SHA-256, mode, file/byte aggregate, + default-one/four-channel bounds, preservation of a complete tree after exclusive-root collision, + cancellation after first bytes, no later progress, owned-root removal, and a still-usable + authenticated connection. Both serial and concurrent incremental RSS values are below the 80 MiB + ceiling and both cancellation settlements are below 10 seconds. The adjacent exact full-size SFTP + case also passes in each job. +- Six primary native contract cells pass at this exact head: Linux x64 `87709667467` and Linux arm64 + `87709667417` each report 96 files, 826 passed/one declared skip; Darwin arm64 `87709667400` and + Darwin x64 `87709667493` each report 96 files, 825 passed/two declared skips; Windows x64 + `87709667451` and Windows arm64 `87709667453` each report 97 files, 813 passed/18 declared skips. + The live test is skipped off Linux and earns evidence only from the two explicit live steps. +- Adjacent artifact cells: Linux arm64 oldest-userland supplement `87710915918`, Linux x64 supplement + `87710916014`, and Windows x64 floor `87711893263` pass. Windows arm64 floor `87711893219` is the + only artifact failure: the hosted image is Windows build 26200 while the contract requires 26100; + platform and architecture pass and only `osBuild` is false. Before rejection it verifies the exact + artifact digest, 60 entries/42 files/85,213,511 bytes, content + `sha256:02edf462be83c2864a89546d5344d348f9e07ce10964660342608a8c614e47db`, Node + 24.18.0/ABI 137, resized PTY exit 23, watcher lifecycle, two-second resource settlement, 49,242,112 + RSS bytes, 6,511.513 ms smoke, and 8,268.018 ms total. This is the declared hosted-runner gap, not a + change regression. +- Repository/adjacent runs: PR Checks `29524562195`, job `87709594108`, passes. Golden E2E run + `29524562284` passes Linux `87709594188` and macOS `87709594317`. Computer-use run `29524562096` + passes Windows and, on same-head failed-job attempt 2, Ubuntu `87713234187`. Attempt 1 Ubuntu job + `87709593614` passed its first two gedit cases but lost focus during `paste-text` with + `window_not_focused`; the unchanged rerun passes the complete job, so no code or timeout change is + attributed to that UI-focus flake. +- Disposition/residual boundary: this disconnected live package is complete. It proves native Linux + loopback OpenSSH, not high RTT, `MaxSessions=1`, proxy jumps, cross-family remotes, primitive + probe/cache, optional tar fast path, Windows transfer, remote bundled-Node verification/install/ + publish/launch, product/Beta/fallback/default behavior, tuple enablement, publication, Apple + signing, or SignPath. Legacy remains the only production/default path. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit From 20f8aebb8b7b3b2e2731df36afb97c7b4a297fef Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 15:12:03 -0400 Subject: [PATCH 233/267] feat(ssh): add bounded Windows relay file destination Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 17 +- ...github-release-implementation-checklist.md | 105 +++++- ...-relay-runtime-command-file-destination.ts | 288 +++++++++++++++++ ...sh-relay-runtime-posix-file-destination.ts | 302 ++---------------- ...h-relay-runtime-system-ssh-file-channel.ts | 6 +- ...y-runtime-windows-file-destination.test.ts | 260 +++++++++++++++ ...-relay-runtime-windows-file-destination.ts | 157 +++++++++ 9 files changed, 849 insertions(+), 289 deletions(-) create mode 100644 src/main/ssh/ssh-relay-runtime-command-file-destination.ts create mode 100644 src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts create mode 100644 src/main/ssh/ssh-relay-runtime-windows-file-destination.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index fe859cd0b6d..060ed032bc6 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -288,6 +288,7 @@ jobs: src/main/ssh/ssh-relay-runtime-source-stream.test.ts \ src/main/ssh/ssh-relay-runtime-posix-control-command.test.ts \ src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts \ + src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts \ src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts \ src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts \ @@ -1027,6 +1028,7 @@ jobs: src/main/ssh/ssh-relay-runtime-source-stream.test.ts ` src/main/ssh/ssh-relay-runtime-posix-control-command.test.ts ` src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts ` + src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts ` src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts ` src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 4f7d8a2ec2c..a6fe6d77643 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -202,6 +202,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-runtime-source-stream.test.ts', 'ssh-relay-runtime-posix-control-command.test.ts', 'ssh-relay-runtime-posix-file-destination.test.ts', + 'ssh-relay-runtime-windows-file-destination.test.ts', 'ssh-relay-runtime-posix-tree-transfer.test.ts', 'ssh-relay-runtime-system-ssh-file-channel.test.ts', 'ssh-relay-runtime-sftp-file-destination.test.ts', 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 618c748228a..fa3615303f9 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 @@ -9,9 +9,20 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — restricted-primitive -live/full-size POSIX system-SSH proof is exact-head green; record evidence and audit the next -disconnected transfer package, 2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected Windows +system-SSH bounded binary per-file destination is locally green; native Windows CI is next, +2026-07-15, Codex implementation owner.** +The active slice is limited to a fixed PowerShell 5.1/.NET receiver, a bounded binary stdin header, +one exact raw payload, exclusive `FileMode.CreateNew`, 64 KiB reads/writes, early-EOF and extra-byte +rejection, and owned-file cleanup. It must reuse the proven command-channel lifecycle without +changing POSIX behavior. Remote-tree composition, product callers, legacy upload/fallback/default +behavior, tuple enablement, publication, and SignPath remain out of scope. +`E-M6-WINDOWS-SYSTEM-SSH-FILE-LOCAL-RED-001` proves the workflow oracle passes 9/9 and the new +purpose suite fails solely because the audited production destination is absent. +`E-M6-WINDOWS-SYSTEM-SSH-FILE-LOCAL-001` records 14/15 Windows purpose cases locally with the real +PowerShell case honestly skipped, all 25 prior POSIX destination cases, 234 focused cases, 670 broad +relay cases, 282/282 release contracts, typecheck, full lint, formatting, diff, protected-resolver, +and no-product-consumer gates. Native Windows x64/arm64 PowerShell 5.1 proof remains required. `E-M6-POSIX-SYSTEM-SSH-TREE-AUDIT-001` requires a bounded no-input control-command owner rather than the legacy unbounded-output command helper, exclusive `0700` root creation, parent-first declared directories, exact per-file source-stream composition, owned-root cleanup, and a worst-case 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 ef759ff3693..21a7db30459 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected POSIX system-SSH/no-tar transfer composition**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **POSIX SYSTEM-SSH LIVE FULL-SIZE EXACT-HEAD GREEN / EVIDENCE COMMIT NEXT — 2026-07-15, Codex implementation owner** — `E-M6-POSIX-SYSTEM-SSH-TREE-LIVE-CI-001` proves restricted-primitive exact full-size transfer, tree integrity, exclusive collision behavior, cancellation cleanup, connection reuse, latency, RSS, and one/four-channel bounds on native Linux x64 and arm64. All six primary native jobs, both Linux supplements, Windows x64 floor, PR Checks, Golden E2E, and computer-use pass; the artifact run is red only for the declared Windows arm64 hosted build 26200 versus required 26100 mismatch after complete runtime verification. Commit and push this evidence only, then audit the next disconnected transfer package. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected Windows system-SSH bounded binary per-file destination**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **WINDOWS SYSTEM-SSH PER-FILE DESTINATION LOCALLY GREEN / NATIVE WINDOWS CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-WINDOWS-SYSTEM-SSH-FILE-LOCAL-001` proves the disconnected fixed-command binary receiver contract, bounded header ownership, generic lifecycle extraction, every prior POSIX regression, broad relay/release/static gates, and production isolation locally. The real PowerShell 5.1 case is explicitly skipped on macOS and earns no native claim; commit and push the exact package, then require native Windows x64/arm64 proof before closing it. Do not compose a remote tree, connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work in this package. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -18135,6 +18135,98 @@ src/main/ssh/ssh-remote-node-resolution.ts src/main/ssh/ssh-remote-node-resoluti publish/launch, product/Beta/fallback/default behavior, tuple enablement, publication, Apple signing, or SignPath. Legacy remains the only production/default path. +### E-M6-WINDOWS-SYSTEM-SSH-FILE-AUDIT-001 — bounded per-file receiver contract + +- Date/owner/base: 2026-07-15, Codex implementation owner; audit and purpose-test package atop exact + base `aba3f7c649b8412b40871d8c3a82938e0094739e`. +- Scope: one disconnected per-file destination over the existing authenticated command-channel + abstraction. The client sends one fixed PowerShell 5.1/.NET receiver command, then a bounded binary + header containing protocol magic/version, strict UTF-8 path length, safe-integer payload length, + and path bytes, followed by exact raw payload bytes. No path or payload is embedded in PowerShell + source, a command argument, JSON, base64, or a whole-file buffer. +- Remote contract: use `[Console]::OpenStandardInput()`, strict UTF-8 decoding, a 64 KiB payload + buffer, and `FileStream` with `FileMode.CreateNew`, `FileAccess.Write`, `FileShare.None`, and + sequential-scan intent. Reject malformed headers, path bounds, early EOF, and one extra byte. On + any post-create failure, close the stream before deleting only the file this receiver created. +- Lifecycle contract: extract the already-proven command-file write/EOF/cancellation/forced- + settlement owner from the POSIX wrapper without changing behavior. Header acceptance is awaited + before returning the Windows destination; a header/open failure is joined with bounded channel + cleanup. The exact caller signal remains single-owned. +- Required RED/green proof: purpose cases for fixed command/path separation, exact header bytes, + invalid input before channel open, retained-header cancellation, remote nonzero settlement, + zero-byte and chunked binary payloads, collision preservation, early EOF/extra-byte cleanup, and a + native Windows PowerShell 5.1 process oracle. Pin the suite exactly once in both native artifact + workflow command families and update the workflow oracle before implementation. +- Explicit residual boundary: no Windows remote-tree/staging-root owner, case-collision scan, remote + capability classification/cache, live OpenSSH remote, full-size/runtime measurement, endpoint- + protection fault, long-path qualification, bundled-Node verification/install/publish/launch, + product/Beta/settings/fallback/tuple/publication/default behavior, or SignPath is included. Legacy + remains the only production/default path and every tuple stays disabled. + +### E-M6-WINDOWS-SYSTEM-SSH-FILE-LOCAL-RED-001 — audited destination is absent + +- Date/owner/base: 2026-07-15, Codex implementation owner; purpose test, native-workflow wiring, and + checklist-only diff atop exact base `aba3f7c649b8412b40871d8c3a82938e0094739e` on Apple arm64, + macOS 26.2 build 25C56, Node 26.0.0, pnpm 10.24.0. +- Command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs`. +- Result: the 9/9 native-workflow oracle cases pass and prove the purpose suite occurs exactly once + in both native artifact command families. The purpose suite fails before collection solely because + `./ssh-relay-runtime-windows-file-destination` does not exist. This is the intended RED boundary; + no production module has been added or changed. +- Residual boundary: no PowerShell behavior or Windows runner claim follows from missing-module RED. + All audit, purpose, native Windows, POSIX regression, broad relay, static, and isolation gates remain + required. + +### E-M6-WINDOWS-SYSTEM-SSH-FILE-LOCAL-001 — bounded receiver package is locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted implementation, + purpose-test, workflow, and checklist package atop exact base + `aba3f7c649b8412b40871d8c3a82938e0094739e` on Apple arm64, macOS 26.2 build 25C56, Node 26.0.0, + pnpm 10.24.0. The local Node version is newer than the repository's Node 24 contract, and the real + PowerShell 5.1 purpose case is skipped off Windows, so exact-head native workflow proof remains + mandatory. +- Implementation: extract the proven 250 ms graceful/2-second forced command-file lifecycle into + `ssh-relay-runtime-command-file-destination.ts` while retaining POSIX error identity and all 25 + POSIX purpose cases. Add a disconnected Windows wrapper with an at-most 32,788-byte header, + 32-KiB strict UTF-8 path bound, safe-integer payload size, fixed encoded receiver command, + 64-KiB remote buffer, `CreateNew`/`FileShare.None`/sequential-scan stream, exact EOF/extra-byte + rejection, and owned-file cleanup. The existing legacy Windows uploader is unchanged. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1` with the Windows purpose, POSIX control/file/tree, system-channel, source-tree/scan/ + stream, system-fallback, connection, and workflow-oracle suites. Result: 11 files, 234 passed and two + declared platform skips out of 236; 7.95 seconds real, 171,311,104-byte maximum RSS, 96,097,560-byte + peak footprint, zero swaps and block I/O. The Windows file suite contributes 14 passed plus one + macOS-skipped real-PowerShell case; the workflow oracle passes 9/9. +- Broad relay command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts`. Result: 51 files passed, five files skipped; 670 + passed and eight declared skips out of 678; 23.96 seconds real, 248,102,912-byte maximum RSS, + 96,163,264-byte peak footprint, zero swaps and block I/O. +- Release-contract command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs`. Result: 50 files + and 282/282 cases pass; 14.35 seconds real, 189,628,416-byte maximum RSS, 95,769,976-byte peak + footprint, zero swaps and block I/O. +- Static evidence: `/usr/bin/time -l pnpm typecheck` passes in 2.45 seconds real with + 1,241,055,232-byte maximum RSS and 96,146,784-byte peak footprint. `/usr/bin/time -l pnpm lint` + passes in 20.18 seconds real with 2,063,761,408-byte maximum RSS and 96,343,488-byte peak footprint; + its 26 warnings are pre-existing outside this diff. Full lint proves switch exhaustiveness, 41 + reliability gates, the 355-entry max-lines ratchet with no new bypass, bundled guides, and all + localization checks. Targeted `oxlint` and `oxfmt --check` pass. +- Isolation evidence: `git diff --check` passes; `git diff --exit-code HEAD -- +src/main/ssh/ssh-remote-node-resolution.ts src/main/ssh/ssh-remote-node-resolution.test.ts` is empty. + An asserted non-test reference search finds the Windows destination export only in its owning + production module. The intended diff is limited to the generic lifecycle extraction, POSIX type/ + wrapper delegation, system-channel type generalization, disconnected Windows module/purpose test, + native workflow/oracle wiring, and both checklist documents. No legacy uploader, product importer, + settings, Beta, fallback, tuple, publication, default-path, or signing behavior changes. +- Residual boundary: local execution does not prove that the fixed script parses or behaves under + Windows PowerShell 5.1/.NET Framework 4.8. Exact-head native Windows x64/arm64 purpose proof and all + adjacent native regression cells remain required. A Windows remote tree/staging owner, live + OpenSSH, full-size runtime, endpoint-protection faults, long-path qualification, capability + classification/cache, remote verification/install/publish/launch, product/Beta/fallback/default + wiring, tuple enablement, publication, Apple signing, and SignPath remain absent. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -18193,10 +18285,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Audit and RED-test only the smallest purpose-named live/full-size loopback OpenSSH proof that calls -the exact disconnected POSIX system-SSH tree owner on target-native Linux x64/arm64 CI. Do not -check the broad Milestone 6 SFTP or POSIX system-SSH items yet. Do not add an Electron/startup/product -importer, per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. +Commit and push the exact `E-M6-WINDOWS-SYSTEM-SSH-FILE-LOCAL-001` package, then require the real +PowerShell 5.1 process oracle on native Windows x64/arm64 plus all-six native and adjacent repository +regression cells. Do not check the broad Milestone 6 Windows system-SSH item yet. Do not add a remote- +tree owner, Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, +release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. diff --git a/src/main/ssh/ssh-relay-runtime-command-file-destination.ts b/src/main/ssh/ssh-relay-runtime-command-file-destination.ts new file mode 100644 index 00000000000..990b88803b4 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-command-file-destination.ts @@ -0,0 +1,288 @@ +import type { SshRelayRuntimeSourceDestination } from './ssh-relay-runtime-source-stream' + +type ChannelWriteCallback = (error?: Error) => void + +export type SshRelayRuntimeCommandFileChannel = Readonly<{ + write: (chunk: Buffer, callback: ChannelWriteCallback) => void + end: () => void + settled: Promise + requestClose: () => void + forceClose: () => void +}> + +export type OpenSshRelayRuntimeCommandFileDestinationOptions = Readonly<{ + command: string + fileKind: 'POSIX' | 'Windows' + signal: AbortSignal + openChannel: (command: string, signal: AbortSignal) => Promise +}> + +export const SSH_RELAY_RUNTIME_COMMAND_FILE_DESTINATION_LIMITS = Object.freeze({ + gracefulCloseMs: 250, + totalCloseMs: 2_000 +}) + +type DestinationState = 'open' | 'closing' | 'complete' | 'aborting' | 'aborted' +type ChannelSettlement = Readonly<{ ok: true } | { ok: false; error: unknown }> +type AbortOutcome = Readonly<{ reason: unknown }> + +function validateOptions(options: OpenSshRelayRuntimeCommandFileDestinationOptions): void { + if ( + !options || + typeof options.command !== 'string' || + options.command === '' || + (options.fileKind !== 'POSIX' && options.fileKind !== 'Windows') || + typeof options.openChannel !== 'function' || + !options.signal + ) { + throw new Error('SSH relay runtime command file destination input is invalid') + } +} + +function validateChannel( + channel: SshRelayRuntimeCommandFileChannel, + fileKind: 'POSIX' | 'Windows' +): void { + if ( + !channel || + typeof channel.write !== 'function' || + typeof channel.end !== 'function' || + typeof channel.settled?.then !== 'function' || + typeof channel.requestClose !== 'function' || + typeof channel.forceClose !== 'function' + ) { + throw new Error(`SSH relay runtime ${fileKind} file channel is invalid`) + } +} + +function cleanupFailure( + failures: readonly unknown[], + fileKind: 'POSIX' | 'Windows' +): unknown | undefined { + if (failures.length === 0) { + return undefined + } + return failures.length === 1 + ? failures[0] + : new AggregateError(failures, `SSH relay runtime ${fileKind} file channel cleanup failed`) +} + +function waitForSettlement( + settlement: Promise, + timeoutMs: number +): Promise { + return new Promise((resolve) => { + const timeout = setTimeout(() => resolve(false), timeoutMs) + void settlement.then(() => { + clearTimeout(timeout) + resolve(true) + }) + }) +} + +async function settleCancelledChannel( + channel: SshRelayRuntimeCommandFileChannel, + settlement: Promise, + fileKind: 'POSIX' | 'Windows' +): Promise { + const failures: unknown[] = [] + try { + channel.requestClose() + } catch (error) { + failures.push(error) + } + + const graceful = await waitForSettlement( + settlement, + SSH_RELAY_RUNTIME_COMMAND_FILE_DESTINATION_LIMITS.gracefulCloseMs + ) + if (!graceful) { + try { + channel.forceClose() + } catch (error) { + failures.push(error) + } + const forced = await waitForSettlement( + settlement, + SSH_RELAY_RUNTIME_COMMAND_FILE_DESTINATION_LIMITS.totalCloseMs - + SSH_RELAY_RUNTIME_COMMAND_FILE_DESTINATION_LIMITS.gracefulCloseMs + ) + if (!forced) { + failures.unshift(new Error(`SSH relay runtime ${fileKind} file channel settlement timed out`)) + } + } + + const failure = cleanupFailure(failures, fileKind) + if (failure !== undefined) { + throw failure + } +} + +function createDestination( + signal: AbortSignal, + channel: SshRelayRuntimeCommandFileChannel, + fileKind: 'POSIX' | 'Windows' +): SshRelayRuntimeSourceDestination { + const channelSettlement: Promise = channel.settled.then( + () => ({ ok: true }), + (error: unknown) => ({ ok: false, error }) + ) + let state: DestinationState = 'open' + let activeOperation: Promise | undefined + let abortPromise: Promise | undefined + let abortReason: unknown + let resolveAbortOutcome: (outcome: AbortOutcome) => void = () => {} + const abortOutcome = new Promise((resolve) => { + resolveAbortOutcome = resolve + }) + + const assertWritable = (): void => { + if (state !== 'open') { + throw new Error(`SSH relay runtime ${fileKind} file destination is closed`) + } + if (activeOperation) { + throw new Error(`SSH relay runtime ${fileKind} file destination has a concurrent operation`) + } + } + const runExclusive = (operation: () => Promise): Promise => { + const pending = operation() + activeOperation = pending + void pending + .finally(() => { + if (activeOperation === pending) { + activeOperation = undefined + } + }) + .catch(() => {}) + return pending + } + const waitForOperation = async (operation: Promise): Promise => { + const result = await Promise.race([ + operation.then( + () => ({ kind: 'complete' as const }), + (error: unknown) => ({ kind: 'failed' as const, error }) + ), + abortOutcome.then((outcome) => ({ kind: 'aborted' as const, outcome })) + ]) + if (result.kind === 'aborted') { + throw result.outcome.reason + } + if (state === 'aborting' || state === 'aborted') { + throw (await abortOutcome).reason + } + if (result.kind === 'failed') { + throw result.error + } + } + + const performAbort = async (): Promise => { + const reason = abortReason + let failure: unknown + try { + await settleCancelledChannel(channel, channelSettlement, fileKind) + } catch (error) { + failure = error + } finally { + state = 'aborted' + signal.removeEventListener('abort', onSignalAbort) + // Why: retained transport buffers are released only after the channel settles or times out. + resolveAbortOutcome({ reason }) + } + if (failure !== undefined) { + throw failure + } + } + const abort = (reason: unknown): Promise => { + if (abortPromise) { + return abortPromise + } + if (state === 'complete' || state === 'aborted') { + abortPromise = Promise.resolve() + return abortPromise + } + state = 'aborting' + abortReason = reason + abortPromise = performAbort() + return abortPromise + } + function onSignalAbort(): void { + void abort(signal.reason).catch(() => {}) + } + + const write = (chunk: Buffer): Promise => { + try { + assertWritable() + } catch (error) { + return Promise.reject(error) + } + if (!Buffer.isBuffer(chunk) || chunk.length === 0) { + return Promise.reject( + new Error(`SSH relay runtime ${fileKind} file destination chunk is empty`) + ) + } + return runExclusive(async () => { + signal.throwIfAborted() + // Why: the callback is the byte-retention boundary before a source worker reuses its buffer. + const accepted = new Promise((resolve, reject) => { + channel.write(chunk, (error) => (error ? reject(error) : resolve())) + }) + await waitForOperation(accepted) + signal.throwIfAborted() + }) + } + const close = (): Promise => { + try { + assertWritable() + } catch (error) { + return Promise.reject(error) + } + state = 'closing' + return runExclusive(async () => { + signal.throwIfAborted() + channel.end() + const settled = await Promise.race([ + channelSettlement, + abortOutcome.then((outcome) => ({ ok: false as const, error: outcome.reason })) + ]) + if (state === 'aborting' || state === 'aborted') { + throw (await abortOutcome).reason + } + if (!settled.ok) { + throw settled.error + } + signal.throwIfAborted() + state = 'complete' + signal.removeEventListener('abort', onSignalAbort) + }) + } + + signal.addEventListener('abort', onSignalAbort, { once: true }) + if (signal.aborted) { + onSignalAbort() + } + return Object.freeze({ write, close, abort }) +} + +export async function openSshRelayRuntimeCommandFileDestination( + options: OpenSshRelayRuntimeCommandFileDestinationOptions +): Promise { + validateOptions(options) + options.signal.throwIfAborted() + const channel = await options.openChannel(options.command, options.signal) + validateChannel(channel, options.fileKind) + const destination = createDestination(options.signal, channel, options.fileKind) + try { + options.signal.throwIfAborted() + return destination + } catch (error) { + try { + await destination.abort(error) + } catch (cleanupError) { + throw new AggregateError( + [error, cleanupError], + `SSH relay runtime ${options.fileKind} file open cancellation cleanup failed` + ) + } + throw error + } +} diff --git a/src/main/ssh/ssh-relay-runtime-posix-file-destination.ts b/src/main/ssh/ssh-relay-runtime-posix-file-destination.ts index 1bf7858351b..56b0f9a3dd9 100644 --- a/src/main/ssh/ssh-relay-runtime-posix-file-destination.ts +++ b/src/main/ssh/ssh-relay-runtime-posix-file-destination.ts @@ -1,14 +1,11 @@ +import { + openSshRelayRuntimeCommandFileDestination, + SSH_RELAY_RUNTIME_COMMAND_FILE_DESTINATION_LIMITS, + type SshRelayRuntimeCommandFileChannel +} from './ssh-relay-runtime-command-file-destination' import type { SshRelayRuntimeSourceDestination } from './ssh-relay-runtime-source-stream' -type ChannelWriteCallback = (error?: Error) => void - -export type SshRelayRuntimePosixFileChannel = Readonly<{ - write: (chunk: Buffer, callback: ChannelWriteCallback) => void - end: () => void - settled: Promise - requestClose: () => void - forceClose: () => void -}> +export type SshRelayRuntimePosixFileChannel = SshRelayRuntimeCommandFileChannel export type OpenSshRelayRuntimePosixFileDestinationOptions = Readonly<{ remotePath: string @@ -17,30 +14,8 @@ export type OpenSshRelayRuntimePosixFileDestinationOptions = Readonly<{ openChannel: (command: string, signal: AbortSignal) => Promise }> -export const SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS = Object.freeze({ - gracefulCloseMs: 250, - totalCloseMs: 2_000 -}) - -type DestinationState = 'open' | 'closing' | 'complete' | 'aborting' | 'aborted' - -type ChannelSettlement = Readonly<{ ok: true } | { ok: false; error: unknown }> - -type AbortOutcome = Readonly<{ reason: unknown }> - -function validateRemotePath(remotePath: string): void { - const segments = remotePath.split('/') - if ( - remotePath === '/' || - !remotePath.startsWith('/') || - remotePath.includes('\0') || - remotePath.includes('\n') || - remotePath.includes('\r') || - segments.slice(1).some((segment) => segment === '' || segment === '.' || segment === '..') - ) { - throw new Error('SSH relay runtime POSIX file destination path is invalid') - } -} +export const SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS = + SSH_RELAY_RUNTIME_COMMAND_FILE_DESTINATION_LIMITS function validateOptions(options: OpenSshRelayRuntimePosixFileDestinationOptions): void { if ( @@ -51,22 +26,19 @@ function validateOptions(options: OpenSshRelayRuntimePosixFileDestinationOptions ) { throw new Error('SSH relay runtime POSIX file destination input is invalid') } - validateRemotePath(options.remotePath) - if (options.mode !== 0o644 && options.mode !== 0o755) { - throw new Error('SSH relay runtime POSIX file destination mode is invalid') - } -} - -function validateChannel(channel: SshRelayRuntimePosixFileChannel): void { + const segments = options.remotePath.split('/') if ( - !channel || - typeof channel.write !== 'function' || - typeof channel.end !== 'function' || - typeof channel.settled?.then !== 'function' || - typeof channel.requestClose !== 'function' || - typeof channel.forceClose !== 'function' + options.remotePath === '/' || + !options.remotePath.startsWith('/') || + options.remotePath.includes('\0') || + options.remotePath.includes('\n') || + options.remotePath.includes('\r') || + segments.slice(1).some((segment) => segment === '' || segment === '.' || segment === '..') ) { - throw new Error('SSH relay runtime POSIX file channel is invalid') + throw new Error('SSH relay runtime POSIX file destination path is invalid') + } + if (options.mode !== 0o644 && options.mode !== 0o755) { + throw new Error('SSH relay runtime POSIX file destination mode is invalid') } } @@ -81,238 +53,14 @@ function buildCommand(remotePath: string, mode: 0o644 | 0o755): string { return `umask 077; set -C; cat > ${quotedPath} && chmod ${finalMode} ${quotedPath}` } -function cleanupFailure(failures: readonly unknown[]): unknown | undefined { - if (failures.length === 0) { - return undefined - } - return failures.length === 1 - ? failures[0] - : new AggregateError(failures, 'SSH relay runtime POSIX file channel cleanup failed') -} - -function waitForSettlement( - settlement: Promise, - timeoutMs: number -): Promise { - return new Promise((resolve) => { - const timeout = setTimeout(() => resolve(false), timeoutMs) - void settlement.then(() => { - clearTimeout(timeout) - resolve(true) - }) - }) -} - -async function settleCancelledChannel( - channel: SshRelayRuntimePosixFileChannel, - settlement: Promise -): Promise { - const failures: unknown[] = [] - try { - channel.requestClose() - } catch (error) { - failures.push(error) - } - - const graceful = await waitForSettlement( - settlement, - SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS.gracefulCloseMs - ) - if (!graceful) { - try { - channel.forceClose() - } catch (error) { - failures.push(error) - } - const forced = await waitForSettlement( - settlement, - SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS.totalCloseMs - - SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS.gracefulCloseMs - ) - if (!forced) { - failures.unshift(new Error('SSH relay runtime POSIX file channel settlement timed out')) - } - } - - const failure = cleanupFailure(failures) - if (failure !== undefined) { - throw failure - } -} - -function createDestination( - options: OpenSshRelayRuntimePosixFileDestinationOptions, - channel: SshRelayRuntimePosixFileChannel -): SshRelayRuntimeSourceDestination { - const { signal } = options - const channelSettlement: Promise = channel.settled.then( - () => ({ ok: true }), - (error: unknown) => ({ ok: false, error }) - ) - let state: DestinationState = 'open' - let activeOperation: Promise | undefined - let abortPromise: Promise | undefined - let abortReason: unknown - let resolveAbortOutcome: (outcome: AbortOutcome) => void = () => {} - const abortOutcome = new Promise((resolve) => { - resolveAbortOutcome = resolve - }) - - const assertWritable = (): void => { - if (state !== 'open') { - throw new Error('SSH relay runtime POSIX file destination is closed') - } - if (activeOperation) { - throw new Error('SSH relay runtime POSIX file destination has a concurrent operation') - } - } - - const runExclusive = (operation: () => Promise): Promise => { - const pending = operation() - activeOperation = pending - void pending - .finally(() => { - if (activeOperation === pending) { - activeOperation = undefined - } - }) - .catch(() => {}) - return pending - } - - const waitForOperation = async (operation: Promise): Promise => { - const result = await Promise.race([ - operation.then( - () => ({ kind: 'complete' as const }), - (error: unknown) => ({ kind: 'failed' as const, error }) - ), - abortOutcome.then((outcome) => ({ kind: 'aborted' as const, outcome })) - ]) - if (result.kind === 'aborted') { - throw result.outcome.reason - } - if (state === 'aborting' || state === 'aborted') { - const outcome = await abortOutcome - throw outcome.reason - } - if (result.kind === 'failed') { - throw result.error - } - } - - const performAbort = async (): Promise => { - const reason = abortReason - let failure: unknown - try { - await settleCancelledChannel(channel, channelSettlement) - } catch (error) { - failure = error - } finally { - state = 'aborted' - signal.removeEventListener('abort', onSignalAbort) - // Why: retained transport buffers are released only after the channel has settled or timed out. - resolveAbortOutcome({ reason }) - } - if (failure !== undefined) { - throw failure - } - } - - const abort = (reason: unknown): Promise => { - if (abortPromise) { - return abortPromise - } - if (state === 'complete' || state === 'aborted') { - abortPromise = Promise.resolve() - return abortPromise - } - state = 'aborting' - abortReason = reason - abortPromise = performAbort() - return abortPromise - } - - function onSignalAbort(): void { - void abort(signal.reason).catch(() => {}) - } - - const write = (chunk: Buffer): Promise => { - try { - assertWritable() - } catch (error) { - return Promise.reject(error) - } - if (!Buffer.isBuffer(chunk) || chunk.length === 0) { - return Promise.reject(new Error('SSH relay runtime POSIX file destination chunk is empty')) - } - return runExclusive(async () => { - signal.throwIfAborted() - // Why: the callback is the byte-retention boundary before a source worker reuses its buffer. - const accepted = new Promise((resolve, reject) => { - channel.write(chunk, (error) => (error ? reject(error) : resolve())) - }) - await waitForOperation(accepted) - signal.throwIfAborted() - }) - } - - const close = (): Promise => { - try { - assertWritable() - } catch (error) { - return Promise.reject(error) - } - state = 'closing' - return runExclusive(async () => { - signal.throwIfAborted() - channel.end() - const settled = await Promise.race([ - channelSettlement, - abortOutcome.then((outcome) => ({ ok: false as const, error: outcome.reason })) - ]) - if (state === 'aborting' || state === 'aborted') { - const outcome = await abortOutcome - throw outcome.reason - } - if (!settled.ok) { - throw settled.error - } - signal.throwIfAborted() - state = 'complete' - signal.removeEventListener('abort', onSignalAbort) - }) - } - - signal.addEventListener('abort', onSignalAbort, { once: true }) - if (signal.aborted) { - onSignalAbort() - } - return Object.freeze({ write, close, abort }) -} - export async function openSshRelayRuntimePosixFileDestination( options: OpenSshRelayRuntimePosixFileDestinationOptions ): Promise { validateOptions(options) - options.signal.throwIfAborted() - const channel = await options.openChannel( - buildCommand(options.remotePath, options.mode), - options.signal - ) - validateChannel(channel) - const destination = createDestination(options, channel) - try { - options.signal.throwIfAborted() - return destination - } catch (error) { - try { - await destination.abort(error) - } catch (cleanupError) { - throw new AggregateError( - [error, cleanupError], - 'SSH relay runtime POSIX file open cancellation cleanup failed' - ) - } - throw error - } + return await openSshRelayRuntimeCommandFileDestination({ + command: buildCommand(options.remotePath, options.mode), + fileKind: 'POSIX', + signal: options.signal, + openChannel: options.openChannel + }) } diff --git a/src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.ts b/src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.ts index ace3381340d..daba2e3189c 100644 --- a/src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.ts +++ b/src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.ts @@ -1,5 +1,5 @@ import type { ClientChannel } from 'ssh2' -import type { SshRelayRuntimePosixFileChannel } from './ssh-relay-runtime-posix-file-destination' +import type { SshRelayRuntimeCommandFileChannel } from './ssh-relay-runtime-command-file-destination' import type { SystemSshCommandChannel } from './system-ssh-command' export type SshRelayRuntimeSystemSshConnection = Readonly<{ @@ -134,7 +134,7 @@ function waitForSettlement(channel: SystemSshCommandChannel): Promise { }) } -function adaptChannel(channel: SystemSshCommandChannel): SshRelayRuntimePosixFileChannel { +function adaptChannel(channel: SystemSshCommandChannel): SshRelayRuntimeCommandFileChannel { validateChannel(channel) const settled = waitForSettlement(channel) const input = channel.stdin as unknown as { @@ -184,7 +184,7 @@ export async function openSshRelayRuntimeSystemSshFileChannel( connection: SshRelayRuntimeSystemSshConnection, command: string, signal: AbortSignal -): Promise { +): Promise { validateInput(connection, command, signal) // Why: the file destination owns this signal's bounded teardown; forwarding it would double-kill. const channel = (await connection.exec(command)) as SystemSshCommandChannel diff --git a/src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts b/src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts new file mode 100644 index 00000000000..cb7b06d2f65 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts @@ -0,0 +1,260 @@ +import { spawn } from 'node:child_process' +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { + openSshRelayRuntimeWindowsFileDestination, + SSH_RELAY_RUNTIME_WINDOWS_FILE_DESTINATION_LIMITS +} from './ssh-relay-runtime-windows-file-destination' + +type Callback = (error?: Error) => void + +function createChannel() { + let resolveSettled: () => void = () => {} + let rejectSettled: (error: Error) => void = () => {} + const settled = new Promise((resolve, reject) => { + resolveSettled = resolve + rejectSettled = reject + }) + const channel = { + write: vi.fn((_chunk: Buffer, callback: Callback) => callback()), + end: vi.fn(), + settled, + requestClose: vi.fn(), + forceClose: vi.fn() + } + return { channel, resolve: resolveSettled, reject: rejectSettled } +} + +function decodeHeader(frame: Buffer) { + const pathLength = frame.readUInt32LE(8) + return { + magic: frame.subarray(0, 8).toString('ascii'), + pathLength, + expectedSize: frame.readBigUInt64LE(12), + remotePath: frame.subarray(20, 20 + pathLength).toString('utf8') + } +} + +function decodePowerShellCommand(command: string): string { + const encoded = command.match(/-EncodedCommand\s+([A-Za-z0-9+/=]+)$/u)?.[1] + if (!encoded) { + throw new Error('PowerShell receiver command is not encoded') + } + return Buffer.from(encoded, 'base64').toString('utf16le') +} + +function openDestination( + channel: ReturnType['channel'], + overrides: Partial[0]> = {} +) { + return openSshRelayRuntimeWindowsFileDestination({ + remotePath: 'C:/Users/测试/.orca-remote/stage/bin/node.exe', + expectedSize: 65_537, + signal: new AbortController().signal, + openChannel: vi.fn(async () => channel), + ...overrides + }) +} + +afterEach(() => { + vi.restoreAllMocks() +}) + +describe('SSH relay runtime Windows system-SSH file destination', () => { + it('opens one fixed receiver and awaits an exact bounded binary header', async () => { + const { channel, resolve } = createChannel() + let headerCallback: Callback | undefined + channel.write.mockImplementationOnce((_chunk, callback) => { + headerCallback = callback + }) + const openChannel = vi.fn(async (_command: string, _signal: AbortSignal) => channel) + let opened = false + const opening = openDestination(channel, { openChannel }).then((destination) => { + opened = true + return destination + }) + + await vi.waitFor(() => expect(channel.write).toHaveBeenCalledOnce()) + expect(opened).toBe(false) + expect(openChannel).toHaveBeenCalledOnce() + const command = openChannel.mock.calls[0]?.[0] ?? '' + expect(command).toMatch(/^powershell\.exe .* -EncodedCommand [A-Za-z0-9+/=]+$/u) + expect(command).not.toContain('测试') + expect(command).not.toContain('65_537') + const script = decodePowerShellCommand(command) + expect(script).toContain('[Console]::OpenStandardInput()') + expect(script).toContain('[IO.FileMode]::CreateNew') + expect(script).toContain('[IO.FileShare]::None') + expect(script).toContain('New-Object byte[] 65536') + expect(script).toContain('$inputStream.ReadByte() -ne -1') + for (const forbidden of ['ReadToEnd', 'CopyTo(', 'FromBase64String', 'ConvertFrom-Json']) { + expect(script).not.toContain(forbidden) + } + + const frame = channel.write.mock.calls[0]?.[0] + expect(Buffer.isBuffer(frame)).toBe(true) + expect(frame.length).toBeLessThanOrEqual( + SSH_RELAY_RUNTIME_WINDOWS_FILE_DESTINATION_LIMITS.maximumHeaderBytes + ) + expect(decodeHeader(frame)).toEqual({ + magic: 'ORCARLY1', + pathLength: Buffer.byteLength('C:/Users/测试/.orca-remote/stage/bin/node.exe'), + expectedSize: 65_537n, + remotePath: 'C:/Users/测试/.orca-remote/stage/bin/node.exe' + }) + + headerCallback?.() + const destination = await opening + const closing = destination.close() + resolve() + await closing + }) + + it.each([ + ['', 1], + ['relative/file', 1], + ['C:/', 1], + ['C:/owned/../file', 1], + ['C:/owned/report.txt:stream', 1], + ['C:/owned/NUL', 1], + ['C:/owned/file\nnext', 1], + ['C:/owned/file', -1], + ['C:/owned/file', Number.MAX_SAFE_INTEGER + 1], + ['C:/owned/file', 1.5] + ])('rejects hostile path %j or size %j before channel open', async (remotePath, expectedSize) => { + const { channel } = createChannel() + const openChannel = vi.fn(async () => channel) + + await expect( + openDestination(channel, { remotePath, expectedSize, openChannel }) + ).rejects.toThrow(/invalid/i) + expect(openChannel).not.toHaveBeenCalled() + }) + + it('joins a failed header write with bounded channel cleanup', async () => { + const { channel, resolve } = createChannel() + channel.write.mockImplementationOnce((_chunk, callback) => { + callback(new Error('header rejected')) + }) + channel.requestClose.mockImplementation(() => resolve()) + + await expect(openDestination(channel)).rejects.toThrow('header rejected') + expect(channel.requestClose).toHaveBeenCalledOnce() + expect(channel.forceClose).not.toHaveBeenCalled() + }) + + it('settles retained-header cancellation before rejecting open', async () => { + const { channel, resolve } = createChannel() + const controller = new AbortController() + const reason = new Error('cancelled retained Windows header') + channel.write.mockImplementationOnce(() => {}) + channel.requestClose.mockImplementation(() => resolve()) + const opening = openDestination(channel, { signal: controller.signal }) + + await vi.waitFor(() => expect(channel.write).toHaveBeenCalledOnce()) + controller.abort(reason) + await expect(opening).rejects.toBe(reason) + expect(channel.requestClose).toHaveBeenCalledOnce() + expect(channel.forceClose).not.toHaveBeenCalled() + }) + + it('propagates receiver failure only after EOF and remote settlement', async () => { + const { channel, reject } = createChannel() + const destination = await openDestination(channel, { expectedSize: 0 }) + const closing = destination.close() + + expect(channel.end).toHaveBeenCalledOnce() + reject(new Error('receiver rejected extra byte')) + await expect(closing).rejects.toThrow('receiver rejected extra byte') + }) + + it.skipIf(process.platform !== 'win32')( + 'proves PowerShell 5.1 binary fidelity, exclusive collision, and exact-size rejection', + async () => { + const root = await mkdtemp(path.join(tmpdir(), 'orca-relay-win-file-')) + try { + const payload = Buffer.from([0, 13, 10, 255, 128, 1, 2, 3, 0, 254]) + const exactPath = path.join(root, '测试 payload.bin') + const exact = await openSshRelayRuntimeWindowsFileDestination({ + remotePath: exactPath, + expectedSize: payload.length, + signal: new AbortController().signal, + openChannel: openPowerShellChannel + }) + await exact.write(payload.subarray(0, 3)) + await exact.write(payload.subarray(3)) + await exact.close() + expect(await readFile(exactPath)).toEqual(payload) + + const collisionPath = path.join(root, 'collision.bin') + await writeFile(collisionPath, 'original') + const collision = await openSshRelayRuntimeWindowsFileDestination({ + remotePath: collisionPath, + expectedSize: 0, + signal: new AbortController().signal, + openChannel: openPowerShellChannel + }) + await expect(collision.close()).rejects.toThrow(/PowerShell receiver failed/i) + expect(await readFile(collisionPath, 'utf8')).toBe('original') + + for (const [name, expectedSize, bytes] of [ + ['early.bin', 2, Buffer.from([1])], + ['extra.bin', 1, Buffer.from([1, 2])] + ] as const) { + const remotePath = path.join(root, name) + const destination = await openSshRelayRuntimeWindowsFileDestination({ + remotePath, + expectedSize, + signal: new AbortController().signal, + openChannel: openPowerShellChannel + }) + await destination.write(bytes) + await expect(destination.close()).rejects.toThrow(/PowerShell receiver failed/i) + await expect(readFile(remotePath)).rejects.toMatchObject({ code: 'ENOENT' }) + } + } finally { + await rm(root, { recursive: true, force: true }) + } + }, + 30_000 + ) +}) + +async function openPowerShellChannel(command: string) { + const encoded = command.match(/-EncodedCommand\s+([A-Za-z0-9+/=]+)$/u)?.[1] + if (!encoded) { + throw new Error('PowerShell receiver command is not encoded') + } + const child = spawn( + 'powershell.exe', + ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded], + { stdio: ['pipe', 'ignore', 'pipe'], windowsHide: true } + ) + let diagnostic = '' + child.stderr.setEncoding('utf8') + child.stderr.on('data', (chunk: string) => { + diagnostic = `${diagnostic}${chunk}`.slice(-4_096) + }) + const settled = new Promise((resolve, reject) => { + child.once('error', reject) + child.once('close', (code, signal) => { + if (code === 0) { + resolve() + } else { + reject(new Error(`PowerShell receiver failed (${code ?? signal}): ${diagnostic}`)) + } + }) + }) + return { + write: (chunk: Buffer, callback: Callback) => { + child.stdin.write(chunk, (error) => callback(error ?? undefined)) + }, + end: () => child.stdin.end(), + settled, + requestClose: () => child.kill(), + forceClose: () => child.kill('SIGKILL') + } +} diff --git a/src/main/ssh/ssh-relay-runtime-windows-file-destination.ts b/src/main/ssh/ssh-relay-runtime-windows-file-destination.ts new file mode 100644 index 00000000000..91397e742b6 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-windows-file-destination.ts @@ -0,0 +1,157 @@ +import { assertSafeRemotePathSegment, normalizeWindowsRemotePath } from './ssh-remote-platform' +import { powerShellCommand } from './ssh-remote-powershell' +import { + openSshRelayRuntimeCommandFileDestination, + type SshRelayRuntimeCommandFileChannel +} from './ssh-relay-runtime-command-file-destination' +import type { SshRelayRuntimeSourceDestination } from './ssh-relay-runtime-source-stream' + +export type OpenSshRelayRuntimeWindowsFileDestinationOptions = Readonly<{ + remotePath: string + expectedSize: number + signal: AbortSignal + openChannel: (command: string, signal: AbortSignal) => Promise +}> + +const HEADER_MAGIC = Buffer.from('ORCARLY1', 'ascii') +const FIXED_HEADER_BYTES = 20 +const MAXIMUM_PATH_BYTES = 32 * 1024 +const PAYLOAD_BUFFER_BYTES = 64 * 1024 + +export const SSH_RELAY_RUNTIME_WINDOWS_FILE_DESTINATION_LIMITS = Object.freeze({ + maximumHeaderBytes: FIXED_HEADER_BYTES + MAXIMUM_PATH_BYTES, + maximumPathBytes: MAXIMUM_PATH_BYTES, + payloadBufferBytes: PAYLOAD_BUFFER_BYTES +}) + +const RECEIVER_SCRIPT = ` +$ErrorActionPreference = 'Stop' +function Read-Exact([System.IO.Stream]$Stream, [byte[]]$Buffer, [int]$Count) { + $offset = 0 + while ($offset -lt $Count) { + $read = $Stream.Read($Buffer, $offset, $Count - $offset) + if ($read -eq 0) { throw 'SSH relay runtime Windows receiver early EOF' } + $offset += $read + } +} +$inputStream = [Console]::OpenStandardInput() +$outputStream = $null +$ownsFile = $false +try { + [byte[]]$header = New-Object byte[] ${FIXED_HEADER_BYTES} + Read-Exact $inputStream $header ${FIXED_HEADER_BYTES} + if ([Text.Encoding]::ASCII.GetString($header, 0, 8) -ne 'ORCARLY1') { throw 'bad header' } + [uint32]$pathLength = [BitConverter]::ToUInt32($header, 8) + [int64]$expectedSize = [BitConverter]::ToInt64($header, 12) + if ($pathLength -eq 0 -or $pathLength -gt ${MAXIMUM_PATH_BYTES}) { throw 'bad path length' } + if ($expectedSize -lt 0 -or $expectedSize -gt 9007199254740991) { throw 'bad size' } + [byte[]]$pathBytes = New-Object byte[] ([int]$pathLength) + Read-Exact $inputStream $pathBytes ([int]$pathLength) + $utf8 = New-Object Text.UTF8Encoding($false, $true) + $path = $utf8.GetString($pathBytes) + if (-not [IO.Path]::IsPathRooted($path) -or $path.IndexOf([char]0) -ge 0) { throw 'bad path' } + $outputStream = New-Object System.IO.FileStream($path, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None, ${PAYLOAD_BUFFER_BYTES}, [IO.FileOptions]::SequentialScan) + $ownsFile = $true + [byte[]]$buffer = New-Object byte[] ${PAYLOAD_BUFFER_BYTES} + [int64]$remaining = $expectedSize + while ($remaining -gt 0) { + $wanted = [int][Math]::Min([int64]$buffer.Length, $remaining) + $read = $inputStream.Read($buffer, 0, $wanted) + if ($read -eq 0) { throw 'SSH relay runtime Windows receiver early EOF' } + $outputStream.Write($buffer, 0, $read) + $remaining -= $read + } + if ($inputStream.ReadByte() -ne -1) { throw 'SSH relay runtime Windows receiver extra byte' } + $outputStream.Flush() + $outputStream.Dispose() + $outputStream = $null + $ownsFile = $false +} catch { + try { if ($null -ne $outputStream) { $outputStream.Dispose() } } catch {} + try { if ($ownsFile) { [IO.File]::Delete($path) } } catch {} + throw 'SSH relay runtime Windows receiver failed' +} +`.trim() + +const RECEIVER_COMMAND = powerShellCommand(RECEIVER_SCRIPT) + +function normalizeRemotePath(remotePath: string): string { + if ( + typeof remotePath !== 'string' || + remotePath === '' || + remotePath.includes('\0') || + remotePath.includes('\r') || + remotePath.includes('\n') + ) { + throw new Error('SSH relay runtime Windows file destination path is invalid') + } + const normalized = normalizeWindowsRemotePath(remotePath) + const driveRooted = /^[A-Za-z]:\//u.test(normalized) + const uncRooted = /^\/\/[^/]+\/[^/]+\//u.test(normalized) + if ((!driveRooted && !uncRooted) || normalized.endsWith('/')) { + throw new Error('SSH relay runtime Windows file destination path is invalid') + } + const segments = driveRooted ? normalized.slice(3).split('/') : normalized.slice(2).split('/') + try { + for (const segment of segments) { + assertSafeRemotePathSegment(segment, 'windows') + } + } catch { + throw new Error('SSH relay runtime Windows file destination path is invalid') + } + return normalized +} + +function buildHeader(remotePath: string, expectedSize: number): Buffer { + const pathBytes = Buffer.from(remotePath, 'utf8') + if (pathBytes.length === 0 || pathBytes.length > MAXIMUM_PATH_BYTES) { + throw new Error('SSH relay runtime Windows file destination path is invalid') + } + const header = Buffer.allocUnsafe(FIXED_HEADER_BYTES + pathBytes.length) + HEADER_MAGIC.copy(header, 0) + header.writeUInt32LE(pathBytes.length, 8) + header.writeBigUInt64LE(BigInt(expectedSize), 12) + pathBytes.copy(header, FIXED_HEADER_BYTES) + return header +} + +function validateOptions(options: OpenSshRelayRuntimeWindowsFileDestinationOptions): string { + if ( + !options || + typeof options.openChannel !== 'function' || + !options.signal || + !Number.isSafeInteger(options.expectedSize) || + options.expectedSize < 0 + ) { + throw new Error('SSH relay runtime Windows file destination input is invalid') + } + return normalizeRemotePath(options.remotePath) +} + +export async function openSshRelayRuntimeWindowsFileDestination( + options: OpenSshRelayRuntimeWindowsFileDestinationOptions +): Promise { + const remotePath = validateOptions(options) + const header = buildHeader(remotePath, options.expectedSize) + const destination = await openSshRelayRuntimeCommandFileDestination({ + command: RECEIVER_COMMAND, + fileKind: 'Windows', + signal: options.signal, + openChannel: options.openChannel + }) + try { + // Why: the receiver must authenticate framing before any source buffer can be borrowed. + await destination.write(header) + return destination + } catch (error) { + try { + await destination.abort(error) + } catch (cleanupError) { + throw new AggregateError( + [error, cleanupError], + 'SSH relay runtime Windows file header cleanup failed' + ) + } + throw error + } +} From d24f877d96f07d691508cc686c0791a86d3dd308 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 15:36:03 -0400 Subject: [PATCH 234/267] docs(ssh): record native Windows relay transfer proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 17 +++-- ...github-release-implementation-checklist.md | 73 +++++++++++++++++-- 2 files changed, 76 insertions(+), 14 deletions(-) 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 fa3615303f9..fc6fbf7b87c 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 @@ -10,19 +10,22 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected Windows -system-SSH bounded binary per-file destination is locally green; native Windows CI is next, -2026-07-15, Codex implementation owner.** -The active slice is limited to a fixed PowerShell 5.1/.NET receiver, a bounded binary stdin header, -one exact raw payload, exclusive `FileMode.CreateNew`, 64 KiB reads/writes, early-EOF and extra-byte -rejection, and owned-file cleanup. It must reuse the proven command-channel lifecycle without -changing POSIX behavior. Remote-tree composition, product callers, legacy upload/fallback/default +system-SSH staging-control contract audit, 2026-07-15, Codex implementation owner.** +The next slice is limited to fixed-command, bounded binary PowerShell 5.1 requests for exclusive +staging-root creation, declared-directory creation, and owned-root cleanup. It must preserve a +pre-existing root, bound and strictly decode paths, join cancellation/settlement, and remain +disconnected. File/tree composition, live OpenSSH, product callers, legacy upload/fallback/default behavior, tuple enablement, publication, and SignPath remain out of scope. `E-M6-WINDOWS-SYSTEM-SSH-FILE-LOCAL-RED-001` proves the workflow oracle passes 9/9 and the new purpose suite fails solely because the audited production destination is absent. `E-M6-WINDOWS-SYSTEM-SSH-FILE-LOCAL-001` records 14/15 Windows purpose cases locally with the real PowerShell case honestly skipped, all 25 prior POSIX destination cases, 234 focused cases, 670 broad relay cases, 282/282 release contracts, typecheck, full lint, formatting, diff, protected-resolver, -and no-product-consumer gates. Native Windows x64/arm64 PowerShell 5.1 proof remains required. +and no-product-consumer gates. `E-M6-WINDOWS-SYSTEM-SSH-FILE-CI-001` closes native proof: Windows +x64 and arm64 each pass all 15 cases through real `powershell.exe`; all six primary jobs, both Linux +supplements, Windows x64 floor, PR Checks, Golden E2E, and computer-use pass. The same-SHA rerun +closes an adjacent watcher flake and retains only the declared hosted Windows arm64 build-26200 +versus required-26100 rejection after complete runtime smoke. `E-M6-POSIX-SYSTEM-SSH-TREE-AUDIT-001` requires a bounded no-input control-command owner rather than the legacy unbounded-output command helper, exclusive `0700` root creation, parent-first declared directories, exact per-file source-stream composition, owned-root cleanup, and a worst-case 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 21a7db30459..0710dc08c23 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected Windows system-SSH bounded binary per-file destination**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **WINDOWS SYSTEM-SSH PER-FILE DESTINATION LOCALLY GREEN / NATIVE WINDOWS CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-WINDOWS-SYSTEM-SSH-FILE-LOCAL-001` proves the disconnected fixed-command binary receiver contract, bounded header ownership, generic lifecycle extraction, every prior POSIX regression, broad relay/release/static gates, and production isolation locally. The real PowerShell 5.1 case is explicitly skipped on macOS and earns no native claim; commit and push the exact package, then require native Windows x64/arm64 proof before closing it. Do not compose a remote tree, connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work in this package. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected Windows system-SSH staging-control contract audit**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **WINDOWS PER-FILE NATIVE PROOF CLOSED / STAGING CONTROL AUDIT ACTIVE — 2026-07-15, Codex implementation owner** — `E-M6-WINDOWS-SYSTEM-SSH-FILE-CI-001` proves the real PowerShell receiver on native Windows x64/arm64 and closes the adjacent watcher flake with a same-SHA rerun; only the declared hosted Windows arm64 build-26200 versus required-26100 floor rejection remains. Audit the next disconnected fixed-command staging-root/directory/cleanup contract before writing RED tests. Do not compose file streaming into a tree, connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work in this package. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -18227,6 +18227,65 @@ src/main/ssh/ssh-remote-node-resolution.ts src/main/ssh/ssh-remote-node-resoluti classification/cache, remote verification/install/publish/launch, product/Beta/fallback/default wiring, tuple enablement, publication, Apple signing, and SignPath remain absent. +### E-M6-WINDOWS-SYSTEM-SSH-FILE-CI-001 — exact-head native PowerShell receiver proof + +- Date/owner/head: 2026-07-15, Codex implementation owner; exact head + `20f8aebb8b7b3b2e2731df36afb97c7b4a297fef` on draft PR #8741. Artifact workflow run + `29527086460`; adjacent PR Checks `29527086453`, Golden E2E `29527086508`, and computer-use + `29527086498` all complete successfully on that head. +- Native Windows x64 job `87718089673`: requested `windows-2022`, resolved `win22` image + `20260714.244.1`, X64, Node 24.18.0. The Windows destination suite passes 15/15 in 3.214 seconds; + its real `powershell.exe` binary-fidelity/collision/exact-size case passes in 3.089 seconds. The + primary contract set passes 98 files and 828 tests with 18 declared platform skips in 39.14 + seconds. +- Native Windows arm64 job `87718089682`: requested `windows-11-arm`, resolved `win11-arm64` image + `20260714.109.1`, ARM64, Node 24.18.0. The Windows destination suite passes 15/15 in 3.674 seconds; + its real `powershell.exe` case passes in 3.532 seconds. The primary contract set passes 98 files + and 828 tests with 18 declared platform skips in 30.61 seconds. +- Other exact-head primary jobs pass: Darwin x64 `87718089684`, Darwin arm64 `87718089798`, Linux + x64 `87718089690`, and Linux arm64 `87718089769`. Linux supplements `87719287527` and + `87719287531` and Windows x64 floor `87720166503` pass. Rerunning failed jobs on the same SHA also + reran dependency jobs; all six primary jobs and those three adjacent artifact jobs pass again. +- Windows arm64 floor attempt 1 job `87720166584` fails during an adjacent watcher smoke with + `watcher create exceeded 15000 ms` after observing `update`; no timeout or watcher behavior was + changed. Same-SHA attempt 2 job `87721893505` passes the complete 60-entry/42-file/85,213,511-byte + runtime, content `sha256:02edf462be83c2864a89546d5344d348f9e07ce10964660342608a8c614e47db`, + Node 24.18.0/ABI 137, PTY exit 23/resize, and watcher create/update/delete smoke. It then rejects + only hosted Windows build 26200 against the required exact build-26100 floor (`platform=true`, + `architecture=true`, `osBuild=false`). This is the declared hosted-runner gap, not a package + regression. +- Disposition/residual boundary: the disconnected per-file receiver package is complete. This proves + a real local Windows `powershell.exe` process on native x64/arm64 clients, not OpenSSH transport to + a Windows remote, the exact Windows PowerShell/.NET/OpenSSH minimum combination, full-size transfer, + long paths, endpoint-protection locks, remote tree/staging ownership, capability classification, + bundled-Node verification/install/publish/launch, product/Beta/fallback/default behavior, tuple + enablement, publication, Apple signing, or SignPath. Legacy remains the only production/default + path and every tuple stays disabled. + +### E-M6-WINDOWS-SYSTEM-SSH-CONTROL-AUDIT-001 — bounded staging control contract + +- Date/owner/base: 2026-07-15, Codex implementation owner; audit-only package atop exact base + `20f8aebb8b7b3b2e2731df36afb97c7b4a297fef`. +- Scope: one disconnected fixed Windows PowerShell 5.1/.NET command accepting a bounded binary stdin + request. Supported operations are exclusive staging-root creation, one declared child-directory + creation, and owned-root cleanup. Root and child paths are strict UTF-8 length-prefixed fields; + no path is embedded in PowerShell source, a command argument, JSON, base64, or unbounded input. +- Ownership/safety: rooted drive and UNC paths must pass client validation before opening a channel; + manifest segments retain the existing reserved-name/case-collision rules. Root creation must fail + if the target exists and must never authorize cleanup of that pre-existing tree. A later tree owner + may request cleanup only after positive exclusive-root creation. Directory operations are + parent-first and bounded; remote failures, cancellation, and cleanup settlement are all awaited. +- Required RED/green proof: fixed command/request framing, strict field bounds/UTF-8, hostile rooted + paths rejected before channel open, exclusive collision preservation, Unicode directory creation, + owned-root removal, nonzero remote settlement, retained-write cancellation, and native Windows + `powershell.exe` execution. Pin the purpose suite exactly once in both native artifact workflow + command families and update the workflow oracle before implementation. +- Explicit residual boundary: no file destination composition, tree streaming/concurrency, live + Windows OpenSSH, full-size/runtime measurement, long-path qualification, endpoint-protection fault, + primitive detector/cache, remote bundled-Node verification/install/publish/launch, product/Beta/ + settings/fallback/tuple/publication/default behavior, or SignPath is included. Legacy remains the + sole production/default path and every tuple stays disabled. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -18285,11 +18344,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the exact `E-M6-WINDOWS-SYSTEM-SSH-FILE-LOCAL-001` package, then require the real -PowerShell 5.1 process oracle on native Windows x64/arm64 plus all-six native and adjacent repository -regression cells. Do not check the broad Milestone 6 Windows system-SSH item yet. Do not add a remote- -tree owner, Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, -release publication, or default behavior. +Write and run the purpose-named RED suite and native-workflow oracle for +`E-M6-WINDOWS-SYSTEM-SSH-CONTROL-AUDIT-001`, proving failure solely because the audited production +module is absent. Do not check the broad Milestone 6 Windows system-SSH item yet. Do not compose file +streaming into a remote-tree owner or add an Electron/startup/product importer, per-target mode +wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. From 68737239b98d88c5b60995935fcadadb60bf5b43 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 15:50:36 -0400 Subject: [PATCH 235/267] feat(ssh): add bounded Windows relay staging control Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 9 +- ...github-release-implementation-checklist.md | 77 ++++- ...ay-runtime-windows-staging-control.test.ts | 285 ++++++++++++++++++ ...h-relay-runtime-windows-staging-control.ts | 213 +++++++++++++ 6 files changed, 581 insertions(+), 6 deletions(-) create mode 100644 src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts create mode 100644 src/main/ssh/ssh-relay-runtime-windows-staging-control.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 060ed032bc6..d90f0de0a07 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -289,6 +289,7 @@ jobs: src/main/ssh/ssh-relay-runtime-posix-control-command.test.ts \ src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts \ src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts \ + src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts \ src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts \ src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts \ @@ -1029,6 +1030,7 @@ jobs: src/main/ssh/ssh-relay-runtime-posix-control-command.test.ts ` src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts ` src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts ` + src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts ` src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts ` src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index a6fe6d77643..a5ee8115b4e 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -203,6 +203,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-runtime-posix-control-command.test.ts', 'ssh-relay-runtime-posix-file-destination.test.ts', 'ssh-relay-runtime-windows-file-destination.test.ts', + 'ssh-relay-runtime-windows-staging-control.test.ts', 'ssh-relay-runtime-posix-tree-transfer.test.ts', 'ssh-relay-runtime-system-ssh-file-channel.test.ts', 'ssh-relay-runtime-sftp-file-destination.test.ts', 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 fc6fbf7b87c..2f9aa143edd 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 @@ -10,12 +10,19 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected Windows -system-SSH staging-control contract audit, 2026-07-15, Codex implementation owner.** +system-SSH staging control is locally green; native Windows CI is next, 2026-07-15, Codex +implementation owner.** The next slice is limited to fixed-command, bounded binary PowerShell 5.1 requests for exclusive staging-root creation, declared-directory creation, and owned-root cleanup. It must preserve a pre-existing root, bound and strictly decode paths, join cancellation/settlement, and remain disconnected. File/tree composition, live OpenSSH, product callers, legacy upload/fallback/default behavior, tuple enablement, publication, and SignPath remain out of scope. +`E-M6-WINDOWS-SYSTEM-SSH-CONTROL-LOCAL-RED-001` proves the native workflow oracle passes 9/9 and +the purpose suite fails solely because the audited production staging-control module is absent. +`E-M6-WINDOWS-SYSTEM-SSH-CONTROL-LOCAL-001` records 16/17 purpose cases locally with the real +PowerShell case honestly skipped, 207 focused cases, 686 broad relay cases, 282/282 release +contracts, typecheck, full lint, formatting, diff, protected-resolver, and no-product-consumer +gates. Native Windows x64/arm64 `powershell.exe` proof remains required. `E-M6-WINDOWS-SYSTEM-SSH-FILE-LOCAL-RED-001` proves the workflow oracle passes 9/9 and the new purpose suite fails solely because the audited production destination is absent. `E-M6-WINDOWS-SYSTEM-SSH-FILE-LOCAL-001` records 14/15 Windows purpose cases locally with the real 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 0710dc08c23..ed966eee804 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected Windows system-SSH staging-control contract audit**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **WINDOWS PER-FILE NATIVE PROOF CLOSED / STAGING CONTROL AUDIT ACTIVE — 2026-07-15, Codex implementation owner** — `E-M6-WINDOWS-SYSTEM-SSH-FILE-CI-001` proves the real PowerShell receiver on native Windows x64/arm64 and closes the adjacent watcher flake with a same-SHA rerun; only the declared hosted Windows arm64 build-26200 versus required-26100 floor rejection remains. Audit the next disconnected fixed-command staging-root/directory/cleanup contract before writing RED tests. Do not compose file streaming into a tree, connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work in this package. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected Windows system-SSH staging-control native proof**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **WINDOWS STAGING CONTROL LOCALLY GREEN / NATIVE WINDOWS CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-WINDOWS-SYSTEM-SSH-CONTROL-LOCAL-001` proves the fixed-command bounded request, strict rooted-path/descendant validation, exclusive root/directory creation, parent-first enforcement, owned-root removal primitive, 30-second ceiling, joined command-channel lifecycle, prior transfer regressions, and production isolation locally. The real PowerShell 5.1 case is skipped on macOS and earns no native claim; commit and push the exact package, then require native Windows x64/arm64 proof. Do not compose file streaming into a tree, connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work in this package. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -18286,6 +18286,73 @@ src/main/ssh/ssh-remote-node-resolution.ts src/main/ssh/ssh-remote-node-resoluti settings/fallback/tuple/publication/default behavior, or SignPath is included. Legacy remains the sole production/default path and every tuple stays disabled. +### E-M6-WINDOWS-SYSTEM-SSH-CONTROL-LOCAL-RED-001 — audited staging control is absent + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; purpose test, native-workflow + wiring, and checklist-only diff atop exact base + `d24f877d96f07d691508cc686c0791a86d3dd308` on Apple arm64, macOS 26.2 build 25C56, Node + 26.0.0, pnpm 10.24.0. +- Command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs`. +- Result: expected failure in 1.39 seconds. The workflow oracle passes 9/9 and proves the new purpose + suite occurs exactly once in both native artifact command families. The purpose suite fails before + collection solely because `./ssh-relay-runtime-windows-staging-control` does not exist; zero + production modules are added or changed at this boundary. +- Residual boundary: no PowerShell staging behavior or Windows runner claim follows from a missing + module. Purpose, native Windows, prior Windows/POSIX regression, broad relay/release, static, + isolation, and no-product-consumer gates remain required. + +### E-M6-WINDOWS-SYSTEM-SSH-CONTROL-LOCAL-001 — bounded staging control is locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted implementation, + purpose-test, native-workflow, and checklist package atop exact base + `d24f877d96f07d691508cc686c0791a86d3dd308` on Apple arm64, macOS 26.2 build 25C56, Node + 26.0.0, pnpm 10.24.0. The local Node version is newer than the repository's Node 24 contract and + the real Windows PowerShell case is skipped off Windows, so exact-head native proof remains + mandatory. +- Implementation: add one disconnected fixed encoded PowerShell receiver with an at-most + 65,556-byte request, two independently bounded 32-KiB strict-UTF-8 path fields, operation byte and + zeroed reserved bytes. Client validation rejects non-rooted, reserved, traversal, non-descendant, + and oversized paths before channel open. The receiver rejects malformed framing/extra bytes, + requires each direct parent to exist, uses fail-on-existing `New-Item` for root/directories, and + removes only a root whose ownership is tracked by a later tree caller. Each operation has a + 30-second ceiling and reuses the proven 250 ms graceful/2-second forced channel settlement. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1` with the staging-control, Windows/POSIX file/control/tree, system-channel, + source-tree/scan/stream, system-fallback, connection, and workflow-oracle suites. Result: 11 files, + 207 passed and three declared platform skips out of 210; 8.85 seconds real, 184,500,224-byte + maximum RSS, 96,818,624-byte peak footprint, zero swaps and block I/O. The staging-control suite + contributes 16 passed plus one macOS-skipped real-PowerShell case; the workflow oracle passes 9/9. +- Broad relay command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts`. Result: 52 files passed, five files skipped; 686 + passed and nine declared skips out of 695; 113.77 seconds real, 257,605,632-byte maximum RSS, + 97,572,312-byte peak footprint, zero swaps and block I/O. This and the release/lint commands ran + concurrently, so their wall times are recorded but are not standalone performance baselines. +- Release-contract command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs`. Result: 50 files + and 282/282 cases pass; 81.81 seconds real, 189,317,120-byte maximum RSS, 96,146,808-byte peak + footprint, zero swaps and block I/O. Two prior quoted-wildcard invocations found no literal file; + they were command errors and were immediately replaced by these exact checklist commands. +- Static evidence: `pnpm typecheck` passes. `/usr/bin/time -l pnpm lint` passes in 78.78 seconds real + with 2,106,277,888-byte maximum RSS and 96,081,152-byte peak footprint; its 26 warnings are + pre-existing outside this diff. Full lint proves switch exhaustiveness, 41 reliability gates, the + 355-entry max-lines ratchet with no new bypass, bundled guides, and localization checks. Targeted + `oxlint` and `oxfmt --check` pass. +- Isolation evidence: `git diff --check` passes; `git diff --exit-code +d24f877d96f07d691508cc686c0791a86d3dd308 -- src/main/ssh/ssh-remote-node-resolution.ts +src/main/ssh/ssh-remote-node-resolution.test.ts` is empty. A non-test reference search finds the + staging-control export only in its owning production module. The intended diff is limited to the + disconnected staging-control module/purpose test, native workflow/oracle wiring, and both + checklist documents. No existing file receiver, POSIX/SFTP transfer, legacy uploader, product + importer, settings, Beta, fallback, tuple, publication, default-path, or signing behavior changes. +- Residual boundary: local execution does not prove Windows PowerShell 5.1 parsing, exclusive + directory semantics, or cleanup on native Windows. Exact-head native Windows x64/arm64 purpose + proof and all-six adjacent native regression cells remain required. File/tree composition, live + Windows OpenSSH, full-size runtime transfer, long paths, endpoint-protection faults, capability + classification/cache, bundled-Node verification/install/publish/launch, product/Beta/fallback/ + default behavior, tuple enablement, publication, Apple signing, and SignPath remain absent. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -18344,9 +18411,9 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Write and run the purpose-named RED suite and native-workflow oracle for -`E-M6-WINDOWS-SYSTEM-SSH-CONTROL-AUDIT-001`, proving failure solely because the audited production -module is absent. Do not check the broad Milestone 6 Windows system-SSH item yet. Do not compose file +Commit and push the exact `E-M6-WINDOWS-SYSTEM-SSH-CONTROL-LOCAL-001` package, then require the real +PowerShell 5.1 process oracle on native Windows x64/arm64 plus all-six native and adjacent repository +regression cells. Do not check the broad Milestone 6 Windows system-SSH item yet. Do not compose file streaming into a remote-tree owner or add an Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets diff --git a/src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts b/src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts new file mode 100644 index 00000000000..1338f22b712 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts @@ -0,0 +1,285 @@ +import { spawn } from 'node:child_process' +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' + +import type { SshRelayRuntimeCommandFileChannel } from './ssh-relay-runtime-command-file-destination' +import { + runSshRelayRuntimeWindowsStagingControl, + SSH_RELAY_RUNTIME_WINDOWS_STAGING_CONTROL_LIMITS +} from './ssh-relay-runtime-windows-staging-control' + +type Callback = (error?: Error) => void + +function createChannel() { + let resolveSettled: () => void = () => {} + let rejectSettled: (error: Error) => void = () => {} + const settled = new Promise((resolve, reject) => { + resolveSettled = resolve + rejectSettled = reject + }) + const channel = { + write: vi.fn((_chunk: Buffer, callback: Callback) => callback()), + end: vi.fn(), + settled, + requestClose: vi.fn(), + forceClose: vi.fn() + } + return { channel, resolve: resolveSettled, reject: rejectSettled } +} + +function decodeRequest(frame: Buffer) { + const rootLength = frame.readUInt32LE(12) + const pathLength = frame.readUInt32LE(16) + const rootStart = 20 + const pathStart = rootStart + rootLength + return { + magic: frame.subarray(0, 8).toString('ascii'), + operation: frame.readUInt8(8), + reserved: frame.subarray(9, 12), + remoteRoot: frame.subarray(rootStart, pathStart).toString('utf8'), + remotePath: frame.subarray(pathStart, pathStart + pathLength).toString('utf8') + } +} + +function decodePowerShellCommand(command: string): string { + const encoded = command.match(/-EncodedCommand\s+([A-Za-z0-9+/=]+)$/u)?.[1] + if (!encoded) { + throw new Error('PowerShell staging control command is not encoded') + } + return Buffer.from(encoded, 'base64').toString('utf16le') +} + +function runControl( + channel: SshRelayRuntimeCommandFileChannel, + overrides: Partial[0]> = {} +) { + return runSshRelayRuntimeWindowsStagingControl({ + operation: 'create-directory', + remoteRoot: 'C:/Users/测试/.orca-remote/stage', + remotePath: 'C:/Users/测试/.orca-remote/stage/bin/native', + signal: new AbortController().signal, + openChannel: vi.fn(async () => channel), + ...overrides + }) +} + +afterEach(() => { + vi.useRealTimers() + vi.restoreAllMocks() +}) + +describe('SSH relay runtime Windows system-SSH staging control', () => { + it.each([ + ['create-root', 1, ''], + ['create-directory', 2, 'C:/Users/测试/.orca-remote/stage/bin/native'], + ['remove-root', 3, ''] + ] as const)( + 'sends one fixed bounded binary %s request', + async (operation, opcode, remotePath) => { + const fixture = createChannel() + const openChannel = vi.fn(async (_command: string, _signal: AbortSignal) => fixture.channel) + const running = runControl(fixture.channel, { + operation, + remotePath: remotePath || undefined, + openChannel + }) + + await vi.waitFor(() => expect(fixture.channel.end).toHaveBeenCalledOnce()) + expect(openChannel).toHaveBeenCalledOnce() + const command = openChannel.mock.calls[0]?.[0] ?? '' + expect(command).toMatch(/^powershell\.exe .* -EncodedCommand [A-Za-z0-9+/=]+$/u) + expect(command).not.toContain('测试') + const script = decodePowerShellCommand(command) + expect(script).toContain('[Console]::OpenStandardInput()') + expect(script).toContain('New-Item -ItemType Directory -Path') + expect(script).toContain('[IO.Path]::GetDirectoryName') + expect(script).toContain('[IO.Directory]::Delete') + expect(script).toContain('$inputStream.ReadByte() -ne -1') + for (const forbidden of ['ReadToEnd', 'FromBase64String', 'ConvertFrom-Json']) { + expect(script).not.toContain(forbidden) + } + + const frame = fixture.channel.write.mock.calls[0]?.[0] + expect(Buffer.isBuffer(frame)).toBe(true) + expect(frame.length).toBeLessThanOrEqual( + SSH_RELAY_RUNTIME_WINDOWS_STAGING_CONTROL_LIMITS.maximumRequestBytes + ) + expect(decodeRequest(frame)).toEqual({ + magic: 'ORCACTL1', + operation: opcode, + reserved: Buffer.alloc(3), + remoteRoot: 'C:/Users/测试/.orca-remote/stage', + remotePath + }) + fixture.resolve() + await expect(running).resolves.toBeUndefined() + } + ) + + it.each([ + ['create-root', 'relative/stage', undefined], + ['create-root', 'C:/', undefined], + ['create-root', 'C:/owned/../stage', undefined], + ['create-root', 'C:/owned/NUL', undefined], + [ + 'create-root', + `C:/${'a'.repeat(SSH_RELAY_RUNTIME_WINDOWS_STAGING_CONTROL_LIMITS.maximumPathBytes)}`, + undefined + ], + ['create-root', 'C:/owned/stage', 'C:/owned/stage/extra'], + ['create-directory', 'C:/owned/stage', undefined], + ['create-directory', 'C:/owned/stage', 'C:/outside'], + ['create-directory', 'C:/owned/stage', 'C:/owned/stage'], + ['create-directory', 'C:/owned/stage', 'C:/owned/stage/report.txt:stream'] + ] as const)( + 'rejects hostile %s root %j path %j before channel open', + async (operation, remoteRoot, remotePath) => { + const fixture = createChannel() + const openChannel = vi.fn(async () => fixture.channel) + await expect( + runControl(fixture.channel, { operation, remoteRoot, remotePath, openChannel }) + ).rejects.toThrow(/invalid/i) + expect(openChannel).not.toHaveBeenCalled() + } + ) + + it('settles retained-request cancellation before rejecting', async () => { + const fixture = createChannel() + const controller = new AbortController() + const reason = new Error('cancelled retained Windows control request') + fixture.channel.write.mockImplementationOnce(() => {}) + fixture.channel.requestClose.mockImplementation(() => fixture.resolve()) + const running = runControl(fixture.channel, { signal: controller.signal }) + + await vi.waitFor(() => expect(fixture.channel.write).toHaveBeenCalledOnce()) + controller.abort(reason) + await expect(running).rejects.toBe(reason) + expect(fixture.channel.requestClose).toHaveBeenCalledOnce() + expect(fixture.channel.forceClose).not.toHaveBeenCalled() + }) + + it('propagates remote failure only after request EOF and settlement', async () => { + const fixture = createChannel() + const running = runControl(fixture.channel) + await vi.waitFor(() => expect(fixture.channel.end).toHaveBeenCalledOnce()) + const error = new Error('PowerShell staging control failed') + fixture.reject(error) + await expect(running).rejects.toBe(error) + }) + + it('turns the command ceiling into bounded cancellation and settlement', async () => { + vi.useFakeTimers() + const fixture = createChannel() + fixture.channel.requestClose.mockImplementation(() => fixture.resolve()) + const running = runControl(fixture.channel) + const rejection = expect(running).rejects.toThrow(/timed out/i) + + await vi.advanceTimersByTimeAsync( + SSH_RELAY_RUNTIME_WINDOWS_STAGING_CONTROL_LIMITS.commandTimeoutMs + ) + await rejection + expect(fixture.channel.requestClose).toHaveBeenCalledOnce() + expect(fixture.channel.forceClose).not.toHaveBeenCalled() + }) + + it.skipIf(process.platform !== 'win32')( + 'proves exclusive root ownership, Unicode directories, and owned cleanup in PowerShell 5.1', + async () => { + const parent = await mkdtemp(path.join(tmpdir(), 'orca-relay-win-control-')) + const remoteRoot = path.join(parent, '测试 stage') + const remoteParent = path.join(remoteRoot, 'bin') + const remotePath = path.join(remoteParent, 'native') + try { + await runSshRelayRuntimeWindowsStagingControl({ + operation: 'create-root', + remoteRoot, + signal: new AbortController().signal, + openChannel: openPowerShellChannel + }) + const marker = path.join(remoteRoot, 'original.txt') + await writeFile(marker, 'preserve') + await expect( + runSshRelayRuntimeWindowsStagingControl({ + operation: 'create-root', + remoteRoot, + signal: new AbortController().signal, + openChannel: openPowerShellChannel + }) + ).rejects.toThrow(/PowerShell staging control failed/i) + expect(await readFile(marker, 'utf8')).toBe('preserve') + + await expect( + runSshRelayRuntimeWindowsStagingControl({ + operation: 'create-directory', + remoteRoot, + remotePath, + signal: new AbortController().signal, + openChannel: openPowerShellChannel + }) + ).rejects.toThrow(/PowerShell staging control failed/i) + await runSshRelayRuntimeWindowsStagingControl({ + operation: 'create-directory', + remoteRoot, + remotePath: remoteParent, + signal: new AbortController().signal, + openChannel: openPowerShellChannel + }) + await runSshRelayRuntimeWindowsStagingControl({ + operation: 'create-directory', + remoteRoot, + remotePath, + signal: new AbortController().signal, + openChannel: openPowerShellChannel + }) + await runSshRelayRuntimeWindowsStagingControl({ + operation: 'remove-root', + remoteRoot, + signal: new AbortController().signal, + openChannel: openPowerShellChannel + }) + await expect(readFile(marker)).rejects.toMatchObject({ code: 'ENOENT' }) + } finally { + await rm(parent, { recursive: true, force: true }) + } + }, + 30_000 + ) +}) + +async function openPowerShellChannel(command: string): Promise { + const encoded = command.match(/-EncodedCommand\s+([A-Za-z0-9+/=]+)$/u)?.[1] + if (!encoded) { + throw new Error('PowerShell staging control command is not encoded') + } + const child = spawn( + 'powershell.exe', + ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded], + { stdio: ['pipe', 'ignore', 'pipe'], windowsHide: true } + ) + let diagnostic = '' + child.stderr.setEncoding('utf8') + child.stderr.on('data', (chunk: string) => { + diagnostic = `${diagnostic}${chunk}`.slice(-4_096) + }) + const settled = new Promise((resolve, reject) => { + child.once('error', reject) + child.once('close', (code, signal) => { + if (code === 0) { + resolve() + } else { + reject(new Error(`PowerShell staging control failed (${code ?? signal}): ${diagnostic}`)) + } + }) + }) + return { + write: (chunk: Buffer, callback: Callback) => { + child.stdin.write(chunk, (error) => callback(error ?? undefined)) + }, + end: () => child.stdin.end(), + settled, + requestClose: () => child.kill(), + forceClose: () => child.kill('SIGKILL') + } +} diff --git a/src/main/ssh/ssh-relay-runtime-windows-staging-control.ts b/src/main/ssh/ssh-relay-runtime-windows-staging-control.ts new file mode 100644 index 00000000000..0de0db3c57c --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-windows-staging-control.ts @@ -0,0 +1,213 @@ +import { assertSafeRemotePathSegment, normalizeWindowsRemotePath } from './ssh-remote-platform' +import { powerShellCommand } from './ssh-remote-powershell' +import { + openSshRelayRuntimeCommandFileDestination, + type SshRelayRuntimeCommandFileChannel +} from './ssh-relay-runtime-command-file-destination' + +export type SshRelayRuntimeWindowsStagingOperation = + | 'create-root' + | 'create-directory' + | 'remove-root' + +export type RunSshRelayRuntimeWindowsStagingControlOptions = Readonly<{ + operation: SshRelayRuntimeWindowsStagingOperation + remoteRoot: string + remotePath?: string + signal: AbortSignal + openChannel: (command: string, signal: AbortSignal) => Promise +}> + +const REQUEST_MAGIC = Buffer.from('ORCACTL1', 'ascii') +const FIXED_REQUEST_BYTES = 20 +const MAXIMUM_PATH_BYTES = 32 * 1024 +const COMMAND_TIMEOUT_MS = 30_000 + +const OPERATION_CODES: Record = { + 'create-root': 1, + 'create-directory': 2, + 'remove-root': 3 +} + +export const SSH_RELAY_RUNTIME_WINDOWS_STAGING_CONTROL_LIMITS = Object.freeze({ + commandTimeoutMs: COMMAND_TIMEOUT_MS, + maximumPathBytes: MAXIMUM_PATH_BYTES, + maximumRequestBytes: FIXED_REQUEST_BYTES + MAXIMUM_PATH_BYTES * 2 +}) + +const RECEIVER_SCRIPT = ` +$ErrorActionPreference = 'Stop' +function Read-Exact([System.IO.Stream]$Stream, [byte[]]$Buffer, [int]$Count) { + $offset = 0 + while ($offset -lt $Count) { + $read = $Stream.Read($Buffer, $offset, $Count - $offset) + if ($read -eq 0) { throw 'SSH relay runtime Windows staging control early EOF' } + $offset += $read + } +} +$inputStream = [Console]::OpenStandardInput() +try { + [byte[]]$header = New-Object byte[] ${FIXED_REQUEST_BYTES} + Read-Exact $inputStream $header ${FIXED_REQUEST_BYTES} + if ([Text.Encoding]::ASCII.GetString($header, 0, 8) -ne 'ORCACTL1') { throw 'bad header' } + [byte]$operation = $header[8] + if ($header[9] -ne 0 -or $header[10] -ne 0 -or $header[11] -ne 0) { throw 'bad reserved bytes' } + [uint32]$rootLength = [BitConverter]::ToUInt32($header, 12) + [uint32]$pathLength = [BitConverter]::ToUInt32($header, 16) + if ($rootLength -eq 0 -or $rootLength -gt ${MAXIMUM_PATH_BYTES}) { throw 'bad root length' } + if ($pathLength -gt ${MAXIMUM_PATH_BYTES}) { throw 'bad path length' } + if (($operation -eq 2 -and $pathLength -eq 0) -or ($operation -ne 2 -and $pathLength -ne 0)) { throw 'bad operation path' } + if ($operation -lt 1 -or $operation -gt 3) { throw 'bad operation' } + [byte[]]$rootBytes = New-Object byte[] ([int]$rootLength) + [byte[]]$pathBytes = New-Object byte[] ([int]$pathLength) + Read-Exact $inputStream $rootBytes ([int]$rootLength) + Read-Exact $inputStream $pathBytes ([int]$pathLength) + if ($inputStream.ReadByte() -ne -1) { throw 'extra byte' } + $utf8 = New-Object Text.UTF8Encoding($false, $true) + $root = $utf8.GetString($rootBytes) + $path = if ($pathLength -eq 0) { '' } else { $utf8.GetString($pathBytes) } + if (-not [IO.Path]::IsPathRooted($root) -or $root.IndexOf([char]0) -ge 0) { throw 'bad root' } + if ($operation -eq 2) { + if (-not [IO.Path]::IsPathRooted($path) -or $path.IndexOf([char]0) -ge 0 -or -not $path.StartsWith($root + '/', [StringComparison]::Ordinal)) { throw 'bad child path' } + } + switch ($operation) { + 1 { + $parent = [IO.Path]::GetDirectoryName($root) + if ([string]::IsNullOrEmpty($parent) -or -not [IO.Directory]::Exists($parent)) { throw 'missing root parent' } + New-Item -ItemType Directory -Path $root -ErrorAction Stop | Out-Null + } + 2 { + $parent = [IO.Path]::GetDirectoryName($path) + if ([string]::IsNullOrEmpty($parent) -or -not [IO.Directory]::Exists($parent)) { throw 'missing directory parent' } + New-Item -ItemType Directory -Path $path -ErrorAction Stop | Out-Null + } + 3 { + if ([IO.File]::Exists($root)) { throw 'staging root became a file' } + if ([IO.Directory]::Exists($root)) { [IO.Directory]::Delete($root, $true) } + } + } +} catch { + throw 'SSH relay runtime Windows staging control failed' +} +`.trim() + +const RECEIVER_COMMAND = powerShellCommand(RECEIVER_SCRIPT) + +function normalizeAbsolutePath(value: string): string { + if ( + typeof value !== 'string' || + value === '' || + value.includes('\0') || + value.includes('\r') || + value.includes('\n') + ) { + throw new Error('SSH relay runtime Windows staging control path is invalid') + } + const normalized = normalizeWindowsRemotePath(value) + const driveRooted = /^[A-Za-z]:\//u.test(normalized) + const uncRooted = /^\/\/[^/]+\/[^/]+\//u.test(normalized) + if ((!driveRooted && !uncRooted) || normalized.endsWith('/')) { + throw new Error('SSH relay runtime Windows staging control path is invalid') + } + const segments = driveRooted ? normalized.slice(3).split('/') : normalized.slice(2).split('/') + try { + for (const segment of segments) { + assertSafeRemotePathSegment(segment, 'windows') + } + } catch { + throw new Error('SSH relay runtime Windows staging control path is invalid') + } + if (Buffer.byteLength(normalized, 'utf8') > MAXIMUM_PATH_BYTES) { + throw new Error('SSH relay runtime Windows staging control path is invalid') + } + return normalized +} + +function validateOptions(options: RunSshRelayRuntimeWindowsStagingControlOptions): { + operation: SshRelayRuntimeWindowsStagingOperation + remoteRoot: string + remotePath: string +} { + if ( + !options || + typeof options.operation !== 'string' || + !Object.hasOwn(OPERATION_CODES, options.operation) || + !options.signal || + typeof options.openChannel !== 'function' + ) { + throw new Error('SSH relay runtime Windows staging control input is invalid') + } + const remoteRoot = normalizeAbsolutePath(options.remoteRoot) + if (options.operation !== 'create-directory') { + if (options.remotePath !== undefined) { + throw new Error('SSH relay runtime Windows staging control input is invalid') + } + return { operation: options.operation, remoteRoot, remotePath: '' } + } + const remotePath = normalizeAbsolutePath(options.remotePath as string) + // Why: control requests may create only a strict descendant of the root owned by the tree caller. + if (!remotePath.startsWith(`${remoteRoot}/`)) { + throw new Error('SSH relay runtime Windows staging control path is invalid') + } + return { operation: options.operation, remoteRoot, remotePath } +} + +function buildRequest( + operation: SshRelayRuntimeWindowsStagingOperation, + remoteRoot: string, + remotePath: string +): Buffer { + const rootBytes = Buffer.from(remoteRoot, 'utf8') + const pathBytes = Buffer.from(remotePath, 'utf8') + const request = Buffer.alloc(FIXED_REQUEST_BYTES + rootBytes.length + pathBytes.length) + REQUEST_MAGIC.copy(request, 0) + request.writeUInt8(OPERATION_CODES[operation], 8) + request.writeUInt32LE(rootBytes.length, 12) + request.writeUInt32LE(pathBytes.length, 16) + rootBytes.copy(request, FIXED_REQUEST_BYTES) + pathBytes.copy(request, FIXED_REQUEST_BYTES + rootBytes.length) + return request +} + +export async function runSshRelayRuntimeWindowsStagingControl( + options: RunSshRelayRuntimeWindowsStagingControlOptions +): Promise { + const { operation, remoteRoot, remotePath } = validateOptions(options) + const request = buildRequest(operation, remoteRoot, remotePath) + const commandController = new AbortController() + const onCallerAbort = (): void => commandController.abort(options.signal.reason) + options.signal.addEventListener('abort', onCallerAbort, { once: true }) + if (options.signal.aborted) { + onCallerAbort() + } + const timeout = setTimeout( + () => commandController.abort(new Error('SSH relay runtime Windows staging control timed out')), + COMMAND_TIMEOUT_MS + ) + try { + const destination = await openSshRelayRuntimeCommandFileDestination({ + command: RECEIVER_COMMAND, + fileKind: 'Windows', + signal: commandController.signal, + openChannel: options.openChannel + }) + try { + await destination.write(request) + await destination.close() + } catch (error) { + try { + // Why: a retained request buffer must settle before this bounded control operation returns. + await destination.abort(error) + } catch (cleanupError) { + throw new AggregateError( + [error, cleanupError], + 'SSH relay runtime Windows staging control cleanup failed' + ) + } + throw error + } + } finally { + clearTimeout(timeout) + options.signal.removeEventListener('abort', onCallerAbort) + } +} From c39e20cc6ccdfc66dbac25bd17d86fd4e53fdacd Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 16:15:27 -0400 Subject: [PATCH 236/267] docs(ssh): record native Windows staging proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 18 +++-- ...github-release-implementation-checklist.md | 72 +++++++++++++++++-- 2 files changed, 76 insertions(+), 14 deletions(-) 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 2f9aa143edd..c512e0c4d29 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 @@ -10,19 +10,23 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected Windows -system-SSH staging control is locally green; native Windows CI is next, 2026-07-15, Codex -implementation owner.** -The next slice is limited to fixed-command, bounded binary PowerShell 5.1 requests for exclusive -staging-root creation, declared-directory creation, and owned-root cleanup. It must preserve a -pre-existing root, bound and strictly decode paths, join cancellation/settlement, and remain -disconnected. File/tree composition, live OpenSSH, product callers, legacy upload/fallback/default -behavior, tuple enablement, publication, and SignPath remain out of scope. +system-SSH tree-composition contract audit, 2026-07-15, Codex implementation owner.** +The next slice is limited to composing the proven staging control, per-file receiver, source stream, +and existing system-SSH channel adapter into one exclusive Windows staging tree with parent-first +directories, bounded concurrency/progress, and joined owned-root cleanup. Live OpenSSH, product +callers, legacy upload/fallback/default behavior, tuple enablement, publication, and SignPath remain +out of scope. `E-M6-WINDOWS-SYSTEM-SSH-CONTROL-LOCAL-RED-001` proves the native workflow oracle passes 9/9 and the purpose suite fails solely because the audited production staging-control module is absent. `E-M6-WINDOWS-SYSTEM-SSH-CONTROL-LOCAL-001` records 16/17 purpose cases locally with the real PowerShell case honestly skipped, 207 focused cases, 686 broad relay cases, 282/282 release contracts, typecheck, full lint, formatting, diff, protected-resolver, and no-product-consumer gates. Native Windows x64/arm64 `powershell.exe` proof remains required. +`E-M6-WINDOWS-SYSTEM-SSH-CONTROL-CI-001` closes that native gate: Windows x64/arm64 pass all +17 staging-control cases through real `powershell.exe`; all six primary cells, both Linux +supplements, Windows x64 floor, PR Checks, Golden E2E, and computer-use pass after an unchanged +Darwin x64 toolchain-timeout rerun. Windows arm64 retains only hosted build 26200 versus required +26100 after complete runtime smoke. `E-M6-WINDOWS-SYSTEM-SSH-FILE-LOCAL-RED-001` proves the workflow oracle passes 9/9 and the new purpose suite fails solely because the audited production destination is absent. `E-M6-WINDOWS-SYSTEM-SSH-FILE-LOCAL-001` records 14/15 Windows purpose cases locally with the real 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 ed966eee804..04a3c3262a8 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected Windows system-SSH staging-control native proof**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **WINDOWS STAGING CONTROL LOCALLY GREEN / NATIVE WINDOWS CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-WINDOWS-SYSTEM-SSH-CONTROL-LOCAL-001` proves the fixed-command bounded request, strict rooted-path/descendant validation, exclusive root/directory creation, parent-first enforcement, owned-root removal primitive, 30-second ceiling, joined command-channel lifecycle, prior transfer regressions, and production isolation locally. The real PowerShell 5.1 case is skipped on macOS and earns no native claim; commit and push the exact package, then require native Windows x64/arm64 proof. Do not compose file streaming into a tree, connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work in this package. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected Windows system-SSH tree-composition contract audit**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **WINDOWS STAGING CONTROL NATIVE PROOF CLOSED / TREE COMPOSITION AUDIT ACTIVE — 2026-07-15, Codex implementation owner** — `E-M6-WINDOWS-SYSTEM-SSH-CONTROL-CI-001` proves the real PowerShell staging-control contract on native Windows x64/arm64 and all adjacent exact-head gates, with only the declared hosted Windows arm64 floor gap. Audit the next disconnected tree owner before writing RED tests. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -18353,6 +18353,64 @@ src/main/ssh/ssh-remote-node-resolution.test.ts` is empty. A non-test reference classification/cache, bundled-Node verification/install/publish/launch, product/Beta/fallback/ default behavior, tuple enablement, publication, Apple signing, and SignPath remain absent. +### E-M6-WINDOWS-SYSTEM-SSH-CONTROL-CI-001 — exact-head native staging-control proof + +- Date/owner/head: 2026-07-15, Codex implementation owner; exact head + `68737239b98d88c5b60995935fcadadb60bf5b43` on draft PR #8741. Artifact run + `29529608561`; PR Checks `29529608567`, Golden E2E `29529608624`, and computer-use + `29529608564` all complete successfully on that head. +- Windows x64 job `87726531814`: requested `windows-2022`, resolved `win22` image + `20260714.244.1`, X64, Node 24.18.0. The staging-control suite passes 17/17 in 4.674 seconds; + its real `powershell.exe` exclusive-root/Unicode/owned-cleanup case passes in 4.310 seconds. The + primary set passes 99 files and 845 tests with 18 declared skips in 29.44 seconds. +- Windows arm64 job `87726531789`: requested `windows-11-arm`, resolved `win11-arm64` image + `20260714.109.1`, ARM64, Node 24.18.0. The staging-control suite passes 17/17 in 5.790 seconds; + its real `powershell.exe` case passes in 5.439 seconds. The primary set passes 99 files and 845 + tests with 18 declared skips in 34.11 seconds. +- First-attempt Linux x64 `87726531841`, Linux arm64 `87726531859`, and Darwin arm64 + `87726531848` pass. Darwin x64 `87726531879` times out only in the adjacent + `ssh-relay-runtime-toolchain.test.mjs` 30-second toolchain digest case. No timeout or product code + changes; unchanged attempt-2 Darwin x64 job `87730137143` passes. Both Linux supplements + `87731258107` and `87731258215` and Windows x64 floor `87728977786` pass. +- Windows arm64 floor jobs `87728977738` and unchanged rerun `87730137293` verify the complete + 60-entry/42-file/85,213,511-byte runtime, content + `sha256:02edf462be83c2864a89546d5344d348f9e07ce10964660342608a8c614e47db`, Node + 24.18.0/ABI 137, PTY exit 23/resize, and watcher create/update/delete smoke. Both then reject only + hosted build 26200 against required build 26100 (`platform=true`, `architecture=true`, + `osBuild=false`). This remains the declared hosted-runner gap. +- Disposition/residual boundary: the disconnected staging-control package is complete. This proves + native local Windows PowerShell semantics on x64/arm64 clients, not a Windows OpenSSH remote, + full-size transfer, tree composition, long paths, endpoint-protection locks, primitive detection, + remote verification/install/publish/launch, product/Beta/fallback/default behavior, tuple + enablement, publication, Apple signing, or SignPath. Legacy remains the sole production/default + path and every tuple stays disabled. + +### E-M6-WINDOWS-SYSTEM-SSH-TREE-AUDIT-001 — bounded disconnected tree composition + +- Date/owner/base: 2026-07-15, Codex implementation owner; audit-only package atop exact base + `68737239b98d88c5b60995935fcadadb60bf5b43`. +- Scope: compose one verified Windows scanned source tree, the proven staging-control primitive, the + proven per-file binary receiver, the source-stream scheduler, and the existing authenticated + system-SSH file-channel adapter. Create one exclusive root, create declared directories + parent-first, then stream each declared file to its exact descendant path. Default concurrency is + one and the existing package maximum remains four; return only aggregate progress and counts. +- Ownership/lifecycle: mark the root owned only after exclusive creation settles successfully. Never + clean a collision root. On every later failure or cancellation, stop and join source workers, then + run bounded cleanup with a separate five-second signal and join any cleanup failure with the + primary error. Every control/file channel uses the same operation signal; cleanup is the only + deliberately separate bounded signal. +- Required RED/green proof: reject non-Windows trees, unsafe roots, non-system transports, invalid + concurrency, and pre-abort before exec; exact root/directory/file request order and paths; exact + bytes/counts/progress; default-one and maximum-four active files; collision preservation; directory + and file failures clean only an owned root; retained-write cancellation settles before cleanup; + cleanup timeout is joined below ten seconds; no later progress; and no production consumer. Pin + the suite once in both native workflow command families before implementation. +- Explicit residual boundary: adapter-backed composition is not live Windows OpenSSH, a full-size + runtime measurement, long-path or endpoint-protection proof, primitive classification/cache, + remote bundled-Node verification/install/publish/launch, product/Beta/settings/fallback/tuple/ + publication/default behavior, or SignPath. Legacy remains the sole production/default path and + every tuple stays disabled. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -18411,11 +18469,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the exact `E-M6-WINDOWS-SYSTEM-SSH-CONTROL-LOCAL-001` package, then require the real -PowerShell 5.1 process oracle on native Windows x64/arm64 plus all-six native and adjacent repository -regression cells. Do not check the broad Milestone 6 Windows system-SSH item yet. Do not compose file -streaming into a remote-tree owner or add an Electron/startup/product importer, per-target mode -wiring, fallback, tuple enablement, release publication, or default behavior. +Write and run the purpose-named RED suite and native-workflow oracle for +`E-M6-WINDOWS-SYSTEM-SSH-TREE-AUDIT-001`, proving failure solely because the audited production +module is absent. Do not check the broad Milestone 6 Windows system-SSH item yet. Do not add an +Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release +publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. From 139bcd16d36bfa6d0b8db16ab8f2f2223325a7f6 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 16:32:49 -0400 Subject: [PATCH 237/267] feat(ssh): compose bounded Windows relay tree transfer Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 2 + .../ssh-relay-runtime-workflow.test.mjs | 1 + ...elease-implementation-checklist-summary.md | 17 +- ...github-release-implementation-checklist.md | 77 +++- ...elay-runtime-windows-tree-transfer.test.ts | 390 ++++++++++++++++++ ...ssh-relay-runtime-windows-tree-transfer.ts | 192 +++++++++ 6 files changed, 671 insertions(+), 8 deletions(-) create mode 100644 src/main/ssh/ssh-relay-runtime-windows-tree-transfer.test.ts create mode 100644 src/main/ssh/ssh-relay-runtime-windows-tree-transfer.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index d90f0de0a07..9ba6d320dd0 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -290,6 +290,7 @@ jobs: src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts \ src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts \ src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts \ + src/main/ssh/ssh-relay-runtime-windows-tree-transfer.test.ts \ src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts \ src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts \ src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts \ @@ -1031,6 +1032,7 @@ jobs: src/main/ssh/ssh-relay-runtime-posix-file-destination.test.ts ` src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts ` src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts ` + src/main/ssh/ssh-relay-runtime-windows-tree-transfer.test.ts ` src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts ` src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts ` src/main/ssh/ssh-relay-runtime-sftp-file-destination.test.ts ` diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index a5ee8115b4e..5601f06bb9f 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -204,6 +204,7 @@ describe('SSH relay runtime artifact workflow', () => { 'ssh-relay-runtime-posix-file-destination.test.ts', 'ssh-relay-runtime-windows-file-destination.test.ts', 'ssh-relay-runtime-windows-staging-control.test.ts', + 'ssh-relay-runtime-windows-tree-transfer.test.ts', 'ssh-relay-runtime-posix-tree-transfer.test.ts', 'ssh-relay-runtime-system-ssh-file-channel.test.ts', 'ssh-relay-runtime-sftp-file-destination.test.ts', 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 c512e0c4d29..a98bab60975 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 @@ -10,12 +10,17 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected Windows -system-SSH tree-composition contract audit, 2026-07-15, Codex implementation owner.** -The next slice is limited to composing the proven staging control, per-file receiver, source stream, -and existing system-SSH channel adapter into one exclusive Windows staging tree with parent-first -directories, bounded concurrency/progress, and joined owned-root cleanup. Live OpenSSH, product -callers, legacy upload/fallback/default behavior, tuple enablement, publication, and SignPath remain -out of scope. +system-SSH tree-composition native CI proof, 2026-07-15, Codex implementation owner.** +The local package is green; push its isolated commit and require exact-head all-six native artifact +and adjacent workflow evidence before auditing any live/full-size Windows OpenSSH package. Live +OpenSSH, product callers, legacy upload/fallback/default behavior, tuple enablement, publication, +and SignPath remain out of scope. +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LOCAL-RED-001` proves the native workflow oracle passes 9/9 and the +purpose suite fails solely because the audited production tree-composition module is absent. +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LOCAL-001` records 8/8 purpose and 9/9 workflow-oracle cases, 258 +focused passes with three declared skips, 694 broad relay passes with nine declared skips, 282/282 +release contracts, typecheck, full lint, formatting, max-lines, diff, protected-resolver, and +no-product-consumer gates. Exact-head native CI remains required. `E-M6-WINDOWS-SYSTEM-SSH-CONTROL-LOCAL-RED-001` proves the native workflow oracle passes 9/9 and the purpose suite fails solely because the audited production staging-control module is absent. `E-M6-WINDOWS-SYSTEM-SSH-CONTROL-LOCAL-001` records 16/17 purpose cases locally with the real 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 04a3c3262a8..18a69f57aa7 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected Windows system-SSH tree-composition contract audit**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **WINDOWS STAGING CONTROL NATIVE PROOF CLOSED / TREE COMPOSITION AUDIT ACTIVE — 2026-07-15, Codex implementation owner** — `E-M6-WINDOWS-SYSTEM-SSH-CONTROL-CI-001` proves the real PowerShell staging-control contract on native Windows x64/arm64 and all adjacent exact-head gates, with only the declared hosted Windows arm64 floor gap. Audit the next disconnected tree owner before writing RED tests. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected Windows system-SSH tree-composition native CI proof**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **WINDOWS TREE COMPOSITION LOCAL GREEN / EXACT-HEAD NATIVE CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-WINDOWS-SYSTEM-SSH-TREE-LOCAL-001` proves the disconnected composer and every audited local lifecycle gate. Commit and push the isolated package, then require all-six native artifact and adjacent exact-head workflows before auditing live/full-size Windows OpenSSH. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -18411,6 +18411,79 @@ src/main/ssh/ssh-remote-node-resolution.test.ts` is empty. A non-test reference publication/default behavior, or SignPath. Legacy remains the sole production/default path and every tuple stays disabled. +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LOCAL-RED-001 — audited tree composer is absent + +- Date/owner/base: 2026-07-15, Codex implementation owner; uncommitted test/workflow package atop + `c39e20cc6ccdfc66dbac25bd17d86fd4e53fdacd`. +- Format command: + `pnpm exec oxfmt --check .github/workflows/ssh-relay-runtime-artifacts.yml config/scripts/ssh-relay-runtime-workflow.test.mjs src/main/ssh/ssh-relay-runtime-windows-tree-transfer.test.ts 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` + passes all five files in 9.616 seconds. +- RED command: + `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-runtime-windows-tree-transfer.test.ts config/scripts/ssh-relay-runtime-workflow.test.mjs`. + The workflow oracle passes 9/9 in 215 ms. The purpose suite collects zero tests and fails at its + production import only with `Cannot find module '/src/main/ssh/ssh-relay-runtime-windows-tree-transfer'`; + there is no assertion, fixture, workflow-oracle, or adjacent failure. +- Disposition: the RED state matches `E-M6-WINDOWS-SYSTEM-SSH-TREE-AUDIT-001`. Implement only the + disconnected composer and expand the green suite to every audited lifecycle case. No product + consumer, live Windows OpenSSH claim, default/fallback change, tuple enablement, publication, or + SignPath work is authorized by this evidence. + +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LOCAL-001 — disconnected tree composer is locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted implementation, + purpose-test, native-workflow, and checklist package atop exact base + `c39e20cc6ccdfc66dbac25bd17d86fd4e53fdacd` on Apple arm64, macOS 26.2 build 25C56, Node + 26.5.0, pnpm 10.24.0. Local Node is newer than the repository's Node 24 contract, so exact-head + all-six native proof remains mandatory. +- Implementation: add one disconnected Windows tree owner over the proven staging-control, + per-file receiver, source-stream scheduler, and authenticated system-SSH channel adapter. It + validates a Windows scanned tree, rooted drive/UNC staging path, system transport, exact signal, + and one-to-four concurrency before I/O; defaults to one file; creates one exclusive root and every + declared directory parent-first; streams exact descendant paths/sizes; and returns only frozen + aggregate progress. Root ownership begins only after successful exclusive creation. Later failure + or cancellation joins all source workers before a separate five-second owned-root cleanup, and + joins primary plus cleanup errors. +- Purpose command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-windows-tree-transfer.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs`. Result: two files and 17/17 cases pass: 8/8 + composer lifecycle cases plus the 9/9 workflow oracle; 2.17 seconds real, 139,853,824-byte maximum + RSS, 98,866,936-byte peak footprint, zero swaps and block I/O. Proof covers hostile/pre-aborted + input before I/O, exact root/parent-first directories/files/bytes/counts/path-free progress, + default-one and maximum-four destinations, collision preservation, directory/file failure, + retained-write cancellation before cleanup, joined cleanup failure, five-second cleanup timeout, + and no later progress. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1` with the Windows tree/control/file, POSIX tree/control/file, system-channel, + source-tree/scan/stream, system-fallback, connection, and workflow-oracle suites. Result: 13 files, + 258 passed and three declared platform skips out of 261; 20.40 seconds real, 186,826,752-byte + maximum RSS, 97,179,360-byte peak footprint, zero swaps and block I/O. +- Broad relay command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts`. Result: 53 files passed, five files skipped; 694 + passed and nine declared skips out of 703; 46.75 seconds real, 324,173,824-byte maximum RSS, + 97,146,592-byte peak footprint, zero swaps and block I/O. +- Release-contract command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs`. Result: 50 + files and 282/282 cases pass; 19.62 seconds real, 195,706,880-byte maximum RSS, 97,900,328-byte + peak footprint, zero swaps and block I/O. +- Static evidence: final-code `pnpm typecheck` passes. `/usr/bin/time -l pnpm lint` passes in 23.95 + seconds real with 2,073,870,336-byte maximum RSS and 97,949,456-byte peak footprint; its 26 + warnings pre-exist outside this diff. Full lint proves switch exhaustiveness, 41 reliability gates, + the 355-entry max-lines ratchet with no new bypass, bundled guides, and localization checks. + Targeted `oxlint` and six-file `oxfmt --check` pass. +- Isolation evidence: `git diff --check` passes; `git diff --exit-code HEAD -- +src/main/ssh/ssh-remote-node-resolution.ts src/main/ssh/ssh-remote-node-resolution.test.ts` is + empty. An asserted non-test reference search finds the new export only in its owning production + module. The intended diff is limited to the disconnected composer/purpose test, its one entry in + each native workflow command family and oracle, and both checklist documents. No legacy uploader, + Electron/startup/product importer, settings, Beta mode, fallback, tuple, publication, default-path, + or signing behavior changes. +- Residual boundary: this is adapter-backed composition, not live Windows OpenSSH, a full-size + runtime transfer/measurement, long-path or endpoint-protection proof, primitive classification/ + cache, remote bundled-Node verification/install/publish/launch, product/Beta/fallback/default + behavior, tuple enablement, publication, Apple signing, or SignPath. Exact-head all-six native and + adjacent workflow proof remains required before this package closes. Legacy remains the sole + production/default path and every tuple stays disabled. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/src/main/ssh/ssh-relay-runtime-windows-tree-transfer.test.ts b/src/main/ssh/ssh-relay-runtime-windows-tree-transfer.test.ts new file mode 100644 index 00000000000..802ff0bbbd4 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-windows-tree-transfer.test.ts @@ -0,0 +1,390 @@ +import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { publishSshRelayArtifactCacheEntry } from './ssh-relay-artifact-cache-entry' +import { createSshRelayArtifactCacheEntryFixture } from './ssh-relay-artifact-cache-entry-fixture' +import { + acquireSshRelayArtifactCacheInUseLease, + type SshRelayArtifactCacheInUseLease +} from './ssh-relay-artifact-cache-in-use-lease' +import { scanSshRelayRuntimeSourceTree } from './ssh-relay-runtime-source-scan' +import { createSshRelayRuntimeSourceTree } from './ssh-relay-runtime-source-tree' +import type { SshRelayRuntimeSystemSshConnection } from './ssh-relay-runtime-system-ssh-file-channel' +import { + transferSshRelayRuntimeTreeViaWindowsSystemSsh, + SSH_RELAY_RUNTIME_WINDOWS_TREE_TRANSFER_LIMITS +} from './ssh-relay-runtime-windows-tree-transfer' + +const stagingControl = vi.hoisted(() => vi.fn()) +const openFileDestination = vi.hoisted(() => vi.fn()) + +vi.mock('./ssh-relay-runtime-windows-staging-control', () => ({ + runSshRelayRuntimeWindowsStagingControl: stagingControl +})) +vi.mock('./ssh-relay-runtime-windows-file-destination', () => ({ + openSshRelayRuntimeWindowsFileDestination: openFileDestination +})) + +const cleanupRoots = new Set() +const cleanupLeases = new Set() + +async function treeFixture(os: 'win32' | 'linux' = 'win32') { + const root = await mkdtemp(join(tmpdir(), 'orca-relay-windows-tree-')) + cleanupRoots.add(root) + const inputRoot = join(root, 'input') + await mkdir(inputRoot) + const fixture = await createSshRelayArtifactCacheEntryFixture({ root: inputRoot, os }) + const cacheRoot = join(root, 'cache') + const entry = await publishSshRelayArtifactCacheEntry({ + cacheRoot, + artifact: fixture.artifact, + archivePath: fixture.archivePath + }) + const lease = await acquireSshRelayArtifactCacheInUseLease({ cacheRoot, entry }) + cleanupLeases.add(lease) + const source = createSshRelayRuntimeSourceTree({ + kind: 'ready', + source: 'cache', + artifact: fixture.artifact, + entry, + lease + }) + return scanSshRelayRuntimeSourceTree(source, new AbortController().signal) +} + +function connection(system = true): SshRelayRuntimeSystemSshConnection { + return { usesSystemSshTransport: () => system, exec: vi.fn() } +} + +function retainDestinationWrites() { + let active = 0 + let peak = 0 + let holding = true + const pending = new Set<() => void>() + openFileDestination.mockImplementation(async ({ signal }: { signal: AbortSignal }) => { + active += 1 + peak = Math.max(peak, active) + let owned = true + const settle = (): void => { + if (owned) { + owned = false + active -= 1 + } + } + return { + write: vi.fn(() => { + if (!holding) { + return Promise.resolve() + } + return new Promise((resolve, reject) => { + const release = (): void => { + signal.removeEventListener('abort', onAbort) + pending.delete(release) + resolve() + } + const onAbort = (): void => { + pending.delete(release) + reject(signal.reason) + } + pending.add(release) + signal.addEventListener('abort', onAbort, { once: true }) + }) + }), + close: vi.fn(async () => settle()), + abort: vi.fn(async () => settle()) + } + }) + return { + peak: () => peak, + release: () => { + holding = false + for (const release of pending) { + release() + } + } + } +} + +beforeEach(() => { + stagingControl.mockReset().mockResolvedValue(undefined) + openFileDestination.mockReset().mockImplementation(async () => ({ + write: vi.fn(async () => {}), + close: vi.fn(async () => {}), + abort: vi.fn(async () => {}) + })) +}) + +afterEach(async () => { + vi.useRealTimers() + await Promise.all([...cleanupLeases].map((lease) => lease.release().catch(() => {}))) + cleanupLeases.clear() + await Promise.all([...cleanupRoots].map((root) => rm(root, { recursive: true, force: true }))) + cleanupRoots.clear() +}) + +describe('SSH relay runtime Windows system-SSH tree transfer', () => { + it('rejects POSIX trees, unsafe roots, transport, concurrency, and pre-abort before I/O', async () => { + const windows = await treeFixture() + const linux = await treeFixture('linux') + const signal = new AbortController().signal + for (const options of [ + { tree: linux, connection: connection(), remoteStagingRoot: 'C:/stage', signal }, + { tree: windows, connection: connection(), remoteStagingRoot: '../stage', signal }, + { tree: windows, connection: connection(), remoteStagingRoot: 'C:/CON/stage', signal }, + { tree: windows, connection: connection(), remoteStagingRoot: '//server/share', signal }, + { tree: windows, connection: connection(false), remoteStagingRoot: 'C:/stage', signal }, + { + tree: windows, + connection: connection(), + remoteStagingRoot: 'C:/stage', + maximumConcurrency: 5, + signal + } + ]) { + await expect(transferSshRelayRuntimeTreeViaWindowsSystemSsh(options)).rejects.toThrow() + } + const controller = new AbortController() + const reason = new Error('pre-aborted') + controller.abort(reason) + await expect( + transferSshRelayRuntimeTreeViaWindowsSystemSsh({ + tree: windows, + connection: connection(), + remoteStagingRoot: 'C:/stage', + signal: controller.signal + }) + ).rejects.toBe(reason) + expect(stagingControl).not.toHaveBeenCalled() + expect(openFileDestination).not.toHaveBeenCalled() + }) + + it('creates parent-first directories and streams every exact file before returning progress', async () => { + const tree = await treeFixture() + const remoteRoot = 'C:/Users/测试/.orca/stage' + const written = new Map() + const events: string[] = [] + stagingControl.mockImplementation(async ({ operation, remotePath }) => { + events.push(operation === 'create-directory' ? `directory:${remotePath}` : operation) + }) + openFileDestination.mockImplementation( + async ({ remotePath, expectedSize, signal, openChannel }) => { + events.push(`file:${remotePath}`) + expect(expectedSize).toBe(tree.files.find((file) => remotePath.endsWith(file.path))?.size) + expect(signal).toBeInstanceOf(AbortSignal) + expect(openChannel).toBeTypeOf('function') + return { + write: vi.fn(async (chunk: Buffer) => { + written.set( + remotePath, + Buffer.concat([written.get(remotePath) ?? Buffer.alloc(0), chunk]) + ) + }), + close: vi.fn(async () => {}), + abort: vi.fn(async () => {}) + } + } + ) + const progress: unknown[] = [] + const result = await transferSshRelayRuntimeTreeViaWindowsSystemSsh({ + tree, + connection: connection(), + remoteStagingRoot: remoteRoot, + maximumConcurrency: 2, + signal: new AbortController().signal, + onProgress: (value) => progress.push(value) + }) + + expect(result).toMatchObject({ + remoteStagingRoot: remoteRoot, + filesCompleted: tree.fileCount, + totalFiles: tree.fileCount, + bytesTransferred: tree.expandedBytes, + totalBytes: tree.expandedBytes + }) + expect(stagingControl.mock.calls[0]?.[0]).toMatchObject({ + operation: 'create-root', + remoteRoot + }) + const directoryCalls = stagingControl.mock.calls.slice(1).map(([value]) => value) + const expectedDirectories = [...tree.directories] + .sort( + (left, right) => + left.path.split('/').length - right.path.split('/').length || + (left.path < right.path ? -1 : left.path > right.path ? 1 : 0) + ) + .map((directory) => ({ + operation: 'create-directory', + remoteRoot, + remotePath: `${remoteRoot}/${directory.path}` + })) + expect(directoryCalls).toEqual( + expectedDirectories.map((value) => expect.objectContaining(value)) + ) + expect(events.findIndex((event) => event.startsWith('file:'))).toBe(tree.directories.length + 1) + for (const file of tree.files) { + const remotePath = `${remoteRoot}/${file.path}` + expect(written.get(remotePath)).toEqual(await readFile(file.localPath)) + expect(openFileDestination).toHaveBeenCalledWith( + expect.objectContaining({ remotePath, expectedSize: file.size }) + ) + } + expect(progress.length).toBeGreaterThan(0) + expect(progress.at(-1)).toMatchObject({ + filesCompleted: tree.fileCount, + bytesTransferred: tree.expandedBytes, + activeFiles: 0 + }) + expect(progress.some((value) => 'path' in (value as object))).toBe(false) + }) + + it('does not clean a pre-existing root when exclusive creation fails', async () => { + const tree = await treeFixture() + const collision = new Error('root exists') + stagingControl.mockRejectedValueOnce(collision) + await expect( + transferSshRelayRuntimeTreeViaWindowsSystemSsh({ + tree, + connection: connection(), + remoteStagingRoot: 'C:/existing/stage', + signal: new AbortController().signal + }) + ).rejects.toBe(collision) + expect(stagingControl).toHaveBeenCalledOnce() + }) + + it('defaults to one active destination and permits no more than four', async () => { + const tree = await treeFixture() + for (const maximumConcurrency of [undefined, 4]) { + const retained = retainDestinationWrites() + const transfer = transferSshRelayRuntimeTreeViaWindowsSystemSsh({ + tree, + connection: connection(), + remoteStagingRoot: 'C:/private/stage', + maximumConcurrency, + signal: new AbortController().signal + }) + const expected = maximumConcurrency ?? 1 + await vi.waitFor(() => expect(retained.peak()).toBe(expected)) + retained.release() + await transfer + expect(retained.peak()).toBeLessThanOrEqual(expected) + openFileDestination.mockReset() + } + }) + + it('cleans an owned root after a directory failure', async () => { + const tree = await treeFixture() + const primary = new Error('directory denied') + stagingControl.mockImplementation(async ({ operation }) => { + if (operation === 'create-directory') { + throw primary + } + }) + await expect( + transferSshRelayRuntimeTreeViaWindowsSystemSsh({ + tree, + connection: connection(), + remoteStagingRoot: 'C:/private/stage', + signal: new AbortController().signal + }) + ).rejects.toBe(primary) + expect(stagingControl.mock.calls.map(([value]) => value.operation)).toEqual([ + 'create-root', + 'create-directory', + 'remove-root' + ]) + }) + + it('joins file and owned-root cleanup failures', async () => { + const tree = await treeFixture() + const primary = new Error('remote disk full') + const cleanup = new Error('endpoint protection lock') + openFileDestination.mockRejectedValueOnce(primary) + stagingControl.mockImplementation(async ({ operation }: { operation: string }) => { + if (operation === 'remove-root') { + throw cleanup + } + }) + await expect( + transferSshRelayRuntimeTreeViaWindowsSystemSsh({ + tree, + connection: connection(), + remoteStagingRoot: 'C:/private/stage', + signal: new AbortController().signal + }) + ).rejects.toMatchObject({ errors: expect.arrayContaining([primary, cleanup]) }) + expect(stagingControl.mock.calls.at(-1)?.[0]).toMatchObject({ operation: 'remove-root' }) + }) + + it('settles a retained write before cleanup on cancellation and emits no later progress', async () => { + const tree = await treeFixture() + const controller = new AbortController() + const reason = new Error('cancelled Windows tree transfer') + const events: string[] = [] + const progress: unknown[] = [] + stagingControl.mockImplementation(async ({ operation }) => events.push(operation)) + openFileDestination.mockImplementation(async ({ signal }: { signal: AbortSignal }) => ({ + write: vi.fn( + () => + new Promise((_resolve, reject) => { + events.push('write-start') + signal.addEventListener('abort', () => reject(signal.reason), { once: true }) + }) + ), + close: vi.fn(async () => events.push('file-close')), + abort: vi.fn(async () => events.push('file-abort')) + })) + const transfer = transferSshRelayRuntimeTreeViaWindowsSystemSsh({ + tree, + connection: connection(), + remoteStagingRoot: 'C:/private/stage', + signal: controller.signal, + onProgress: (value) => progress.push(value) + }) + await vi.waitFor(() => expect(events).toContain('write-start')) + controller.abort(reason) + await expect(transfer).rejects.toBe(reason) + expect(events.indexOf('file-abort')).toBeLessThan(events.indexOf('remove-root')) + const settledProgress = progress.length + await new Promise((resolve) => setTimeout(resolve, 0)) + expect(progress).toHaveLength(settledProgress) + }) + + it('bounds unresponsive owned-root cleanup below ten seconds', async () => { + const tree = await treeFixture() + vi.useFakeTimers() + const primary = new Error('file open failed') + openFileDestination.mockRejectedValueOnce(primary) + stagingControl.mockImplementation(({ operation, signal }) => { + if (operation !== 'remove-root') { + return Promise.resolve() + } + return new Promise((_resolve, reject) => { + signal.addEventListener('abort', () => reject(signal.reason), { once: true }) + }) + }) + const startedAt = Date.now() + const transfer = transferSshRelayRuntimeTreeViaWindowsSystemSsh({ + tree, + connection: connection(), + remoteStagingRoot: 'C:/private/stage', + signal: new AbortController().signal + }) + const rejection = expect(transfer).rejects.toMatchObject({ + errors: expect.arrayContaining([ + primary, + expect.objectContaining({ message: expect.stringMatching(/cleanup timed out/i) }) + ]) + }) + await vi.waitFor(() => + expect(stagingControl.mock.calls.at(-1)?.[0]).toMatchObject({ operation: 'remove-root' }) + ) + await vi.advanceTimersByTimeAsync( + SSH_RELAY_RUNTIME_WINDOWS_TREE_TRANSFER_LIMITS.cleanupTimeoutMs + ) + await rejection + expect(Date.now() - startedAt).toBeLessThan(10_000) + }) +}) diff --git a/src/main/ssh/ssh-relay-runtime-windows-tree-transfer.ts b/src/main/ssh/ssh-relay-runtime-windows-tree-transfer.ts new file mode 100644 index 00000000000..bea22050d55 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-windows-tree-transfer.ts @@ -0,0 +1,192 @@ +import { assertSafeRemotePathSegment, normalizeWindowsRemotePath } from './ssh-remote-platform' +import type { SshRelayRuntimeScannedSourceTree } from './ssh-relay-runtime-source-scan' +import { + streamSshRelayRuntimeSourceTree, + type SshRelayRuntimeSourceStreamOptions, + type SshRelayRuntimeSourceStreamResult +} from './ssh-relay-runtime-source-stream' +import { + openSshRelayRuntimeSystemSshFileChannel, + type SshRelayRuntimeSystemSshConnection +} from './ssh-relay-runtime-system-ssh-file-channel' +import { openSshRelayRuntimeWindowsFileDestination } from './ssh-relay-runtime-windows-file-destination' +import { + runSshRelayRuntimeWindowsStagingControl, + type RunSshRelayRuntimeWindowsStagingControlOptions +} from './ssh-relay-runtime-windows-staging-control' + +export type SshRelayRuntimeWindowsTreeTransferOptions = Readonly<{ + tree: SshRelayRuntimeScannedSourceTree + connection: SshRelayRuntimeSystemSshConnection + remoteStagingRoot: string + signal: AbortSignal + maximumConcurrency?: number + onProgress?: SshRelayRuntimeSourceStreamOptions['onProgress'] +}> + +export type SshRelayRuntimeWindowsTreeTransferResult = Readonly< + SshRelayRuntimeSourceStreamResult & { remoteStagingRoot: string } +> + +const MAXIMUM_CONCURRENT_FILES = 4 +const CLEANUP_TIMEOUT_MS = 5_000 + +type OpenChannel = RunSshRelayRuntimeWindowsStagingControlOptions['openChannel'] + +function normalizeStagingRoot(value: string): string { + if ( + typeof value !== 'string' || + value === '' || + value.includes('\0') || + value.includes('\r') || + value.includes('\n') + ) { + throw new Error('SSH relay runtime Windows staging root is invalid') + } + const normalized = normalizeWindowsRemotePath(value) + const driveRooted = /^[A-Za-z]:\//u.test(normalized) + const uncRooted = /^\/\/[^/]+\/[^/]+\//u.test(normalized) + if ((!driveRooted && !uncRooted) || normalized.endsWith('/')) { + throw new Error('SSH relay runtime Windows staging root is invalid') + } + const segments = driveRooted ? normalized.slice(3).split('/') : normalized.slice(2).split('/') + try { + for (const segment of segments) { + assertSafeRemotePathSegment(segment, 'windows') + } + } catch { + throw new Error('SSH relay runtime Windows staging root is invalid') + } + return normalized +} + +function validateOptions(options: SshRelayRuntimeWindowsTreeTransferOptions): number { + const concurrency = options?.maximumConcurrency ?? 1 + if ( + !options?.tree || + !options.connection || + typeof options.connection.usesSystemSshTransport !== 'function' || + typeof options.connection.exec !== 'function' || + !options.signal || + typeof options.signal.throwIfAborted !== 'function' || + !Number.isInteger(concurrency) || + concurrency < 1 || + concurrency > MAXIMUM_CONCURRENT_FILES + ) { + throw new Error('SSH relay runtime Windows tree transfer input or concurrency is invalid') + } + if (options.tree.os !== 'win32') { + throw new Error('SSH relay runtime Windows tree transfer requires a Windows tree') + } + if (!options.connection.usesSystemSshTransport()) { + throw new Error('SSH relay runtime Windows tree transfer requires system SSH transport') + } + return concurrency +} + +function remoteManifestPath(remoteStagingRoot: string, manifestPath: string): string { + const segments = manifestPath.split('/') + for (const segment of segments) { + assertSafeRemotePathSegment(segment, 'windows') + } + // Why: manifest paths are remote slash paths; client-native path utilities would corrupt them. + return `${remoteStagingRoot}/${segments.join('/')}` +} + +function joinedFailure(primary: unknown, cleanupFailure: unknown | undefined): unknown { + return cleanupFailure === undefined + ? primary + : new AggregateError( + [primary, cleanupFailure], + 'SSH relay runtime Windows tree transfer cleanup failed' + ) +} + +async function cleanupOwnedRoot( + remoteStagingRoot: string, + openChannel: OpenChannel +): Promise { + const controller = new AbortController() + // Why: caller cancellation initiates cleanup; a separate bounded signal lets cleanup still run. + const timeout = setTimeout( + () => controller.abort(new Error('SSH relay runtime Windows tree cleanup timed out')), + CLEANUP_TIMEOUT_MS + ) + try { + await runSshRelayRuntimeWindowsStagingControl({ + operation: 'remove-root', + remoteRoot: remoteStagingRoot, + signal: controller.signal, + openChannel + }) + } finally { + clearTimeout(timeout) + } +} + +export async function transferSshRelayRuntimeTreeViaWindowsSystemSsh( + options: SshRelayRuntimeWindowsTreeTransferOptions +): Promise { + const maximumConcurrency = validateOptions(options) + const { tree, connection, signal, onProgress } = options + const remoteStagingRoot = normalizeStagingRoot(options.remoteStagingRoot) + const openChannel: OpenChannel = (command, exactSignal) => + openSshRelayRuntimeSystemSshFileChannel(connection, command, exactSignal) + const runControl = ( + control: Omit + ): Promise => runSshRelayRuntimeWindowsStagingControl({ ...control, signal, openChannel }) + signal.throwIfAborted() + let rootOwned = false + + try { + await runControl({ operation: 'create-root', remoteRoot: remoteStagingRoot }) + rootOwned = true + signal.throwIfAborted() + + const directories = [...tree.directories].sort( + (left, right) => + left.path.split('/').length - right.path.split('/').length || + (left.path < right.path ? -1 : left.path > right.path ? 1 : 0) + ) + for (const directory of directories) { + signal.throwIfAborted() + await runControl({ + operation: 'create-directory', + remoteRoot: remoteStagingRoot, + remotePath: remoteManifestPath(remoteStagingRoot, directory.path) + }) + signal.throwIfAborted() + } + + const result = await streamSshRelayRuntimeSourceTree({ + tree, + signal, + maximumConcurrency, + onProgress, + openDestination: (file, exactSignal) => + openSshRelayRuntimeWindowsFileDestination({ + remotePath: remoteManifestPath(remoteStagingRoot, file.path), + expectedSize: file.size, + signal: exactSignal, + openChannel + }) + }) + signal.throwIfAborted() + return Object.freeze({ remoteStagingRoot, ...result }) + } catch (error) { + let cleanupFailure: unknown + if (rootOwned) { + try { + await cleanupOwnedRoot(remoteStagingRoot, openChannel) + } catch (cleanupError) { + cleanupFailure = cleanupError + } + } + throw joinedFailure(error, cleanupFailure) + } +} + +export const SSH_RELAY_RUNTIME_WINDOWS_TREE_TRANSFER_LIMITS = Object.freeze({ + maximumConcurrentFiles: MAXIMUM_CONCURRENT_FILES, + cleanupTimeoutMs: CLEANUP_TIMEOUT_MS +}) From 8018152043a982f0d2c8d2907208858551c01176 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 16:50:41 -0400 Subject: [PATCH 238/267] docs(ssh): record native Windows tree composition proof Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 15 +++--- ...github-release-implementation-checklist.md | 54 ++++++++++++++++--- 2 files changed, 56 insertions(+), 13 deletions(-) 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 a98bab60975..3ae50420026 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 @@ -9,18 +9,21 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected Windows -system-SSH tree-composition native CI proof, 2026-07-15, Codex implementation owner.** -The local package is green; push its isolated commit and require exact-head all-six native artifact -and adjacent workflow evidence before auditing any live/full-size Windows OpenSSH package. Live -OpenSSH, product callers, legacy upload/fallback/default behavior, tuple enablement, publication, -and SignPath remain out of scope. +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — live/full-size Windows +system-SSH contract audit, 2026-07-15, Codex implementation owner.** +Audit a purpose-named native Windows OpenSSH/full-size proof over the now-proven disconnected tree +composer before writing its RED suite or workflow fixture. Product callers, legacy upload/fallback/ +default behavior, tuple enablement, publication, and SignPath remain out of scope. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LOCAL-RED-001` proves the native workflow oracle passes 9/9 and the purpose suite fails solely because the audited production tree-composition module is absent. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LOCAL-001` records 8/8 purpose and 9/9 workflow-oracle cases, 258 focused passes with three declared skips, 694 broad relay passes with nine declared skips, 282/282 release contracts, typecheck, full lint, formatting, max-lines, diff, protected-resolver, and no-product-consumer gates. Exact-head native CI remains required. +`E-M6-WINDOWS-SYSTEM-SSH-TREE-CI-001` closes that gate at exact implementation head `139bcd16d3`: +all six primary native jobs, both Linux supplements, Windows x64 floor, PR Checks, Golden E2E, and +computer-use pass. Windows arm64 retains only hosted build 26200 versus required 26100 after complete +85,213,511-byte runtime/Node/PTY/watcher/resource proof. This does not prove live Windows OpenSSH. `E-M6-WINDOWS-SYSTEM-SSH-CONTROL-LOCAL-RED-001` proves the native workflow oracle passes 9/9 and the purpose suite fails solely because the audited production staging-control module is absent. `E-M6-WINDOWS-SYSTEM-SSH-CONTROL-LOCAL-001` records 16/17 purpose cases locally with the real 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 18a69f57aa7..e3de527110f 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected Windows system-SSH tree-composition native CI proof**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **WINDOWS TREE COMPOSITION LOCAL GREEN / EXACT-HEAD NATIVE CI NEXT — 2026-07-15, Codex implementation owner** — `E-M6-WINDOWS-SYSTEM-SSH-TREE-LOCAL-001` proves the disconnected composer and every audited local lifecycle gate. Commit and push the isolated package, then require all-six native artifact and adjacent exact-head workflows before auditing live/full-size Windows OpenSSH. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: live/full-size Windows system-SSH contract audit**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **WINDOWS TREE COMPOSITION NATIVE PROOF CLOSED / LIVE WINDOWS OPENSSH AUDIT ACTIVE — 2026-07-15, Codex implementation owner** — `E-M6-WINDOWS-SYSTEM-SSH-TREE-CI-001` closes the disconnected composer at exact implementation head `139bcd16d36bfa6d0b8db16ab8f2f2223325a7f6`; all six primary native jobs and every adjacent gate pass except the declared Windows arm64 hosted build-26200 versus required-26100 floor check after complete runtime proof. Audit the first real full-size Windows OpenSSH package before writing RED tests or workflow fixture code. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -18484,6 +18484,44 @@ src/main/ssh/ssh-remote-node-resolution.ts src/main/ssh/ssh-remote-node-resoluti adjacent workflow proof remains required before this package closes. Legacy remains the sole production/default path and every tuple stays disabled. +### E-M6-WINDOWS-SYSTEM-SSH-TREE-CI-001 — exact-head native tree-composition proof + +- Date/owner/head: 2026-07-15, Codex implementation owner; exact implementation head + `139bcd16d36bfa6d0b8db16ab8f2f2223325a7f6` on draft PR #8741. Artifact run + `29532537623`; PR Checks `29532537644`, Golden E2E `29532537556`, and computer-use + `29532537582`. The three adjacent workflows complete successfully. +- All six primary Node 24.18.0 contract/build/full-size cells pass. Linux x64 job `87736017311` + (`ubuntu24`, image `20260714.240.1`, X64) passes 99 files, 864 cases with three declared skips; + Linux arm64 `87736017380` (`ubuntu24-arm64`, image `20260714.61.1`, ARM64) passes the same totals. + Darwin arm64 `87736017303` (`macos15`, image `20260715.0234.1`, ARM64) and Darwin x64 + `87736017358` pass 99 files, 863 cases with four declared skips. The complete jobs finish in + 2m38s, 3m42s, 2m34s, and 4m22s respectively. Both Linux jobs also pass their existing exact + full-size live SFTP and restricted POSIX system-SSH regressions; these are adjacent proof, not a + Windows-live claim. +- Windows x64 job `87736017345`: requested `windows-2022`, resolved `win22` image + `20260714.244.1`, X64. Windows arm64 job `87736017295`: requested `windows-11-arm`, resolved + `win11-arm64` image `20260714.109.1`, ARM64. Each passes 100 contract files, 853 cases with 18 + declared skips, exact clean-build/runtime smoke and full-size cache/extraction boundaries. The + complete x64 job finishes in 6m24s and arm64 in 9m51s. The workflow command contains the new + eight-case Windows tree-composition suite exactly once, as locked by the 9/9 workflow oracle. +- Both Linux oldest-userland supplements pass: arm64 `87736995346` and x64 `87736995358`. + Windows x64 floor job `87738195646` passes on Windows Server build 20348 and confirms + `osBuild=true` for the x64 contract. +- Windows arm64 floor job `87738195657` downloads and digest-verifies the exact unpublished native + artifact, then verifies 60 entries, 42 files, 85,213,511 expanded bytes, content + `sha256:02edf462be83c2864a89546d5344d348f9e07ce10964660342608a8c614e47db`, Node + 24.18.0/ABI 137, PTY exit 23 and 37x101 resize, watcher create/update/delete/rename lifecycle, + 2,000 ms resource settlement, 49,307,648 RSS bytes, 6,557.614 ms smoke, and 8,513.024 ms total. + It then rejects only hosted Windows build 26200 against required build 26100: + `platform=true`, `architecture=true`, `osBuild=false`. The runner is `win11-arm64` image + `20260714.109.1`; this remains the declared hosted-runner gap rather than a package regression. +- Disposition/residual boundary: the disconnected Windows tree-composition package is complete. + This proves native Node/client behavior plus previously proven local PowerShell primitives, not a + real Windows OpenSSH server, full-size Windows system-SSH transfer, long paths, endpoint-protection + interference, primitive classification/cache, remote bundled-Node verification/install/publish/ + launch, product/Beta/fallback/default behavior, tuple enablement, publication, Apple signing, or + SignPath. Legacy remains the sole production/default path and every tuple stays disabled. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -18542,11 +18580,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Write and run the purpose-named RED suite and native-workflow oracle for -`E-M6-WINDOWS-SYSTEM-SSH-TREE-AUDIT-001`, proving failure solely because the audited production -module is absent. Do not check the broad Milestone 6 Windows system-SSH item yet. Do not add an -Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release -publication, or default behavior. +Audit a purpose-named full-size Windows system-SSH proof against a real native Windows OpenSSH server. +Fix its exact fixture ownership, authentication/host-key trust, local account/ACLs, PowerShell 5.1/ +.NET receiver command, full runtime bytes/counts, serial/default and four-channel measurements, +cancellation settlement, collision/cleanup cases, x64/arm64 runner coverage, and teardown behavior +before writing a RED suite or workflow steps. Do not check the broad Milestone 6 Windows system-SSH +item yet. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple +enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. From 0a85f88003fee626cbeb91d9ed65880e4b566991 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 17:14:23 -0400 Subject: [PATCH 239/267] test(ssh): add live Windows runtime transfer proof Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 263 ++++++++++++ .../ssh-relay-runtime-workflow.test.mjs | 43 ++ ...elease-implementation-checklist-summary.md | 27 +- ...github-release-implementation-checklist.md | 152 ++++++- ...ndows-system-ssh-openssh-full-size.test.ts | 380 ++++++++++++++++++ 5 files changed, 847 insertions(+), 18 deletions(-) create mode 100644 src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 9ba6d320dd0..a18ea57249a 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -1310,6 +1310,269 @@ jobs: throw 'full-size desktop cache measurement cleanup failed' } + - name: Start loopback Windows OpenSSH system-SSH fixture + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $capabilityName = 'OpenSSH.Server~~~~0.0.1.0' + $capability = Get-WindowsCapability -Online -Name $capabilityName + if ($capability.State -ne 'Installed') { + # Why: use the target OS's native server on both architectures; substitutes earn no proof. + Add-WindowsCapability -Online -Name $capabilityName | Out-Null + $capability = Get-WindowsCapability -Online -Name $capabilityName + } + if ($capability.State -ne 'Installed') { + throw "Windows OpenSSH Server capability is unavailable: $($capability.State)" + } + + $openSshRoot = Join-Path $env:WINDIR 'System32/OpenSSH' + $sshd = Join-Path $openSshRoot 'sshd.exe' + $ssh = Join-Path $openSshRoot 'ssh.exe' + $sshKeygen = Join-Path $openSshRoot 'ssh-keygen.exe' + foreach ($binary in @($sshd, $ssh, $sshKeygen)) { + if (-not (Test-Path -LiteralPath $binary -PathType Leaf)) { + throw "Windows OpenSSH capability is missing $binary" + } + } + $windowsPowerShell = Join-Path $env:WINDIR 'System32/WindowsPowerShell/v1.0/powershell.exe' + if (-not (Test-Path -LiteralPath $windowsPowerShell -PathType Leaf)) { + throw 'Windows PowerShell 5.1 executable is missing' + } + + $fixture = Join-Path $env:RUNNER_TEMP 'orca-live-windows-system-ssh' + if (Test-Path -LiteralPath $fixture) { + throw "Windows OpenSSH fixture root already exists: $fixture" + } + $remoteRoot = Join-Path $fixture 'remote' + $clientHome = Join-Path $fixture 'client-home' + $clientSsh = Join-Path $clientHome '.ssh' + New-Item -ItemType Directory -Path $fixture, $remoteRoot, $clientSsh | Out-Null + $rootMarker = Join-Path $fixture 'fixture-owned' + New-Item -ItemType File -Path $rootMarker | Out-Null + + $fixtureUser = 'orca_ssh_fixture' + if (Get-LocalUser -Name $fixtureUser -ErrorAction SilentlyContinue) { + throw "Windows OpenSSH fixture account already exists: $fixtureUser" + } + $password = ConvertTo-SecureString "$([Guid]::NewGuid().ToString('N'))aA!7" -AsPlainText -Force + $account = New-LocalUser -Name $fixtureUser -Password $password -PasswordNeverExpires ` + -UserMayNotChangePassword -AccountNeverExpires + New-Item -ItemType File -Path (Join-Path $fixture 'account-owned') | Out-Null + $userSid = $account.SID.Value + + function Set-FixtureAcl([string]$Path, [string[]]$Grants) { + & icacls.exe $Path /inheritance:r | Out-Host + if ($LASTEXITCODE -ne 0) { throw "Failed to remove inherited ACLs from $Path" } + $arguments = @($Path, '/grant:r') + $Grants + & icacls.exe @arguments | Out-Host + if ($LASTEXITCODE -ne 0) { throw "Failed to grant fixture ACLs on $Path" } + } + $systemFull = '*S-1-5-18:(OI)(CI)F' + $administratorsFull = '*S-1-5-32-544:(OI)(CI)F' + Set-FixtureAcl $fixture @($systemFull, $administratorsFull, "*$userSid`:RX") + Set-FixtureAcl $remoteRoot @($systemFull, $administratorsFull, "*$userSid`:(OI)(CI)M") + + $clientKey = Join-Path $fixture 'client-key' + $hostKey = Join-Path $fixture 'host-key' + & $sshKeygen -q -t ed25519 -N '' -f $clientKey + if ($LASTEXITCODE -ne 0) { throw 'Windows OpenSSH client key generation failed' } + & $sshKeygen -q -t ed25519 -N '' -f $hostKey + if ($LASTEXITCODE -ne 0) { throw 'Windows OpenSSH host key generation failed' } + $authorizedKeys = Join-Path $fixture 'authorized_keys' + Copy-Item -LiteralPath "$clientKey.pub" -Destination $authorizedKeys + Set-FixtureAcl $authorizedKeys @( + '*S-1-5-18:F', + '*S-1-5-32-544:F', + "*$userSid`:R" + ) + Set-FixtureAcl $hostKey @('*S-1-5-18:F', '*S-1-5-32-544:F') + + $port = 22224 + $knownHosts = Join-Path $clientSsh 'known_hosts' + $hostPublicKey = (Get-Content -LiteralPath "$hostKey.pub" -Raw).Trim() + [IO.File]::WriteAllText( + $knownHosts, + "[127.0.0.1]:$port $hostPublicKey`n", + [Text.UTF8Encoding]::new($false) + ) + $clientConfig = Join-Path $clientSsh 'config' + $knownHostsSshPath = $knownHosts.Replace('\', '/') + [IO.File]::WriteAllText( + $clientConfig, + "Host *`n StrictHostKeyChecking yes`n UserKnownHostsFile $knownHostsSshPath`n", + [Text.UTF8Encoding]::new($false) + ) + + $config = Join-Path $fixture 'sshd_config' + $pidFile = Join-Path $fixture 'sshd.pid' + $log = Join-Path $fixture 'sshd.log' + $hostKeySshPath = $hostKey.Replace('\', '/') + $authorizedKeysSshPath = $authorizedKeys.Replace('\', '/') + $pidSshPath = $pidFile.Replace('\', '/') + @( + "Port $port", + 'ListenAddress 127.0.0.1', + "HostKey $hostKeySshPath", + "PidFile $pidSshPath", + "AuthorizedKeysFile $authorizedKeysSshPath", + "AllowUsers $fixtureUser", + 'PubkeyAuthentication yes', + 'PasswordAuthentication no', + 'KbdInteractiveAuthentication no', + 'ChallengeResponseAuthentication no', + 'StrictModes yes', + 'LogLevel VERBOSE' + ) | Set-Content -LiteralPath $config -Encoding ascii + & $sshd -t -f $config + if ($LASTEXITCODE -ne 0) { throw 'Windows OpenSSH fixture config validation failed' } + + $serviceName = 'sshd' + $service = Get-CimInstance Win32_Service -Filter "Name='$serviceName'" -ErrorAction SilentlyContinue + if (-not $service) { throw 'Windows OpenSSH capability did not register the sshd service' } + if ($service.State -ne 'Stopped') { + throw "Windows OpenSSH service must be stopped before the fixture: $($service.State)" + } + $serviceBackup = [ordered]@{ + pathName = $service.PathName + startMode = $service.StartMode + } | ConvertTo-Json -Compress + $serviceBackupPath = Join-Path $fixture 'service-backup.json' + [IO.File]::WriteAllText( + $serviceBackupPath, + "$serviceBackup`n", + [Text.UTF8Encoding]::new($false) + ) + # Why: mark the reversible service mutation before attempting it so failure still restores. + New-Item -ItemType File -Path (Join-Path $fixture 'service-owned') | Out-Null + $binaryPath = "`"$sshd`" -f `"$config`" -E `"$log`"" + & sc.exe config $serviceName binPath= $binaryPath start= demand | Out-Host + if ($LASTEXITCODE -ne 0) { throw 'Windows OpenSSH fixture service configuration failed' } + Start-Service -Name $serviceName + + $ready = $false + for ($attempt = 1; $attempt -le 40; $attempt += 1) { + $output = & $ssh -F NUL -i $clientKey -p $port -o BatchMode=yes ` + -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes ` + -o "UserKnownHostsFile=$knownHosts" "$fixtureUser@127.0.0.1" ` + 'powershell.exe -NoProfile -NonInteractive -Command "$PSVersionTable.PSVersion.ToString()"' 2>&1 + if ($LASTEXITCODE -eq 0) { + $remotePowerShellVersion = ($output -join '').Trim() + $ready = $true + break + } + Start-Sleep -Milliseconds 250 + } + if (-not $ready) { + if (Test-Path -LiteralPath $log) { Get-Content -LiteralPath $log -Tail 200 } + throw 'Windows OpenSSH fixture did not accept pinned-key authentication' + } + if (([Version]$remotePowerShellVersion).Major -ne 5) { + throw "Windows OpenSSH did not execute Windows PowerShell 5.1: $remotePowerShellVersion" + } + $serverVersion = ((& $sshd -V 2>&1) -join ' ').Trim() + if (-not $serverVersion) { throw 'Windows OpenSSH server version is empty' } + $remoteRootSshPath = $remoteRoot.Replace('\', '/') + @( + "ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_HOST=127.0.0.1", + "ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_PORT=$port", + "ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_USER=$fixtureUser", + "ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_IDENTITY=$clientKey", + "ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_REMOTE_ROOT=$remoteRootSshPath", + "ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_SERVER_VERSION=$serverVersion", + "ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_POWERSHELL_VERSION=$remotePowerShellVersion", + "ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_CLIENT_HOME=$clientHome", + "ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_SERVICE=$serviceName", + "ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_ACCOUNT=$fixtureUser" + ) | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "windows_openssh_server=$serverVersion" + "windows_powershell=$remotePowerShellVersion" + + - name: Measure exact full-size runtime over Windows system SSH + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $firstOutput = Join-Path $env:RUNNER_TEMP 'ssh-relay-runtime/${{ matrix.tuple }}/first' + $env:ORCA_SSH_FORCE_SYSTEM_TRANSPORT = '1' + $env:ORCA_SSH_RELAY_FULL_SIZE_RUNTIME_ROOT = Join-Path $firstOutput 'runtime' + $env:ORCA_SSH_RELAY_FULL_SIZE_IDENTITY = Join-Path ` + $firstOutput 'orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json' + # Why: keep exact host-key trust isolated from the hosted runner's real SSH profile. + $env:HOME = $env:ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_CLIENT_HOME + $env:USERPROFILE = $env:ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_CLIENT_HOME + pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 --reporter=verbose ` + src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts + if ($LASTEXITCODE -ne 0) { throw 'full-size Windows system-SSH measurement failed' } + + - name: Stop loopback Windows OpenSSH system-SSH fixture + if: always() && runner.os == 'Windows' + shell: pwsh + run: | + $ErrorActionPreference = 'Continue' + $failures = [Collections.Generic.List[string]]::new() + $fixture = Join-Path $env:RUNNER_TEMP 'orca-live-windows-system-ssh' + $serviceName = if ($env:ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_SERVICE) { + $env:ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_SERVICE + } else { 'sshd' } + $fixtureUser = if ($env:ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_ACCOUNT) { + $env:ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_ACCOUNT + } else { 'orca_ssh_fixture' } + $ownsFixture = Test-Path -LiteralPath (Join-Path $fixture 'fixture-owned') + $ownsService = Test-Path -LiteralPath (Join-Path $fixture 'service-owned') + $ownsAccount = Test-Path -LiteralPath (Join-Path $fixture 'account-owned') + try { + $service = Get-CimInstance Win32_Service -Filter "Name='$serviceName'" -ErrorAction SilentlyContinue + $servicePid = if ($service) { [int]$service.ProcessId } else { 0 } + if ($ownsService -and $service -and $service.State -ne 'Stopped') { + Stop-Service -Name $serviceName -Force -ErrorAction Stop + if ($servicePid -gt 0) { + Wait-Process -Id $servicePid -Timeout 10 -ErrorAction SilentlyContinue + if (Get-Process -Id $servicePid -ErrorAction SilentlyContinue) { + throw "Windows OpenSSH fixture process did not settle: $servicePid" + } + } + } + if ($ownsService) { + $backupPath = Join-Path $fixture 'service-backup.json' + $backup = Get-Content -LiteralPath $backupPath -Raw | ConvertFrom-Json + $startToken = switch ($backup.startMode) { + 'Auto' { 'auto' } + 'Manual' { 'demand' } + 'Disabled' { 'disabled' } + default { throw "Unsupported original sshd start mode: $($backup.startMode)" } + } + & sc.exe config $serviceName binPath= $backup.pathName start= $startToken | Out-Host + if ($LASTEXITCODE -ne 0) { throw 'Windows OpenSSH service restoration failed' } + $restored = Get-CimInstance Win32_Service -Filter "Name='$serviceName'" -ErrorAction Stop + if ( + $restored.State -ne 'Stopped' -or + $restored.PathName -ne $backup.pathName -or + $restored.StartMode -ne $backup.startMode + ) { + throw 'Windows OpenSSH service did not return to its exact prior state' + } + } + } catch { $failures.Add("service teardown: $($_.Exception.Message)") } + $log = Join-Path $fixture 'sshd.log' + if (Test-Path -LiteralPath $log) { Get-Content -LiteralPath $log -Tail 200 } + try { + if ($ownsAccount -and (Get-LocalUser -Name $fixtureUser -ErrorAction SilentlyContinue)) { + Remove-LocalUser -Name $fixtureUser -ErrorAction Stop + } + } catch { $failures.Add("account teardown: $($_.Exception.Message)") } + try { + $profile = Join-Path $env:SystemDrive "Users/$fixtureUser" + if ($ownsAccount -and (Test-Path -LiteralPath $profile)) { + Remove-Item -LiteralPath $profile -Recurse -Force -ErrorAction Stop + } + if ($ownsFixture -and (Test-Path -LiteralPath $fixture)) { + Remove-Item -LiteralPath $fixture -Recurse -Force -ErrorAction Stop + } + } catch { $failures.Add("filesystem teardown: $($_.Exception.Message)") } + if ($ownsAccount -and (Get-LocalUser -Name $fixtureUser -ErrorAction SilentlyContinue)) { + $failures.Add('fixture account remains registered') + } + if ($failures.Count -gt 0) { throw ($failures -join '; ') } + - name: Upload unpublished artifact evidence uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 5601f06bb9f..9492a17bfdd 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -140,6 +140,49 @@ describe('SSH relay runtime artifact workflow', () => { expect(stop.if).toBe("always() && runner.os == 'Linux'") }) + it('runs full-size Windows system SSH against loopback native OpenSSH', async () => { + const workflow = parse(await readFile(workflowUrl, 'utf8')) + const steps = workflow.jobs['build-windows-runtime'].steps + const start = steps.find( + (step) => step.name === 'Start loopback Windows OpenSSH system-SSH fixture' + ) + const measure = steps.find( + (step) => step.name === 'Measure exact full-size runtime over Windows system SSH' + ) + const stop = steps.find( + (step) => step.name === 'Stop loopback Windows OpenSSH system-SSH fixture' + ) + + expect(start).toBeDefined() + expect(measure).toBeDefined() + expect(stop).toBeDefined() + expect(start.shell).toBe('pwsh') + expect(start.run).toContain('OpenSSH.Server~~~~0.0.1.0') + expect(start.run).toContain('Add-WindowsCapability') + expect(start.run).toContain('New-LocalUser') + expect(start.run).toContain('ListenAddress 127.0.0.1') + expect(start.run).toContain('PasswordAuthentication no') + expect(start.run).toContain('StrictModes yes') + expect(start.run).toContain('StrictHostKeyChecking=yes') + expect(start.run).toContain('powershell.exe') + expect(start.run).toContain('([Version]$remotePowerShellVersion).Major -ne 5') + expect(start.run).not.toContain('administrators_authorized_keys') + expect(start.run).not.toContain('New-Service') + expect(measure.shell).toBe('pwsh') + expect(measure.run).toContain("$env:ORCA_SSH_FORCE_SYSTEM_TRANSPORT = '1'") + expect(measure.run).toContain('ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts') + expect(measure.run).toContain("Join-Path $firstOutput 'runtime'") + expect(stop.if).toBe("always() && runner.os == 'Windows'") + expect(stop.shell).toBe('pwsh') + expect(stop.run).toContain('fixture-owned') + expect(stop.run).toContain('service-owned') + expect(stop.run).toContain('account-owned') + expect(stop.run).toContain('service-backup.json') + expect(stop.run).toContain('sc.exe config') + expect(stop.run).not.toContain('sc.exe delete') + expect(stop.run).toContain('Remove-LocalUser') + }) + it('uploads only the first output after two clean builds verify and compare', async () => { const source = await readFile(workflowUrl, 'utf8') const workflow = parse(source) 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 3ae50420026..eda0970af95 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 @@ -10,16 +10,33 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — live/full-size Windows -system-SSH contract audit, 2026-07-15, Codex implementation owner.** -Audit a purpose-named native Windows OpenSSH/full-size proof over the now-proven disconnected tree -composer before writing its RED suite or workflow fixture. Product callers, legacy upload/fallback/ -default behavior, tuple enablement, publication, and SignPath remain out of scope. +system-SSH exact-head native proof, 2026-07-15, Codex implementation owner.** +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only Windows optional-capability +server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, +serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown +contracts. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LOCAL-RED-001` proves the purpose suite skips without +native inputs, the existing workflow oracle passes 9/9, and the new case fails only because the +audited start step is absent. Add only start/measure/stop wiring, then require native x64/arm64 +evidence. Product callers, legacy upload/fallback/default behavior, tuple enablement, publication, +and SignPath remain out of scope. + +Pre-push fixture review found that native Windows `sshd.exe` may require the fixed `sshd` service +name. The audited contract now requires a stopped capability-owned service, exact image-path/start- +mode snapshot and restoration, and ownership-marked cleanup; it does not create/delete an arbitrary +custom service or touch an already-running server. + +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LOCAL-001` records the final local package green: 10/10 workflow +contracts with one honest live-suite skip, 259 focused passes with five skips, 694 broad relay passes +with ten skips, 283/283 release contracts, typecheck, full lint, 355-entry max-lines ratchet, +PowerShell syntax, formatting, isolation, and protected-resolver checks pass. Native x64/arm64 +capability, ACL, full-size, cancellation, and teardown proof remains required before this package +closes. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LOCAL-RED-001` proves the native workflow oracle passes 9/9 and the purpose suite fails solely because the audited production tree-composition module is absent. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LOCAL-001` records 8/8 purpose and 9/9 workflow-oracle cases, 258 focused passes with three declared skips, 694 broad relay passes with nine declared skips, 282/282 release contracts, typecheck, full lint, formatting, max-lines, diff, protected-resolver, and -no-product-consumer gates. Exact-head native CI remains required. +no-product-consumer gates. The following exact-head native evidence closes its CI gate. `E-M6-WINDOWS-SYSTEM-SSH-TREE-CI-001` closes that gate at exact implementation head `139bcd16d3`: all six primary native jobs, both Linux supplements, Windows x64 floor, PR Checks, Golden E2E, and computer-use pass. Windows arm64 retains only hosted build 26200 versus required 26100 after complete 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 e3de527110f..8e4ba286aaf 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: live/full-size Windows system-SSH contract audit**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **WINDOWS TREE COMPOSITION NATIVE PROOF CLOSED / LIVE WINDOWS OPENSSH AUDIT ACTIVE — 2026-07-15, Codex implementation owner** — `E-M6-WINDOWS-SYSTEM-SSH-TREE-CI-001` closes the disconnected composer at exact implementation head `139bcd16d36bfa6d0b8db16ab8f2f2223325a7f6`; all six primary native jobs and every adjacent gate pass except the declared Windows arm64 hosted build-26200 versus required-26100 floor check after complete runtime proof. Audit the first real full-size Windows OpenSSH package before writing RED tests or workflow fixture code. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: live/full-size Windows system-SSH exact-head native proof**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **LIVE WINDOWS OPENSSH FIXTURE LOCALLY GREEN / NATIVE X64+ARM64 PROOF NEXT — 2026-07-15, Codex implementation owner** — `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LOCAL-001` records the audited loopback fixture, purpose suite, workflow oracle, fixed-name service snapshot/restoration, focused/broad/release regression suites, typecheck, full lint, PowerShell parse, formatting, isolation, and protected-resolver gates locally green. The full-size live suite remains one honest local skip and earns no Windows claim. Commit and push the isolated package, then require exact-head `windows-2022` x64 and `windows-11-arm` arm64 logs before crediting capability installation/start, ACLs, PowerShell 5.1, full-size transfer, or teardown. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -159,10 +159,12 @@ same change as the work it records. - Completed package: the disconnected POSIX system-SSH tree/root composition is green locally and on all six primary Node 24 native clients under E-M6-POSIX-SYSTEM-SSH-TREE-LOCAL-001 and E-M6-POSIX-SYSTEM-SSH-TREE-CI-001. It has no live/full-size SSH or product caller. -- Active package: **Audit in progress — 2026-07-15, Codex implementation owner** — establish the - smallest purpose-named live/full-size loopback OpenSSH proof for the exact disconnected POSIX - system-SSH tree owner, initially on target-native Linux x64/arm64 runners. Do not add product, - settings, fallback, tuple, publication, default behavior, a semantic probe, or a tar fast path. +- Active package: **Exact-head native proof in progress — 2026-07-15, Codex implementation owner** — + the audited purpose suite, loopback workflow wiring, fixed-name service restoration, and every + local regression/isolation gate pass under `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LOCAL-001`. Commit + and push only this package, then collect native x64/arm64 capability, ACL, PowerShell 5.1, + full-size serial/four-channel/cancellation, and teardown evidence. Do not add product, settings, + fallback, tuple, publication, default behavior, or signing. - 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 @@ -18522,6 +18524,132 @@ src/main/ssh/ssh-remote-node-resolution.ts src/main/ssh/ssh-remote-node-resoluti launch, product/Beta/fallback/default behavior, tuple enablement, publication, Apple signing, or SignPath. Legacy remains the sole production/default path and every tuple stays disabled. +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001 — full-size native Windows OpenSSH proof contract + +- Date/owner/base: 2026-07-15, Codex implementation owner; audit-only package atop exact head + `8018152043a982f0d2c8d2907208858551c01176`. The published `windows-2022` and + `windows-11-arm` runner manifests identify the native OS/PowerShell images but do not promise an + installed OpenSSH Server. The fixture therefore queries `OpenSSH.Server~~~~0.0.1.0`, installs that + Windows capability only when absent, and fails closed if its native `sshd.exe` cannot validate or + start. It never substitutes a third-party server or credits a mocked/local-PowerShell-only path. +- Fixture ownership/authentication: create a purpose-named non-admin local account with a random + in-memory password used only to keep the Windows account enabled; authenticate SSH solely with a + freshly generated Ed25519 client key. Put the explicit `AuthorizedKeysFile`, host key, config, + PID/service metadata, and log under one fixture root. Remove inherited ACLs and grant only the + fixture user, LocalSystem, and built-in Administrators as required. Grant that account Modify only + on the fixture remote root. Never use `administrators_authorized_keys` or mutate the runner user's + profile/config. +- Server/trust/shell boundary: require the capability-owned `sshd` service to exist and be stopped; + fail without mutation if it is already running. Snapshot its exact image path and start mode, + temporarily point that fixed-name LocalSystem service at the fixture config, then restore both + values. This avoids assuming Windows `sshd.exe` can register under an arbitrary service name. The + custom config uses a dedicated port and `ListenAddress 127.0.0.1`; disables password, + keyboard-interactive, and challenge-response authentication; enables `StrictModes`; and allows + only the fixture account. Generate a unique server Ed25519 key and pin its exact public key in a + private `known_hosts`; readiness uses `BatchMode=yes`, `IdentitiesOnly=yes`, and + `StrictHostKeyChecking=yes`. Prove the remote command resolves Windows PowerShell 5.1 before any + runtime bytes. The production receiver remains the existing encoded `powershell.exe -NoProfile +-NonInteractive -ExecutionPolicy Bypass`/.NET stream implementation. +- Required measurement: on both native `windows-2022` x64 and `windows-11-arm` arm64 runtime jobs, + scan the exact target-native runtime, transfer its complete declared tree serially through the + default-one path and at maximum concurrency four, hash every transferred file locally through the + loopback runner, and assert exact path/file/byte counts. Record elapsed time, incremental RSS, + peak active files, OpenSSH/PowerShell/image/architecture identities, collision preservation, and + connection reuse. Abort the four-channel path after observed progress; require joined settlement + and owned-root removal below ten seconds, no later progress after 500 ms, and no leaked stage. +- Teardown: explicit root/account/service-mutation ownership markers prevent collision cleanup. The + `always()` step stops only the fixture-started run, waits for its PID/process tree, restores the + capability service's exact prior image path/start mode, deletes only the marked fixture account/ + profile/root, and prints bounded server logs. It may leave the Windows capability and its restored + stopped service installed on the disposable hosted runner, but must leave no account, key, stage, + connection, service mutation, or process owned by the test. +- RED/green sequencing and residual boundary: first add a suite that honestly skips without all + exact runner inputs and an oracle that fails solely for absent audited start/measure/stop steps. + Then add the fixture wiring. Native capability installation/start, ACL behavior, PowerShell 5.1, + full-size transfer, metrics, and teardown earn evidence only from exact-head x64 and arm64 logs. + This loopback Layer A proof does not cover external Windows hosts, long paths, endpoint protection, + high RTT, cross-client Layer B, remote verify/install/publish/launch, product/Beta/fallback/default + behavior, tuple enablement, publication, Apple signing, or SignPath. Legacy remains the sole + production/default path and every tuple stays disabled. + +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LOCAL-RED-001 — audited native fixture steps are absent + +- Date/owner/base: 2026-07-15, Codex implementation owner; uncommitted purpose-test, workflow-oracle, + and checklist package atop exact base `8018152043a982f0d2c8d2907208858551c01176` on Apple arm64, + macOS 26.2 build 25C56, Node 26.5.0, pnpm 10.24.0. Local execution cannot credit native Windows + OpenSSH or the full-size transfer. +- Format command: `pnpm exec oxfmt --write +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +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` + completes in 4.813 seconds. +- RED command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs`. Result: the purpose suite is one declared skip + because none of its exact runner inputs are present; the existing workflow oracle cases pass 9/9; + the new tenth case fails in 11 ms only at `expect(start).toBeDefined()` because + `Start loopback Windows OpenSSH system-SSH fixture` is absent. There is no production-module, + assertion, fixture-input, or adjacent failure. +- Disposition: add only the three audited native workflow steps and pin the purpose suite in the + Windows command family. Do not weaken the oracle, synthesize local evidence, substitute another + server, or claim x64/arm64 capability/ACL/transfer proof until exact-head Actions logs exist. No + product/Beta/fallback/default behavior, tuple, publication, or signing work is authorized. + +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LOCAL-001 — audited native fixture package is locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted purpose-test, + native-workflow, oracle, and checklist package atop exact base + `8018152043a982f0d2c8d2907208858551c01176` on Apple arm64, macOS 26.2 build 25C56, Node 26.5.0, + pnpm 10.24.0. Local Node is newer than the repository's Node 24 contract and macOS cannot execute + the Windows OpenSSH fixture, so exact-head native proof remains mandatory. +- Implementation: add no production module. The Windows runtime job now installs only the native + `OpenSSH.Server~~~~0.0.1.0` capability when absent, creates a non-admin/key-only fixture account, + applies SID-scoped ACLs, generates and pins exact client/host Ed25519 keys, and uses a loopback-only + custom config. It refuses an already-running capability service, snapshots the fixed `sshd` + service's exact image path/start mode, marks each owned mutation, temporarily starts it as + LocalSystem, proves Windows PowerShell 5.1 remotely, and restores the exact stopped service state + in `always()` teardown. The live suite transfers the exact runtime serially/default-one and at + four channels, validates all paths/sizes/hashes, preserves a collision, aborts only after progress, + requires cleanup/no-late-progress, records latency/RSS/channels, and proves connection reuse. +- Purpose/oracle command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs`. Result after the final service-restoration + correction: 10/10 workflow cases pass in 66 ms; the purpose suite is one declared local skip; + total Vitest duration is 572 ms. Independent YAML extraction plus PowerShell + `[scriptblock]::Create()` parses all three start/measure/stop blocks. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1` with the Windows live/tree/control/file, POSIX live/tree/control/file, + system-channel, source-tree/scan/stream, system-fallback, connection, and workflow-oracle suites. + Result: 13 files pass, two live files skip; 259 passed and five declared skips out of 264; 14.82 + seconds real, 178,028,544-byte maximum RSS, 97,670,928-byte peak footprint, zero swaps and block + I/O. The final service-only workflow correction is covered by the later 10/10 oracle rerun. +- Broad relay command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts`. Result: 53 files pass, six files skip; 694 passed + and ten declared skips out of 704; 47.05 seconds real, 303,251,456-byte maximum RSS, + 98,031,496-byte peak footprint, zero swaps and block I/O. +- Final release-contract command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs`. Result: 50 + files and 283/283 cases pass; 18.93 seconds real, 195,936,256-byte maximum RSS, 97,359,680-byte + peak footprint, zero swaps and block I/O. +- Static evidence: `pnpm typecheck` passes. Final `/usr/bin/time -l pnpm lint` passes in 19.25 + seconds real with 2,114,486,272-byte maximum RSS and 96,933,600-byte peak footprint; its 26 + warnings pre-exist outside this diff. Full lint proves switch exhaustiveness, 41 reliability gates, + the 355-entry max-lines ratchet with no new bypass, bundled guides, and localization checks. The + purpose test is 380 lines and adds no max-lines suppression. +- Isolation evidence: targeted `oxfmt --check`, `git diff --check`, protected resolver equality, and + an asserted non-test consumer search pass. The intended diff is limited to one purpose-named test, + three Windows native-workflow steps, one workflow oracle, and both checklist documents. No legacy + uploader, Electron/startup/product importer, settings, Beta mode, fallback, tuple, publication, + default-path, or signing behavior changes. +- Residual boundary: local execution proves only contracts and syntax, not Windows capability + availability/start, SID ACL behavior, real SSH authentication, Windows PowerShell 5.1 over SSH, + target-native full-size transfer, cancellation metrics, or service/account teardown. Both native + x64 and arm64 exact-head jobs must pass before this package closes. External Windows hosts, long + paths, endpoint protection, high RTT, cross-client Layer B, remote verify/install/publish/launch, + product/Beta/fallback/default behavior, tuple enablement, publication, Apple signing, and SignPath + remain open. Legacy remains the sole production/default path and every tuple stays disabled. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -18580,13 +18708,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Audit a purpose-named full-size Windows system-SSH proof against a real native Windows OpenSSH server. -Fix its exact fixture ownership, authentication/host-key trust, local account/ACLs, PowerShell 5.1/ -.NET receiver command, full runtime bytes/counts, serial/default and four-channel measurements, -cancellation settlement, collision/cleanup cases, x64/arm64 runner coverage, and teardown behavior -before writing a RED suite or workflow steps. Do not check the broad Milestone 6 Windows system-SSH -item yet. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple -enablement, release publication, or default behavior. +Commit and push only the locally green live Windows OpenSSH package, then collect exact-head +`windows-2022` x64 and `windows-11-arm` arm64 capability, ACL, PowerShell 5.1, exact full-size +serial/four-channel/cancellation, collision, connection-reuse, and teardown evidence. Do not check +the broad Milestone 6 Windows system-SSH item yet. Do not add an Electron/startup/product importer, +per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. diff --git a/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts b/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts new file mode 100644 index 00000000000..f9a09a07381 --- /dev/null +++ b/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts @@ -0,0 +1,380 @@ +import { createHash } from 'node:crypto' +import { lstat, open, opendir, readFile, rm } from 'node:fs/promises' +import { join } from 'node:path' +import { performance } from 'node:perf_hooks' + +import { describe, expect, it, vi } from 'vitest' + +vi.mock('electron', () => ({ app: { getAppPath: () => process.cwd() } })) + +import type { SshTarget } from '../../shared/ssh-types' +import { SshConnection } from './ssh-connection' +import { + scanSshRelayRuntimeSourceTree, + type SshRelayRuntimeScannedSourceTree +} from './ssh-relay-runtime-source-scan' +import type { SshRelayRuntimeSourceTree } from './ssh-relay-runtime-source-tree' +import { + SSH_RELAY_RUNTIME_SOURCE_STREAM_LIMITS, + type SshRelayRuntimeSourceStreamProgress +} from './ssh-relay-runtime-source-stream' +import { openSshRelayRuntimeSystemSshFileChannel } from './ssh-relay-runtime-system-ssh-file-channel' +import { runSshRelayRuntimeWindowsStagingControl } from './ssh-relay-runtime-windows-staging-control' +import { transferSshRelayRuntimeTreeViaWindowsSystemSsh } from './ssh-relay-runtime-windows-tree-transfer' + +type MeasurementIdentity = Pick< + SshRelayRuntimeSourceTree, + 'tupleId' | 'contentId' | 'os' | 'architecture' +> & { + entries: ( + | { path: string; type: 'directory'; mode: 0o755 } + | { + path: string + type: 'file' + role: SshRelayRuntimeSourceTree['files'][number]['role'] + size: number + mode: 0o644 | 0o755 + sha256: SshRelayRuntimeSourceTree['files'][number]['sha256'] + } + )[] + archive: { expandedSize: number; fileCount: number } +} + +type Measurement = { + result: T + elapsedMs: number + baselineRss: number + peakRss: number + incrementalRssBytes: number +} + +const host = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_HOST +const user = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_USER +const identityFile = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_IDENTITY +const remoteRoot = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_REMOTE_ROOT +const serverVersion = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_SERVER_VERSION +const powerShellVersion = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_POWERSHELL_VERSION +const runtimeRoot = process.env.ORCA_SSH_RELAY_FULL_SIZE_RUNTIME_ROOT +const identityPath = process.env.ORCA_SSH_RELAY_FULL_SIZE_IDENTITY +const port = Number.parseInt(process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_PORT ?? '', 10) +const hasLiveInput = Boolean( + host && + user && + identityFile && + remoteRoot && + serverVersion && + powerShellVersion && + runtimeRoot && + identityPath && + Number.isInteger(port) +) + +function parseIdentity(value: unknown): MeasurementIdentity { + if ( + typeof value !== 'object' || + value === null || + !('entries' in value) || + !Array.isArray(value.entries) || + !('archive' in value) || + typeof value.archive !== 'object' || + value.archive === null + ) { + throw new Error('Live Windows system-SSH measurement identity is incomplete') + } + return value as MeasurementIdentity +} + +function sourceTree(identity: MeasurementIdentity): SshRelayRuntimeSourceTree { + const directories = identity.entries + .filter((entry) => entry.type === 'directory') + .map((entry) => ({ + ...entry, + localPath: join(runtimeRoot as string, ...entry.path.split('/')) + })) + const files = identity.entries + .filter((entry) => entry.type === 'file') + .map((entry) => ({ + ...entry, + localPath: join(runtimeRoot as string, ...entry.path.split('/')) + })) + return Object.freeze({ + tupleId: identity.tupleId, + contentId: identity.contentId, + releaseTag: 'measurement-only', + os: identity.os, + architecture: identity.architecture, + runtimeRoot: runtimeRoot as string, + directories, + files, + fileCount: identity.archive.fileCount, + expandedBytes: identity.archive.expandedSize, + assertLeaseOwned: async () => {} + }) +} + +async function measure(operation: () => Promise): Promise> { + const baselineRss = process.memoryUsage().rss + let peakRss = baselineRss + const sample = (): void => { + peakRss = Math.max(peakRss, process.memoryUsage().rss) + } + const sampler = setInterval(sample, 1) + const startedAt = performance.now() + try { + const result = await operation() + sample() + return { + result, + elapsedMs: performance.now() - startedAt, + baselineRss, + peakRss, + incrementalRssBytes: Math.max(0, peakRss - baselineRss) + } + } finally { + clearInterval(sampler) + } +} + +async function hashFile(path: string): Promise { + const handle = await open(path, 'r') + const digest = createHash('sha256') + const buffer = Buffer.allocUnsafe(64 * 1024) + let position = 0 + try { + while (true) { + const { bytesRead } = await handle.read(buffer, 0, buffer.length, position) + if (bytesRead === 0) { + break + } + digest.update(buffer.subarray(0, bytesRead)) + position += bytesRead + } + } finally { + await handle.close() + } + return `sha256:${digest.digest('hex')}` +} + +async function collectRemotePaths(root: string): Promise { + const paths: string[] = [] + const pending = [{ localPath: root, relativePath: '' }] + while (pending.length > 0) { + const current = pending.pop()! + const directory = await opendir(current.localPath) + for await (const entry of directory) { + const relativePath = current.relativePath + ? `${current.relativePath}/${entry.name}` + : entry.name + paths.push(relativePath) + if (entry.isDirectory()) { + pending.push({ localPath: join(current.localPath, entry.name), relativePath }) + } + } + } + return paths.sort() +} + +async function validateTransferredTree( + tree: SshRelayRuntimeScannedSourceTree, + stage: string +): Promise { + const expectedPaths = [...tree.directories, ...tree.files].map((entry) => entry.path).sort() + // Why: the loopback runner can verify remote bytes without adding a second transfer mechanism. + expect(await collectRemotePaths(stage)).toEqual(expectedPaths) + for (const directory of tree.directories) { + expect((await lstat(join(stage, ...directory.path.split('/')))).isDirectory()).toBe(true) + } + for (const file of tree.files) { + const path = join(stage, ...file.path.split('/')) + const metadata = await lstat(path) + expect(metadata.isFile()).toBe(true) + expect(metadata.size).toBe(file.size) + expect(await hashFile(path)).toBe(file.sha256) + } +} + +function stagePath(name: string, identity: MeasurementIdentity): string { + return join(remoteRoot as string, `${name}-${identity.contentId.slice('sha256:'.length, 23)}`) +} + +function createProgressRecorder(): { + record: (progress: SshRelayRuntimeSourceStreamProgress) => void + snapshot: () => { bytes: number; updates: number; peakActiveFiles: number } +} { + let bytes = 0 + let updates = 0 + let peakActiveFiles = 0 + return { + record: (progress) => { + bytes = progress.bytesTransferred + updates += 1 + peakActiveFiles = Math.max(peakActiveFiles, progress.activeFiles) + }, + snapshot: () => ({ bytes, updates, peakActiveFiles }) + } +} + +async function transfer( + connection: SshConnection, + tree: SshRelayRuntimeScannedSourceTree, + stage: string, + signal: AbortSignal, + onProgress: (progress: SshRelayRuntimeSourceStreamProgress) => void, + maximumConcurrency?: number +) { + return transferSshRelayRuntimeTreeViaWindowsSystemSsh({ + connection, + tree, + remoteStagingRoot: stage, + signal, + onProgress, + ...(maximumConcurrency === undefined ? {} : { maximumConcurrency }) + }) +} + +describe.skipIf(!hasLiveInput)( + 'SSH relay full-size Windows system-SSH transfer through native OpenSSH', + () => { + it( + 'preserves exact bytes with bounded serial, concurrent, collision, and cancellation behavior', + { timeout: 20 * 60_000 }, + async () => { + expect(process.platform).toBe('win32') + expect(process.env.ORCA_SSH_FORCE_SYSTEM_TRANSPORT).toBe('1') + expect(powerShellVersion).toMatch(/^5\.1\./u) + const identity = parseIdentity(JSON.parse(await readFile(identityPath as string, 'utf8'))) + expect(identity.os).toBe('win32') + const tree = await scanSshRelayRuntimeSourceTree( + sourceTree(identity), + new AbortController().signal + ) + const target: SshTarget = { + id: 'live-windows-system-ssh-measurement', + label: 'live-windows-system-ssh-measurement', + host: host as string, + port, + username: user as string, + identityFile: identityFile as string, + identitiesOnly: true, + systemSshConnectionReuse: true, + source: 'manual' + } + const connection = new SshConnection(target, { onStateChange: () => {} }) + const serialStage = stagePath('system-serial', identity) + const concurrentStage = stagePath('system-concurrent', identity) + const cancelledStage = stagePath('system-cancelled', identity) + const stages = [serialStage, concurrentStage, cancelledStage] + await Promise.all(stages.map((stage) => rm(stage, { recursive: true, force: true }))) + + try { + await connection.connect() + expect(connection.usesSystemSshTransport()).toBe(true) + + const serialProgress = createProgressRecorder() + const serial = await measure(() => + transfer( + connection, + tree, + serialStage, + new AbortController().signal, + serialProgress.record + ) + ) + await validateTransferredTree(tree, serialStage) + expect(serialProgress.snapshot().peakActiveFiles).toBe(1) + + const concurrentProgress = createProgressRecorder() + const concurrent = await measure(() => + transfer( + connection, + tree, + concurrentStage, + new AbortController().signal, + concurrentProgress.record, + 4 + ) + ) + await validateTransferredTree(tree, concurrentStage) + expect(concurrentProgress.snapshot().peakActiveFiles).toBeGreaterThan(1) + expect(concurrentProgress.snapshot().peakActiveFiles).toBeLessThanOrEqual(4) + await expect( + transfer(connection, tree, concurrentStage, new AbortController().signal, () => {}) + ).rejects.toBeTruthy() + await validateTransferredTree(tree, concurrentStage) + + const controller = new AbortController() + const cancelledProgress = createProgressRecorder() + let abortRequestedAt = 0 + const cancelled = await measure(async () => { + const outcome = transfer( + connection, + tree, + cancelledStage, + controller.signal, + (progress) => { + cancelledProgress.record(progress) + if (progress.bytesTransferred > 0 && !controller.signal.aborted) { + abortRequestedAt = performance.now() + controller.abort(new Error('live Windows system-SSH cancellation')) + } + }, + 4 + ) + await expect(outcome).rejects.toThrow('live Windows system-SSH cancellation') + return performance.now() - abortRequestedAt + }) + const updatesAfterSettlement = cancelledProgress.snapshot().updates + await expect(lstat(cancelledStage)).rejects.toMatchObject({ code: 'ENOENT' }) + await new Promise((resolve) => setTimeout(resolve, 500)) + await expect(lstat(cancelledStage)).rejects.toMatchObject({ code: 'ENOENT' }) + expect(cancelledProgress.snapshot().updates).toBe(updatesAfterSettlement) + + await runSshRelayRuntimeWindowsStagingControl({ + operation: 'remove-root', + remoteRoot: cancelledStage, + signal: new AbortController().signal, + openChannel: (command, signal) => + openSshRelayRuntimeSystemSshFileChannel(connection, command, signal) + }) + expect(connection.getState().status).toBe('connected') + + const metrics = { + tupleId: identity.tupleId, + contentId: identity.contentId, + files: tree.fileCount, + bytes: tree.expandedBytes, + serverVersion, + powerShellVersion, + runnerImage: process.env.ImageOS, + runnerVersion: process.env.ImageVersion, + runnerArchitecture: process.env.RUNNER_ARCH, + serialElapsedMs: serial.elapsedMs, + serialIncrementalRssBytes: serial.incrementalRssBytes, + serialPeakActiveFiles: serialProgress.snapshot().peakActiveFiles, + concurrentElapsedMs: concurrent.elapsedMs, + concurrentIncrementalRssBytes: concurrent.incrementalRssBytes, + concurrentPeakActiveFiles: concurrentProgress.snapshot().peakActiveFiles, + cancellationSettlementMs: cancelled.result, + cancellationIncrementalRssBytes: cancelled.incrementalRssBytes, + cancellationProgressUpdates: cancelledProgress.snapshot().updates + } + console.log(`ssh_relay_live_windows_system_ssh=${JSON.stringify(metrics)}`) + expect(serial.result.bytesTransferred).toBe(tree.expandedBytes) + expect(concurrent.result.bytesTransferred).toBe(tree.expandedBytes) + expect(serialProgress.snapshot().bytes).toBe(tree.expandedBytes) + expect(concurrentProgress.snapshot().bytes).toBe(tree.expandedBytes) + expect(serial.incrementalRssBytes).toBeLessThanOrEqual( + SSH_RELAY_RUNTIME_SOURCE_STREAM_LIMITS.maximumIncrementalMemoryBytes + ) + expect(concurrent.incrementalRssBytes).toBeLessThanOrEqual( + SSH_RELAY_RUNTIME_SOURCE_STREAM_LIMITS.maximumIncrementalMemoryBytes + ) + expect(cancelled.result).toBeLessThan(10_000) + } finally { + await connection.disconnect() + await Promise.all(stages.map((stage) => rm(stage, { recursive: true, force: true }))) + } + } + ) + } +) From bcbd9d6b36dc8cc2e917fa88eb30f87ab1d88657 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 17:59:55 -0400 Subject: [PATCH 240/267] test(ssh): make Windows live fixture portable Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 132 ++++++------ .../ssh-relay-runtime-workflow.test.mjs | 39 +++- ...elease-implementation-checklist-summary.md | 39 +++- ...github-release-implementation-checklist.md | 194 +++++++++++++++--- ...ndows-system-ssh-openssh-full-size.test.ts | 4 + 5 files changed, 306 insertions(+), 102 deletions(-) diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index a18ea57249a..e0a87217bd1 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -1314,26 +1314,6 @@ jobs: shell: pwsh run: | $ErrorActionPreference = 'Stop' - $capabilityName = 'OpenSSH.Server~~~~0.0.1.0' - $capability = Get-WindowsCapability -Online -Name $capabilityName - if ($capability.State -ne 'Installed') { - # Why: use the target OS's native server on both architectures; substitutes earn no proof. - Add-WindowsCapability -Online -Name $capabilityName | Out-Null - $capability = Get-WindowsCapability -Online -Name $capabilityName - } - if ($capability.State -ne 'Installed') { - throw "Windows OpenSSH Server capability is unavailable: $($capability.State)" - } - - $openSshRoot = Join-Path $env:WINDIR 'System32/OpenSSH' - $sshd = Join-Path $openSshRoot 'sshd.exe' - $ssh = Join-Path $openSshRoot 'ssh.exe' - $sshKeygen = Join-Path $openSshRoot 'ssh-keygen.exe' - foreach ($binary in @($sshd, $ssh, $sshKeygen)) { - if (-not (Test-Path -LiteralPath $binary -PathType Leaf)) { - throw "Windows OpenSSH capability is missing $binary" - } - } $windowsPowerShell = Join-Path $env:WINDIR 'System32/WindowsPowerShell/v1.0/powershell.exe' if (-not (Test-Path -LiteralPath $windowsPowerShell -PathType Leaf)) { throw 'Windows PowerShell 5.1 executable is missing' @@ -1350,14 +1330,67 @@ jobs: $rootMarker = Join-Path $fixture 'fixture-owned' New-Item -ItemType File -Path $rootMarker | Out-Null + $serviceName = 'sshd' + if (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) { + throw 'Windows OpenSSH fixed service name is already registered' + } + $releaseTag = '10.0.0.0p2-Preview' + $archiveName, $expandedName, $expectedArchiveSha256 = switch ($env:RUNNER_ARCH) { + 'X64' { + 'OpenSSH-Win64.zip' + 'OpenSSH-Win64' + '23f50f3458c4c5d0b12217c6a5ddfde0137210a30fa870e98b29827f7b43aba5' + } + 'ARM64' { + 'OpenSSH-ARM64.zip' + 'OpenSSH-ARM64' + '698c6aec31c1dd0fb996206e8741f4531a97355686b5431ef347d531b07fcd42' + } + default { throw "Unsupported Windows OpenSSH fixture architecture: $env:RUNNER_ARCH" } + } + $archive = Join-Path $fixture $archiveName + $releaseUrl = "https://github.com/PowerShell/Win32-OpenSSH/releases/download/$releaseTag/$archiveName" + # Why: this provisions only the CI SSH server; product remotes never perform HTTP downloads. + & curl.exe -fL --connect-timeout 20 --max-time 300 --retry 2 -o $archive $releaseUrl + if ($LASTEXITCODE -ne 0) { throw 'Official Windows OpenSSH fixture download failed' } + $archiveSha256 = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant() + if ($archiveSha256 -ne $expectedArchiveSha256) { + throw "Official Windows OpenSSH fixture archive hash mismatch: $archiveSha256" + } + $portableRoot = Join-Path $fixture 'portable' + Expand-Archive -LiteralPath $archive -DestinationPath $portableRoot + $openSshRoot = Join-Path $portableRoot $expandedName + $nativeFiles = @(Get-ChildItem -LiteralPath $openSshRoot -Recurse -File | Where-Object { + $_.Extension -in @('.exe', '.dll') + }) + if ($nativeFiles.Count -lt 10) { throw 'Official Windows OpenSSH fixture is incomplete' } + foreach ($nativeFile in $nativeFiles) { + $signature = Get-AuthenticodeSignature -LiteralPath $nativeFile.FullName + if ( + $signature.Status -ne 'Valid' -or + $signature.SignerCertificate.Subject -notmatch 'CN=Microsoft Corporation' + ) { + throw "Official Windows OpenSSH fixture signature is invalid: $($nativeFile.Name)" + } + } + $sshd = Join-Path $openSshRoot 'sshd.exe' + $ssh = Join-Path $openSshRoot 'ssh.exe' + $sshKeygen = Join-Path $openSshRoot 'ssh-keygen.exe' + foreach ($binary in @($sshd, $ssh, $sshKeygen)) { + if (-not (Test-Path -LiteralPath $binary -PathType Leaf)) { + throw "Official Windows OpenSSH fixture is missing $binary" + } + } + $fixtureUser = 'orca_ssh_fixture' if (Get-LocalUser -Name $fixtureUser -ErrorAction SilentlyContinue) { throw "Windows OpenSSH fixture account already exists: $fixtureUser" } $password = ConvertTo-SecureString "$([Guid]::NewGuid().ToString('N'))aA!7" -AsPlainText -Force + # Why: collision rejection makes this marker safe to use for partial-creation cleanup. + New-Item -ItemType File -Path (Join-Path $fixture 'account-owned') | Out-Null $account = New-LocalUser -Name $fixtureUser -Password $password -PasswordNeverExpires ` -UserMayNotChangePassword -AccountNeverExpires - New-Item -ItemType File -Path (Join-Path $fixture 'account-owned') | Out-Null $userSid = $account.SID.Value function Set-FixtureAcl([string]$Path, [string[]]$Grants) { @@ -1367,6 +1400,10 @@ jobs: & icacls.exe @arguments | Out-Host if ($LASTEXITCODE -ne 0) { throw "Failed to grant fixture ACLs on $Path" } } + function Set-FixtureOwner([string]$Path, [string]$OwnerSid) { + & icacls.exe $Path /setowner "*$OwnerSid" | Out-Host + if ($LASTEXITCODE -ne 0) { throw "Failed to set fixture owner on $Path" } + } $systemFull = '*S-1-5-18:(OI)(CI)F' $administratorsFull = '*S-1-5-32-544:(OI)(CI)F' Set-FixtureAcl $fixture @($systemFull, $administratorsFull, "*$userSid`:RX") @@ -1385,7 +1422,9 @@ jobs: '*S-1-5-32-544:F', "*$userSid`:R" ) + Set-FixtureOwner $authorizedKeys $userSid Set-FixtureAcl $hostKey @('*S-1-5-18:F', '*S-1-5-32-544:F') + Set-FixtureOwner $hostKey 'S-1-5-18' $port = 22224 $knownHosts = Join-Path $clientSsh 'known_hosts' @@ -1426,27 +1465,11 @@ jobs: & $sshd -t -f $config if ($LASTEXITCODE -ne 0) { throw 'Windows OpenSSH fixture config validation failed' } - $serviceName = 'sshd' - $service = Get-CimInstance Win32_Service -Filter "Name='$serviceName'" -ErrorAction SilentlyContinue - if (-not $service) { throw 'Windows OpenSSH capability did not register the sshd service' } - if ($service.State -ne 'Stopped') { - throw "Windows OpenSSH service must be stopped before the fixture: $($service.State)" - } - $serviceBackup = [ordered]@{ - pathName = $service.PathName - startMode = $service.StartMode - } | ConvertTo-Json -Compress - $serviceBackupPath = Join-Path $fixture 'service-backup.json' - [IO.File]::WriteAllText( - $serviceBackupPath, - "$serviceBackup`n", - [Text.UTF8Encoding]::new($false) - ) - # Why: mark the reversible service mutation before attempting it so failure still restores. - New-Item -ItemType File -Path (Join-Path $fixture 'service-owned') | Out-Null $binaryPath = "`"$sshd`" -f `"$config`" -E `"$log`"" - & sc.exe config $serviceName binPath= $binaryPath start= demand | Out-Host - if ($LASTEXITCODE -ne 0) { throw 'Windows OpenSSH fixture service configuration failed' } + # Why: collision rejection makes this marker safe to use for partial-creation cleanup. + New-Item -ItemType File -Path (Join-Path $fixture 'service-owned') | Out-Null + New-Service -Name $serviceName -DisplayName 'Orca live Windows OpenSSH fixture' ` + -BinaryPathName $binaryPath -StartupType Manual | Out-Null Start-Service -Name $serviceName $ready = $false @@ -1479,12 +1502,14 @@ jobs: "ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_IDENTITY=$clientKey", "ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_REMOTE_ROOT=$remoteRootSshPath", "ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_SERVER_VERSION=$serverVersion", + "ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_ARCHIVE_SHA256=$archiveSha256", "ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_POWERSHELL_VERSION=$remotePowerShellVersion", "ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_CLIENT_HOME=$clientHome", "ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_SERVICE=$serviceName", "ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_ACCOUNT=$fixtureUser" ) | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append "windows_openssh_server=$serverVersion" + "windows_openssh_archive_sha256=$archiveSha256" "windows_powershell=$remotePowerShellVersion" - name: Measure exact full-size runtime over Windows system SSH @@ -1531,24 +1556,15 @@ jobs: } } } - if ($ownsService) { - $backupPath = Join-Path $fixture 'service-backup.json' - $backup = Get-Content -LiteralPath $backupPath -Raw | ConvertFrom-Json - $startToken = switch ($backup.startMode) { - 'Auto' { 'auto' } - 'Manual' { 'demand' } - 'Disabled' { 'disabled' } - default { throw "Unsupported original sshd start mode: $($backup.startMode)" } + if ($ownsService -and $service) { + & sc.exe delete $serviceName | Out-Host + if ($LASTEXITCODE -ne 0) { throw 'Windows OpenSSH fixture service deletion failed' } + for ($attempt = 1; $attempt -le 20; $attempt += 1) { + if (-not (Get-Service -Name $serviceName -ErrorAction SilentlyContinue)) { break } + Start-Sleep -Milliseconds 100 } - & sc.exe config $serviceName binPath= $backup.pathName start= $startToken | Out-Host - if ($LASTEXITCODE -ne 0) { throw 'Windows OpenSSH service restoration failed' } - $restored = Get-CimInstance Win32_Service -Filter "Name='$serviceName'" -ErrorAction Stop - if ( - $restored.State -ne 'Stopped' -or - $restored.PathName -ne $backup.pathName -or - $restored.StartMode -ne $backup.startMode - ) { - throw 'Windows OpenSSH service did not return to its exact prior state' + if (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) { + throw 'Windows OpenSSH fixture service remains registered' } } } catch { $failures.Add("service teardown: $($_.Exception.Message)") } diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 9492a17bfdd..d3161d4854d 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -157,17 +157,32 @@ describe('SSH relay runtime artifact workflow', () => { expect(measure).toBeDefined() expect(stop).toBeDefined() expect(start.shell).toBe('pwsh') - expect(start.run).toContain('OpenSSH.Server~~~~0.0.1.0') - expect(start.run).toContain('Add-WindowsCapability') + expect(start.run).toContain('10.0.0.0p2-Preview') + expect(start.run).toContain('23f50f3458c4c5d0b12217c6a5ddfde0137210a30fa870e98b29827f7b43aba5') + expect(start.run).toContain('698c6aec31c1dd0fb996206e8741f4531a97355686b5431ef347d531b07fcd42') + expect(start.run).toContain('--connect-timeout 20 --max-time 300 --retry 2') + expect(start.run).toContain('Get-AuthenticodeSignature') + expect(start.run).toContain("$signature.Status -ne 'Valid'") + expect(start.run).toContain('CN=Microsoft Corporation') + expect(start.run).not.toContain('Add-WindowsCapability') expect(start.run).toContain('New-LocalUser') + expect(start.run.indexOf("Join-Path $fixture 'account-owned'")).toBeLessThan( + start.run.indexOf('New-LocalUser') + ) expect(start.run).toContain('ListenAddress 127.0.0.1') expect(start.run).toContain('PasswordAuthentication no') expect(start.run).toContain('StrictModes yes') expect(start.run).toContain('StrictHostKeyChecking=yes') expect(start.run).toContain('powershell.exe') expect(start.run).toContain('([Version]$remotePowerShellVersion).Major -ne 5') + expect(start.run).toContain('/setowner') + expect(start.run).toContain("Set-FixtureOwner $hostKey 'S-1-5-18'") + expect(start.run).toContain('Set-FixtureOwner $authorizedKeys $userSid') expect(start.run).not.toContain('administrators_authorized_keys') - expect(start.run).not.toContain('New-Service') + expect(start.run).toContain('New-Service') + expect(start.run.indexOf("Join-Path $fixture 'service-owned'")).toBeLessThan( + start.run.indexOf('New-Service') + ) expect(measure.shell).toBe('pwsh') expect(measure.run).toContain("$env:ORCA_SSH_FORCE_SYSTEM_TRANSPORT = '1'") expect(measure.run).toContain('ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts') @@ -177,9 +192,9 @@ describe('SSH relay runtime artifact workflow', () => { expect(stop.run).toContain('fixture-owned') expect(stop.run).toContain('service-owned') expect(stop.run).toContain('account-owned') - expect(stop.run).toContain('service-backup.json') - expect(stop.run).toContain('sc.exe config') - expect(stop.run).not.toContain('sc.exe delete') + expect(stop.run).not.toContain('service-backup.json') + expect(stop.run).not.toContain('sc.exe config') + expect(stop.run).toContain('sc.exe delete') expect(stop.run).toContain('Remove-LocalUser') }) @@ -430,7 +445,9 @@ describe('SSH relay runtime artifact workflow', () => { windowsRun.indexOf("throw 'runtime reproducibility verification failed'") ) expect(steps[uploadIndex].with.path).toBe('runtime-evidence/${{ matrix.tuple }}/') - expect(source).not.toMatch(/releases\/|gh release|contents:\s*write/i) + expect(source).not.toMatch( + /github\.com\/stablyai\/orca\/releases\/|gh release|contents:\s*write/i + ) }) it('assesses and stages real first-build candidates without signing authority', async () => { @@ -514,7 +531,9 @@ describe('SSH relay runtime artifact workflow', () => { expect(normalizedContainerfile).toContain('python39') expect(normalizedContainerfile).toContain('NODE_GYP_FORCE_PYTHON=/usr/bin/python3.9') expect(normalizedContainerfile).toContain(' which \\\n') - expect(source).not.toMatch(/releases\/|gh release|contents:\s*write/i) + expect(source).not.toMatch( + /github\.com\/stablyai\/orca\/releases\/|gh release|contents:\s*write/i + ) }) it('separates qualifying Windows floors from supplemental Linux userland evidence', async () => { @@ -577,6 +596,8 @@ describe('SSH relay runtime artifact workflow', () => { expect(windowsRun).toContain('--scope full') expect(windowsRun).toContain('$identities.Count -ne 1') expect(windowsRun).toContain('$archives.Count -ne 1') - expect(source).not.toMatch(/releases\/|gh release|contents:\s*write/i) + expect(source).not.toMatch( + /github\.com\/stablyai\/orca\/releases\/|gh release|contents:\s*write/i + ) }) }) 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 eda0970af95..b5c49a804f4 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 @@ -9,9 +9,15 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — live/full-size Windows -system-SSH exact-head native proof, 2026-07-15, Codex implementation owner.** -`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only Windows optional-capability +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — bounded official +Windows OpenSSH fixture correction, 2026-07-15, Codex implementation owner.** +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture +correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned +Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain +forbidden; 10/10 focused workflow contracts, 283/283 release contracts, 694 relay cases with ten +declared skips, typecheck, full lint/reliability/max-lines, PowerShell syntax, formatting, diff, and +protected-resolver isolation pass. Fresh exact-head x64/arm64 proof is still required. +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown contracts. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LOCAL-RED-001` proves the purpose suite skips without @@ -20,10 +26,8 @@ audited start step is absent. Add only start/measure/stop wiring, then require n evidence. Product callers, legacy upload/fallback/default behavior, tuple enablement, publication, and SignPath remain out of scope. -Pre-push fixture review found that native Windows `sshd.exe` may require the fixed `sshd` service -name. The audited contract now requires a stopped capability-owned service, exact image-path/start- -mode snapshot and restoration, and ownership-marked cleanup; it does not create/delete an arbitrary -custom service or touch an already-running server. +Pre-push fixture review found that native Windows `sshd.exe` requires the fixed `sshd` service name. +The corrected audit uses only a collision-checked, ownership-marked fixed-name fixture service. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LOCAL-001` records the final local package green: 10/10 workflow contracts with one honest live-suite skip, 259 focused passes with five skips, 694 broad relay passes @@ -31,6 +35,27 @@ with ten skips, 283/283 release contracts, typecheck, full lint, 355-entry max-l PowerShell syntax, formatting, isolation, and protected-resolver checks pass. Native x64/arm64 capability, ACL, full-size, cancellation, and teardown proof remains required before this package closes. + +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-CI-RED-001` records exact-head x64 job `87745066045`: every +prior build/runtime boundary and native capability/config step passes, but Windows `sshd` rejects +the generated host private key because its owner remains the runner account despite narrow SYSTEM/ +Administrators ACL entries. Ownership-marked teardown restores the stock service and passes. Set +that key's owner to LocalSystem and `authorized_keys` owner to the fixture user, without broadening +ACLs or changing production behavior, then rerun both native architectures. + +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-OWNER-CORRECTION-LOCAL-001` records that exact owner correction +locally green: 10/10 workflow and 283/283 release contracts, PowerShell syntax, targeted lint, +formatting, diff, and protected-resolver checks pass; the live suite remains one honest local skip. +Preserve the original arm64 result, then push for fresh x64/arm64 proof. + +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-ARM64-CAPABILITY-CI-RED-001` records arm64 job `87745066052`: +all prior native build/runtime gates pass, but `Add-WindowsCapability OpenSSH.Server` emits no result +for 19m16s and the existing 30-minute job cancels it; ownership-safe teardown still passes. Do not +increase the timeout. Provision the same immutable official Microsoft Win32-OpenSSH release on both +architectures with per-architecture SHA-256 pins, target-native Authenticode, bounded download, +fixed-name service ownership/deletion, and no production or remote-host HTTP behavior. +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` closes the local correction gate; +push its exact head and require both native architectures before closing the live Windows package. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LOCAL-RED-001` proves the native workflow oracle passes 9/9 and the purpose suite fails solely because the audited production tree-composition module is absent. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LOCAL-001` records 8/8 purpose and 9/9 workflow-oracle cases, 258 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 8e4ba286aaf..fc4ab49d077 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: live/full-size Windows system-SSH exact-head native proof**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **LIVE WINDOWS OPENSSH FIXTURE LOCALLY GREEN / NATIVE X64+ARM64 PROOF NEXT — 2026-07-15, Codex implementation owner** — `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LOCAL-001` records the audited loopback fixture, purpose suite, workflow oracle, fixed-name service snapshot/restoration, focused/broad/release regression suites, typecheck, full lint, PowerShell parse, formatting, isolation, and protected-resolver gates locally green. The full-size live suite remains one honest local skip and earns no Windows claim. Commit and push the isolated package, then require exact-head `windows-2022` x64 and `windows-11-arm` arm64 logs before crediting capability installation/start, ACLs, PowerShell 5.1, full-size transfer, or teardown. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: bounded official Windows OpenSSH fixture correction**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **PINNED MICROSOFT FIXTURE LOCALLY GREEN / EXACT-HEAD NATIVE PROOF NEXT — 2026-07-15, Codex implementation owner** — exact-head run `29535299209` proves x64 needs explicit host-key ownership and the hosted ARM64 image cannot install `OpenSSH.Server~~~~0.0.1.0` within the 30-minute job. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the local correction that replaces that OS-capability dependency on both architectures with one immutable official Microsoft Win32-OpenSSH release, per-architecture SHA-256 pins, target-native Authenticode checks, bounded download/extraction, explicit key owners, and an ownership-marked fixed-name `sshd` service removed in teardown. The workflow oracle permits only that exact external Microsoft fixture release URL while continuing to forbid Orca release URLs, `gh release`, and `contents: write`. This is CI fixture provisioning only; production runtime bytes still cross authenticated SSH and no product remote downloads anything. Push the isolated correction and require fresh exact-head x64/arm64 proof. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -159,11 +159,12 @@ same change as the work it records. - Completed package: the disconnected POSIX system-SSH tree/root composition is green locally and on all six primary Node 24 native clients under E-M6-POSIX-SYSTEM-SSH-TREE-LOCAL-001 and E-M6-POSIX-SYSTEM-SSH-TREE-CI-001. It has no live/full-size SSH or product caller. -- Active package: **Exact-head native proof in progress — 2026-07-15, Codex implementation owner** — - the audited purpose suite, loopback workflow wiring, fixed-name service restoration, and every - local regression/isolation gate pass under `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LOCAL-001`. Commit - and push only this package, then collect native x64/arm64 capability, ACL, PowerShell 5.1, - full-size serial/four-channel/cancellation, and teardown evidence. Do not add product, settings, +- Active package: **Official portable fixture correction in progress — 2026-07-15, Codex + implementation owner** — the ARM64 capability install times out under + `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-ARM64-CAPABILITY-CI-RED-001`. Provision the same exact official + Microsoft Win32-OpenSSH release on x64/arm64 with pinned archive hashes and target-native + Authenticode checks, create/delete only an ownership-marked fixed-name `sshd` service, retain the + explicit key owners, and rerun local plus both native architectures. Do not add product, settings, fallback, tuple, publication, default behavior, or signing. - 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 @@ -18527,11 +18528,15 @@ src/main/ssh/ssh-remote-node-resolution.ts src/main/ssh/ssh-remote-node-resoluti ### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001 — full-size native Windows OpenSSH proof contract - Date/owner/base: 2026-07-15, Codex implementation owner; audit-only package atop exact head - `8018152043a982f0d2c8d2907208858551c01176`. The published `windows-2022` and - `windows-11-arm` runner manifests identify the native OS/PowerShell images but do not promise an - installed OpenSSH Server. The fixture therefore queries `OpenSSH.Server~~~~0.0.1.0`, installs that - Windows capability only when absent, and fails closed if its native `sshd.exe` cannot validate or - start. It never substitutes a third-party server or credits a mocked/local-PowerShell-only path. + `8018152043a982f0d2c8d2907208858551c01176`, corrected after native evidence from run + `29535299209`. The published `windows-2022` and `windows-11-arm` runner manifests identify native + OS/PowerShell images but do not promise an installed OpenSSH Server; ARM64 capability installation + later consumed 19m16s until job timeout. The fixture therefore downloads one exact official + Microsoft Win32-OpenSSH release before starting the loopback host, pins the architecture-specific + archive SHA-256, verifies every extracted native PE with target-native Authenticode, and fails + closed on timeout/hash/signature/shape/start failure. This is CI infrastructure provisioning, not + the production runtime/bootstrap path or a remote-host egress requirement. No third-party server, + mutable latest lookup, mock, or local-PowerShell-only path earns proof. - Fixture ownership/authentication: create a purpose-named non-admin local account with a random in-memory password used only to keep the Windows account enabled; authenticate SSH solely with a freshly generated Ed25519 client key. Put the explicit `AuthorizedKeysFile`, host key, config, @@ -18539,11 +18544,11 @@ src/main/ssh/ssh-remote-node-resolution.ts src/main/ssh/ssh-remote-node-resoluti fixture user, LocalSystem, and built-in Administrators as required. Grant that account Modify only on the fixture remote root. Never use `administrators_authorized_keys` or mutate the runner user's profile/config. -- Server/trust/shell boundary: require the capability-owned `sshd` service to exist and be stopped; - fail without mutation if it is already running. Snapshot its exact image path and start mode, - temporarily point that fixed-name LocalSystem service at the fixture config, then restore both - values. This avoids assuming Windows `sshd.exe` can register under an arbitrary service name. The - custom config uses a dedicated port and `ListenAddress 127.0.0.1`; disables password, +- Server/trust/shell boundary: require the fixed service name `sshd` to be absent; fail without + mutation on collision. Register only that fixture-owned LocalSystem service against the verified + portable binary/config, mark ownership before start, and delete it after PID settlement. This + avoids assuming Windows `sshd.exe` can register under an arbitrary service name. The custom config + uses a dedicated port and `ListenAddress 127.0.0.1`; disables password, keyboard-interactive, and challenge-response authentication; enables `StrictModes`; and allows only the fixture account. Generate a unique server Ed25519 key and pin its exact public key in a private `known_hosts`; readiness uses `BatchMode=yes`, `IdentitiesOnly=yes`, and @@ -18557,12 +18562,10 @@ src/main/ssh/ssh-remote-node-resolution.ts src/main/ssh/ssh-remote-node-resoluti peak active files, OpenSSH/PowerShell/image/architecture identities, collision preservation, and connection reuse. Abort the four-channel path after observed progress; require joined settlement and owned-root removal below ten seconds, no later progress after 500 ms, and no leaked stage. -- Teardown: explicit root/account/service-mutation ownership markers prevent collision cleanup. The - `always()` step stops only the fixture-started run, waits for its PID/process tree, restores the - capability service's exact prior image path/start mode, deletes only the marked fixture account/ - profile/root, and prints bounded server logs. It may leave the Windows capability and its restored - stopped service installed on the disposable hosted runner, but must leave no account, key, stage, - connection, service mutation, or process owned by the test. +- Teardown: explicit root/account/service ownership markers prevent collision cleanup. The + `always()` step stops only the fixture-started run, waits for its PID/process tree, deletes only the + marked fixed-name service/account/profile/root, and prints bounded server logs. It must leave no + account, key, stage, connection, registered service, or process owned by the test. - RED/green sequencing and residual boundary: first add a suite that honestly skips without all exact runner inputs and an oracle that fails solely for absent audited start/measure/stop steps. Then add the fixture wiring. Native capability installation/start, ACL behavior, PowerShell 5.1, @@ -18650,6 +18653,139 @@ config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.m product/Beta/fallback/default behavior, tuple enablement, publication, Apple signing, and SignPath remain open. Legacy remains the sole production/default path and every tuple stays disabled. +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-CI-RED-001 — native x64 rejects implicit host-key ownership + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact head + `0a85f88003fee626cbeb91d9ed65880e4b566991` on draft PR #8741, artifact run + `29535299209`, native Windows x64 job `87745066045` (`windows-2022`). The run remains in progress + while arm64 executes; this entry credits only the completed x64 RED and successful teardown. +- Prior-boundary proof: setup, exact checkout, native MSVC/Node/pnpm, all runtime artifact contracts, + exact Node downloads, two clean runtime builds/equality/smoke, and full-size desktop extraction/ + cache measurements pass. The new start step installs the OS OpenSSH Server capability, creates the + non-admin account/keys/ACLs, validates the custom `sshd_config`, and successfully applies the + reversible fixed-name service configuration. These facts do not credit a live SSH transfer. +- Exact RED: `Start-Service sshd` fails after 84 seconds. The service log says + `Permissions for 'D:/a/_temp/orca-live-windows-system-ssh/host-key' are too open`, + `Unable to load host key ...: bad permissions`, and `sshd: no hostkeys available -- exiting`. + The file already has inheritance removed and only LocalSystem plus built-in Administrators full + access, but its owner remains the runner account that generated it. Windows OpenSSH host-key trust + requires the private-key owner itself to be an accepted service principal, not only narrow ACEs. +- Cleanup proof: the `always()` teardown passes, restores the capability-owned `sshd` service's exact + prior image path/start mode, removes only ownership-marked account/root resources, and leaves no + fixture service process. The measure/upload steps correctly skip; no collision or unrelated state + is deleted. +- Correction/residual boundary: explicitly set the host private-key owner to LocalSystem after + removing inheritance and set `authorized_keys` ownership to the non-admin fixture SID before its + first authentication attempt. Do not broaden either ACL, weaken `StrictModes`, use the admin key + file, substitute a server, or change any production code. Rerun the local oracle/static gates and + require fresh native x64 plus arm64 full-size evidence. This RED proves neither live auth nor + transfer and authorizes no product/Beta/fallback/default, tuple, publication, or signing change. + +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-OWNER-CORRECTION-LOCAL-001 — explicit key owners are locally green + +- Date/owner/base: 2026-07-15, Codex implementation owner; uncommitted workflow-oracle/checklist + correction atop exact RED head `0a85f88003fee626cbeb91d9ed65880e4b566991` on Apple arm64, + macOS 26.2 build 25C56, Node 26.5.0, pnpm 10.24.0. Native Windows execution remains required. +- Correction: after preserving the proven inheritance-free ACLs, call Windows `icacls /setowner` + with LocalSystem SID `S-1-5-18` for the host private key and the exact new local-user SID for + `authorized_keys`. No ACE, account, password, key bytes, service, config, transport, production + module, or product behavior changes. The workflow oracle pins both owner calls. +- Purpose/oracle command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs`. Result: 10/10 workflow cases pass in 63 ms; + the live suite remains one honest local skip; total Vitest duration is 540 ms. Extracted + start/measure/stop blocks all parse through PowerShell `[scriptblock]::Create()`. +- Release-contract command: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +config/scripts/ssh-relay-runtime-*.test.mjs` passes 50 files and 283/283 cases in 11.32 seconds. + Targeted `oxlint` for the changed oracle passes. `oxfmt`, `git diff --check`, and exact protected + resolver equality pass. +- Residual boundary: local syntax/contracts do not prove Windows accepts either owner, SSH auth, + PowerShell 5.1, full-size transfer, cancellation, collision, service restoration, or account/root + teardown. Preserve the original arm64 run result, then push the narrow correction and require + fresh exact-head native x64/arm64 evidence. No product/Beta/fallback/default, tuple, publication, + or signing claim follows. + +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-ARM64-CAPABILITY-CI-RED-001 — hosted ARM64 capability install times out + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact head + `0a85f88003fee626cbeb91d9ed65880e4b566991`, artifact run `29535299209`, native Windows arm64 job + `87745066052` on `windows-11-arm`. The job starts at 21:14:48Z and is cancelled at 21:45:11Z by + its existing 30-minute bound. +- Prior-boundary proof: native setup, exact checkout, Node/pnpm/MSVC, all runtime artifact contracts, + exact Node inputs, two clean builds/equality/runtime smoke, and full-size desktop extraction/cache + pass. The new start step begins at 21:25:43Z. Its last executed command is + `Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0`; it emits no completion or error + before cancellation at 21:44:59Z, 19m16s later. No fixture root/account/key/service is created + because capability installation precedes them; measure/upload correctly skip. +- Teardown proof: the `always()` teardown runs for four seconds and passes after job cancellation. + Ownership markers are absent, so it removes no collision or unrelated account/service/root. The + runner is ephemeral and no production/external host state is touched. +- Evidence-driven correction: do not increase the job timeout or retry an unbounded OS servicing + dependency. Use one immutable official Microsoft Win32-OpenSSH release on both architectures; + pin `OpenSSH-Win64.zip` to + `sha256:23f50f3458c4c5d0b12217c6a5ddfde0137210a30fa870e98b29827f7b43aba5` and + `OpenSSH-ARM64.zip` to + `sha256:698c6aec31c1dd0fb996206e8741f4531a97355686b5431ef347d531b07fcd42` for tag + `10.0.0.0p2-Preview`; bound curl retries/time, verify the archive before extraction, and require + target-native `Valid` Microsoft Authenticode on every extracted PE. Register/delete only the + collision-checked fixed-name `sshd` service. The GitHub download provisions the loopback CI + fixture before testing; the product runtime transfer remains client-to-host over authenticated SSH + with no remote HTTP/GitHub operation. +- Residual boundary: no arm64 SSH authentication/PowerShell/full-size/teardown claim exists yet, and + x64 still needs its explicit key-owner rerun. Add workflow-oracle coverage for release tag, both + hashes, bounded download, Authenticode, fixed-name service ownership/deletion, and product + isolation; rerun local gates and fresh x64/arm64 jobs. No product/Beta/fallback/default, tuple, + publication, or signing change is authorized. + +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001 — pinned portable fixture correction is locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted workflow, purpose-test, + oracle, and checklist correction atop exact pushed head + `0a85f88003fee626cbeb91d9ed65880e4b566991` on Apple arm64, macOS 26.2 build 25C56, Node 26.5.0, + pnpm 10.24.0. Local Node is newer than the repository's Node 24 contract and macOS cannot execute + the Windows fixture, so exact-head native proof remains mandatory. +- Correction: remove only `Add-WindowsCapability` fixture provisioning. Both Windows architectures + download the same immutable official Microsoft Win32-OpenSSH tag `10.0.0.0p2-Preview` with bounded + curl (20-second connect, 300-second total, two retries), verify their exact pinned archive SHA-256, + require target-native `Valid` Microsoft Authenticode on every extracted PE, and create/delete only + a collision-checked, ownership-marked fixed-name `sshd` service. The proven LocalSystem host-key + owner and fixture-SID `authorized_keys` owner remain. Account/service ownership markers are + written after collision rejection but before creation so partial failures remain teardown-owned. + This external download provisions only the loopback CI server; no production module or + remote-host HTTP behavior changes. +- Oracle correction: the three no-publication assertions now reject exact Orca release URLs, + `gh release`, and `contents: write` rather than every third-party path containing `releases/`. + The Windows fixture oracle separately pins the exact Microsoft repository/tag, both hashes, + download bounds, Authenticode policy, owners, service creation/deletion, and absence of + `Add-WindowsCapability`. This permits no mutable URL or Orca publication path. +- Purpose/oracle command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 10/10 workflow cases with one declared + live-suite skip in 1.86 seconds real; maximum RSS is 141,148,160 bytes and peak footprint is + 97,261,280 bytes. Independent YAML extraction plus PowerShell `[scriptblock]::Create()` parses all + three start/measure/stop blocks. +- Release-contract command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs` passes 50 files and 283/283 cases in + 16.36 seconds real; maximum RSS is 195,674,112 bytes and peak footprint is 97,539,904 bytes. +- Broad relay command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts` passes 53 files with six declared file skips and + 694 cases with ten declared skips in 26.11 seconds real; maximum RSS is 299,728,896 bytes and peak + footprint is 97,146,640 bytes. +- Static/isolation evidence: `/usr/bin/time -l pnpm typecheck` passes in 4.35 seconds real. + `/usr/bin/time -l pnpm lint` passes in 21.83 seconds real with only the 26 pre-existing warnings, + 41 reliability gates, the 355-entry max-lines ratchet, localization, and bundled-guide checks; + maximum RSS is 2,037,121,024 bytes and peak footprint is 98,310,024 bytes. Changed-file + `oxfmt --check`, `git diff --check`, and exact protected-resolver equality against `HEAD` pass. + The intended diff contains only the CI workflow, its workflow oracle, its purpose-named live test, + and both checklists; no production consumer exists. +- Residual boundary: local contracts and parsing do not prove the archive download, every extracted + PE's Authenticode status, service start, SID ownership, SSH authentication, remote Windows + PowerShell 5.1, full-size serial/four-channel transfer, cancellation, collision preservation, + connection reuse, or teardown. Commit/push the isolated correction and require fresh exact-head + Windows x64 plus arm64 jobs. Product/Beta/settings/fallback/default behavior, tuple enablement, + release publication, Apple signing, and SignPath remain absent; legacy remains the sole + production/default path. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -18708,11 +18844,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the locally green live Windows OpenSSH package, then collect exact-head -`windows-2022` x64 and `windows-11-arm` arm64 capability, ACL, PowerShell 5.1, exact full-size -serial/four-channel/cancellation, collision, connection-reuse, and teardown evidence. Do not check -the broad Milestone 6 Windows system-SSH item yet. Do not add an Electron/startup/product importer, -per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. +Replace only the timed-out OS-capability fixture dependency with the pinned official Microsoft +portable release contract in +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-ARM64-CAPABILITY-CI-RED-001`, retain the explicit key owners, rerun +local gates, and push for fresh x64/arm64 ACL, PowerShell 5.1, exact full-size serial/four-channel/ +cancellation, collision, connection-reuse, and teardown evidence. Do not check the broad Milestone +6 Windows system-SSH item yet. Do not add an Electron/startup/product importer, per-target mode +wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. diff --git a/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts b/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts index f9a09a07381..4c736b38635 100644 --- a/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +++ b/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts @@ -53,6 +53,7 @@ const user = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_USER const identityFile = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_IDENTITY const remoteRoot = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_REMOTE_ROOT const serverVersion = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_SERVER_VERSION +const fixtureArchiveSha256 = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_ARCHIVE_SHA256 const powerShellVersion = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_POWERSHELL_VERSION const runtimeRoot = process.env.ORCA_SSH_RELAY_FULL_SIZE_RUNTIME_ROOT const identityPath = process.env.ORCA_SSH_RELAY_FULL_SIZE_IDENTITY @@ -63,6 +64,7 @@ const hasLiveInput = Boolean( identityFile && remoteRoot && serverVersion && + fixtureArchiveSha256 && powerShellVersion && runtimeRoot && identityPath && @@ -242,6 +244,7 @@ describe.skipIf(!hasLiveInput)( expect(process.platform).toBe('win32') expect(process.env.ORCA_SSH_FORCE_SYSTEM_TRANSPORT).toBe('1') expect(powerShellVersion).toMatch(/^5\.1\./u) + expect(fixtureArchiveSha256).toMatch(/^[0-9a-f]{64}$/u) const identity = parseIdentity(JSON.parse(await readFile(identityPath as string, 'utf8'))) expect(identity.os).toBe('win32') const tree = await scanSshRelayRuntimeSourceTree( @@ -344,6 +347,7 @@ describe.skipIf(!hasLiveInput)( files: tree.fileCount, bytes: tree.expandedBytes, serverVersion, + fixtureArchiveSha256, powerShellVersion, runnerImage: process.env.ImageOS, runnerVersion: process.env.ImageVersion, From 85731b3feff90047f67b716363b1549b0d79ee2c Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 18:20:42 -0400 Subject: [PATCH 241/267] test(ssh): pin portable OpenSSL fixture library Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 41 ++++++++- .../ssh-relay-runtime-workflow.test.mjs | 9 ++ ...elease-implementation-checklist-summary.md | 18 +++- ...github-release-implementation-checklist.md | 92 ++++++++++++++++--- ...6-07-14-ssh-relay-github-release-plan.html | 17 ++++ 5 files changed, 162 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index e0a87217bd1..3bb3a6201e9 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -1335,16 +1335,18 @@ jobs: throw 'Windows OpenSSH fixed service name is already registered' } $releaseTag = '10.0.0.0p2-Preview' - $archiveName, $expandedName, $expectedArchiveSha256 = switch ($env:RUNNER_ARCH) { + $archiveName, $expandedName, $expectedArchiveSha256, $expectedLibcryptoSha256 = switch ($env:RUNNER_ARCH) { 'X64' { 'OpenSSH-Win64.zip' 'OpenSSH-Win64' '23f50f3458c4c5d0b12217c6a5ddfde0137210a30fa870e98b29827f7b43aba5' + '4652e861c0335ee80a51306ceab75aa35c8865b235f97ce7dd5a0fd9dab44b5d' } 'ARM64' { 'OpenSSH-ARM64.zip' 'OpenSSH-ARM64' '698c6aec31c1dd0fb996206e8741f4531a97355686b5431ef347d531b07fcd42' + '7ad2b7721893c54ad6e4fec1a3477701fb48975323c2c4ac6cd0b8c972ab242a' } default { throw "Unsupported Windows OpenSSH fixture architecture: $env:RUNNER_ARCH" } } @@ -1363,8 +1365,43 @@ jobs: $nativeFiles = @(Get-ChildItem -LiteralPath $openSshRoot -Recurse -File | Where-Object { $_.Extension -in @('.exe', '.dll') }) - if ($nativeFiles.Count -lt 10) { throw 'Official Windows OpenSSH fixture is incomplete' } + $expectedNativeNames = @( + 'libcrypto.dll', + 'scp.exe', + 'sftp-server.exe', + 'sftp.exe', + 'ssh-add.exe', + 'ssh-agent.exe', + 'ssh-keygen.exe', + 'ssh-keyscan.exe', + 'ssh-pkcs11-helper.exe', + 'ssh-shellhost.exe', + 'ssh-sk-helper.exe', + 'ssh.exe', + 'sshd-auth.exe', + 'sshd-session.exe', + 'sshd.exe' + ) | Sort-Object + $actualNativeNames = @($nativeFiles.Name | Sort-Object) + if (($actualNativeNames -join "`n") -ne ($expectedNativeNames -join "`n")) { + throw "Official Windows OpenSSH fixture native-file closure changed: $($actualNativeNames -join ',')" + } + $libcryptoFiles = @($nativeFiles | Where-Object { $_.Name -eq 'libcrypto.dll' }) + if ($libcryptoFiles.Count -ne 1) { + throw 'Official Windows OpenSSH fixture must contain exactly one libcrypto.dll' + } + $libcrypto = $libcryptoFiles[0] + $libcryptoSha256 = (Get-FileHash -LiteralPath $libcrypto.FullName -Algorithm SHA256).Hash.ToLowerInvariant() + if ($libcryptoSha256 -ne $expectedLibcryptoSha256) { + throw "Official Windows OpenSSH libcrypto.dll hash mismatch: $libcryptoSha256" + } + # Why: Microsoft's immutable bundle preserves one unsigned upstream OpenSSL library. + $libcryptoSignature = Get-AuthenticodeSignature -LiteralPath $libcrypto.FullName + if ($libcryptoSignature.Status -ne 'NotSigned') { + throw "Official Windows OpenSSH libcrypto.dll signature changed: $($libcryptoSignature.Status)" + } foreach ($nativeFile in $nativeFiles) { + if ($nativeFile.Name -eq 'libcrypto.dll') { continue } $signature = Get-AuthenticodeSignature -LiteralPath $nativeFile.FullName if ( $signature.Status -ne 'Valid' -or diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index d3161d4854d..13396714d16 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -160,8 +160,17 @@ describe('SSH relay runtime artifact workflow', () => { expect(start.run).toContain('10.0.0.0p2-Preview') expect(start.run).toContain('23f50f3458c4c5d0b12217c6a5ddfde0137210a30fa870e98b29827f7b43aba5') expect(start.run).toContain('698c6aec31c1dd0fb996206e8741f4531a97355686b5431ef347d531b07fcd42') + expect(start.run).toContain('4652e861c0335ee80a51306ceab75aa35c8865b235f97ce7dd5a0fd9dab44b5d') + expect(start.run).toContain('7ad2b7721893c54ad6e4fec1a3477701fb48975323c2c4ac6cd0b8c972ab242a') expect(start.run).toContain('--connect-timeout 20 --max-time 300 --retry 2') expect(start.run).toContain('Get-AuthenticodeSignature') + expect(start.run).toContain('$actualNativeNames -join "`n"') + expect(start.run).toContain('$expectedNativeNames -join "`n"') + expect(start.run).toContain("'ssh-sk-helper.exe'") + expect(start.run).toContain("'sshd-session.exe'") + expect(start.run).toContain('$libcryptoFiles.Count -ne 1') + expect(start.run).toContain("$libcryptoSignature.Status -ne 'NotSigned'") + expect(start.run).toContain("if ($nativeFile.Name -eq 'libcrypto.dll') { continue }") expect(start.run).toContain("$signature.Status -ne 'Valid'") expect(start.run).toContain('CN=Microsoft Corporation') expect(start.run).not.toContain('Add-WindowsCapability') 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 b5c49a804f4..e211b3e7033 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 @@ -17,6 +17,19 @@ Microsoft fixture release URL while Orca release URLs, `gh release`, and `conten forbidden; 10/10 focused workflow contracts, 283/283 release contracts, 694 relay cases with ten declared skips, typecheck, full lint/reliability/max-lines, PowerShell syntax, formatting, diff, and protected-resolver isolation pass. Fresh exact-head x64/arm64 proof is still required. +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LIBCRYPTO-CI-RED-001` records exact head +`bcbd9d6b36dc8cc2e917fa88eb30f87ab1d88657`, artifact run `29538044827`, and native x64 job +`87753850628`: the archive hash passes, but the fail-closed every-PE Authenticode policy rejects the +official bundle's sole upstream `libcrypto.dll`; teardown passes before any account/service creation. +Pin that library's exact per-architecture hash and require it to be the only `NotSigned` PE while +every executable remains valid Microsoft Authenticode. Native ARM64 job `87753850663` independently +reproduces the same archive-green/`libcrypto.dll` RED and passes teardown after all prior runtime +gates, confirming one architecture-consistent correction. +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LIBCRYPTO-CORRECTION-LOCAL-001` records the role-based trust +correction locally green: exact 15-file native closure, both library hashes, sole `NotSigned` +library, valid Microsoft Authenticode for every executable, 10/10 workflow contracts, 283/283 +release contracts, typecheck, full lint/reliability/max-lines, and PowerShell syntax pass. Push its +exact head for fresh native x64/arm64 proof. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown @@ -52,8 +65,9 @@ Preserve the original arm64 result, then push for fresh x64/arm64 proof. all prior native build/runtime gates pass, but `Add-WindowsCapability OpenSSH.Server` emits no result for 19m16s and the existing 30-minute job cancels it; ownership-safe teardown still passes. Do not increase the timeout. Provision the same immutable official Microsoft Win32-OpenSSH release on both -architectures with per-architecture SHA-256 pins, target-native Authenticode, bounded download, -fixed-name service ownership/deletion, and no production or remote-host HTTP behavior. +architectures with per-architecture archive and sole-upstream-library SHA-256 pins, role-based +target-native Authenticode, bounded download, fixed-name service ownership/deletion, and no +production or remote-host HTTP behavior. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` closes the local correction gate; push its exact head and require both native architectures before closing the live Windows package. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LOCAL-RED-001` proves the native workflow oracle passes 9/9 and the 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 fc4ab49d077..3e777cff272 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: bounded official Windows OpenSSH fixture correction**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **PINNED MICROSOFT FIXTURE LOCALLY GREEN / EXACT-HEAD NATIVE PROOF NEXT — 2026-07-15, Codex implementation owner** — exact-head run `29535299209` proves x64 needs explicit host-key ownership and the hosted ARM64 image cannot install `OpenSSH.Server~~~~0.0.1.0` within the 30-minute job. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the local correction that replaces that OS-capability dependency on both architectures with one immutable official Microsoft Win32-OpenSSH release, per-architecture SHA-256 pins, target-native Authenticode checks, bounded download/extraction, explicit key owners, and an ownership-marked fixed-name `sshd` service removed in teardown. The workflow oracle permits only that exact external Microsoft fixture release URL while continuing to forbid Orca release URLs, `gh release`, and `contents: write`. This is CI fixture provisioning only; production runtime bytes still cross authenticated SSH and no product remote downloads anything. Push the isolated correction and require fresh exact-head x64/arm64 proof. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Session checkpoint: **ROLE-BASED LIBCRYPTO FIXTURE TRUST LOCALLY GREEN / EXACT-HEAD NATIVE PROOF NEXT — 2026-07-15, Codex implementation owner** — exact-head run `29535299209` proves x64 needs explicit host-key ownership and the hosted ARM64 image cannot install `OpenSSH.Server~~~~0.0.1.0` within the 30-minute job. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the local correction that replaces that OS-capability dependency on both architectures with one immutable official Microsoft Win32-OpenSSH release. Exact head `bcbd9d6b36dc8cc2e917fa88eb30f87ab1d88657`, artifact run `29538044827`, x64 job `87753850628`, and ARM64 job `87753850663` prove both archive pins but expose each official bundle's sole unsigned upstream `libcrypto.dll` under `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LIBCRYPTO-CI-RED-001`; both cleanups pass before any account/service creation. The HTML plan and checklist record the narrow CI-fixture-only trust rule before code changes. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LIBCRYPTO-CORRECTION-LOCAL-001` now records exact native-file closure, per-architecture library pins, sole `NotSigned` status, mandatory valid Microsoft Authenticode for every executable, and all local/static gates green. Push the isolated correction for fresh exact-head x64/arm64 proof. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -159,13 +159,15 @@ same change as the work it records. - Completed package: the disconnected POSIX system-SSH tree/root composition is green locally and on all six primary Node 24 native clients under E-M6-POSIX-SYSTEM-SSH-TREE-LOCAL-001 and E-M6-POSIX-SYSTEM-SSH-TREE-CI-001. It has no live/full-size SSH or product caller. -- Active package: **Official portable fixture correction in progress — 2026-07-15, Codex - implementation owner** — the ARM64 capability install times out under +- Active package: **Role-based official portable fixture trust correction in progress — 2026-07-15, + Codex implementation owner** — the ARM64 capability install times out under `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-ARM64-CAPABILITY-CI-RED-001`. Provision the same exact official - Microsoft Win32-OpenSSH release on x64/arm64 with pinned archive hashes and target-native - Authenticode checks, create/delete only an ownership-marked fixed-name `sshd` service, retain the - explicit key owners, and rerun local plus both native architectures. Do not add product, settings, - fallback, tuple, publication, default behavior, or signing. + Microsoft Win32-OpenSSH release on x64/arm64 with pinned archive hashes, valid Microsoft + Authenticode for every executable, and a separate per-architecture hash for its sole upstream + `libcrypto.dll`, which must be the only `NotSigned` PE. Create/delete only an ownership-marked + fixed-name `sshd` service, retain the explicit key owners, and rerun local plus both native + architectures. Do not add product, settings, fallback, tuple, publication, default behavior, or + signing. - 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 @@ -18786,6 +18788,75 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 10/10 workflow cases release publication, Apple signing, and SignPath remain absent; legacy remains the sole production/default path. +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LIBCRYPTO-CI-RED-001 — official upstream library invalidates every-PE signing assumption + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact pushed head + `bcbd9d6b36dc8cc2e917fa88eb30f87ab1d88657`, artifact run `29538044827`, native Windows x64 job + `87753850628` on `windows-2022`, and native Windows ARM64 job `87753850663` on `windows-11-arm`. +- Prior-boundary proof: checkout, native MSVC/Node/pnpm, all 283 runtime artifact contracts, exact + Node inputs, two clean runtime builds/equality/smoke, and full-size desktop extraction/cache pass. + The fixture downloads 5,570 KiB in under one second and passes the exact archive SHA-256 + `23f50f3458c4c5d0b12217c6a5ddfde0137210a30fa870e98b29827f7b43aba5`; failure occurs later at the + target-native signature loop, so no live SSH claim follows. +- Exact RED: the start step fails after five seconds with `Official Windows OpenSSH fixture signature +is invalid: libcrypto.dll`. The fail-closed every-PE policy correctly rejects the official + bundle's only DLL before creating an account, service, key, config, or SSH connection. The + `always()` teardown passes in four seconds and removes only the ownership-marked fixture root. +- Independent ARM64 RED: after all the same prior artifact/runtime/full-size cache gates pass, the + ARM64 start step downloads its 4.79 MiB pinned archive and fails after three seconds on the same + `libcrypto.dll` signature assertion. Its ownership-safe teardown passes in three seconds. The + complete ARM64 job takes 12m43s; no capability-install hang, live SSH, or transfer claim follows. +- Independent immutable-byte audit: bounded local downloads reproduce both pinned archive hashes. + Each archive contains exactly 14 executables plus one DLL named `libcrypto.dll`; there is no + `libssl.dll`. The x64 library is 1,769,032 bytes with SHA-256 + `4652e861c0335ee80a51306ceab75aa35c8865b235f97ce7dd5a0fd9dab44b5d`; the ARM64 library is + 1,729,072 bytes with SHA-256 + `7ad2b7721893c54ad6e4fec1a3477701fb48975323c2c4ac6cd0b8c972ab242a`. +- Evidence-driven plan correction: the portable server is only a loopback CI fixture, not a product + runtime asset or remote-host dependency. The HTML plan and checklist now require exact archive + plus `libcrypto.dll` hashes, exactly one library with that name, target-native `NotSigned` status + for that pinned upstream library, and valid Microsoft Authenticode for every executable. Any + additional/missing native file, different signature state, signer, or hash fails closed. This does + not weaken product runtime/native-signing/transfer/install/launch trust. +- Residual boundary: implement only this role-based fixture trust policy, rerun local/static gates, + and require fresh exact-head x64/arm64 + archive/signature/service/auth/full-size/cancellation/cleanup proof. No product/Beta/settings/ + fallback/default, tuple, publication, Apple signing, or SignPath change is authorized; legacy + remains the sole production/default path. + +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LIBCRYPTO-CORRECTION-LOCAL-001 — role-based immutable fixture trust is locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted workflow, oracle, + HTML-plan, and checklist correction atop exact RED head + `bcbd9d6b36dc8cc2e917fa88eb30f87ab1d88657` on Apple arm64, macOS 26.2 build 25C56, + Node 26.5.0, pnpm 10.24.0. Native Windows execution remains required. +- Correction: require the exact 15-name PE closure in both pinned archives. Require exactly one + `libcrypto.dll`, its per-architecture SHA-256 from the independent byte audit, and target-native + `NotSigned` status. Every one of the other 14 executables must retain `Valid` Microsoft + Authenticode. An added/missing/duplicated PE, library signer-state change, library hash change, + executable signer/status change, or archive hash change fails before any account/service/key or + SSH execution. The HTML plan records this CI-fixture-only rule; no product trust policy changes. +- Purpose/oracle command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 10/10 workflow cases with one declared + live-suite skip in 4.76 seconds real; maximum RSS is 144,359,424 bytes and peak footprint is + 98,146,160 bytes. Independent YAML extraction plus PowerShell `[scriptblock]::Create()` parses all + three start/measure/stop blocks. +- Release-contract command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs` passes 50 files and 283/283 cases in + 25.19 seconds real; maximum RSS is 195,805,184 bytes and peak footprint is 97,785,664 bytes. +- Static evidence: `/usr/bin/time -l pnpm typecheck` passes in 3.29 seconds real. Final + `/usr/bin/time -l pnpm lint` passes in 20.69 seconds real with only the 26 pre-existing warnings, + all 41 reliability gates, the 355-entry max-lines ratchet, localization, and bundled-guide checks; + maximum RSS is 2,040,971,264 bytes and peak footprint is 97,113,848 bytes. The prior 694-case broad + relay result remains anchored to `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001`; + this correction changes only workflow fixture trust, its oracle, and planning content. +- Residual boundary: local contracts/syntax cannot prove the native `NotSigned`/Microsoft signature + statuses, service/auth, Windows PowerShell 5.1, full-size transfer, cancellation, collision, + connection reuse, or teardown. Format/diff/isolation-check, commit, and push the isolated + correction, then require fresh exact-head Windows x64/arm64 proof. Legacy remains the sole + production/default path; product/Beta/settings/fallback/tuple/publication/signing remain absent. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -18844,10 +18915,9 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Replace only the timed-out OS-capability fixture dependency with the pinned official Microsoft -portable release contract in -`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-ARM64-CAPABILITY-CI-RED-001`, retain the explicit key owners, rerun -local gates, and push for fresh x64/arm64 ACL, PowerShell 5.1, exact full-size serial/four-channel/ +Commit and push the locally green role-based fixture trust correction in +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LIBCRYPTO-CORRECTION-LOCAL-001`, then require fresh exact-head +x64/arm64 archive/native-closure/signature, ACL, PowerShell 5.1, exact full-size serial/four-channel/ cancellation, collision, connection-reuse, and teardown evidence. Do not check the broad Milestone 6 Windows system-SSH item yet. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index e98ac9ad27c..662022aa2d9 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1650,6 +1650,23 @@

    Prove production behavior, failure behavior, and rollback behavior.

    ordering is asserted + + Hosted Windows OpenSSH fixture trust + + When the hosted ARM64 OS capability is unavailable, use one immutable official + Microsoft Win32-OpenSSH release with exact per-architecture archive hashes. + Require valid Microsoft Authenticode on every executable. The release's sole + upstream native library, libcrypto.dll, is accepted only when it is + the only unsigned PE and matches its separately pinned per-architecture hash; + any added, missing, differently signed, or differently hashed native file fails + closed. + + + This trust exception provisions only the loopback CI server. Production clients + and SSH hosts perform no fixture download, and it does not weaken runtime + artifact, native-signing, transfer, install, or launch trust. + + PTY and watcher From 130d57d951b5e5cfb0e1e21183ce116684924e2b Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 18:44:12 -0400 Subject: [PATCH 242/267] fix(ssh): pin portable fixture signer identities Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 38 ++++- .../ssh-relay-runtime-workflow.test.mjs | 8 +- ...elease-implementation-checklist-summary.md | 31 ++-- ...github-release-implementation-checklist.md | 146 ++++++++++++++---- ...6-07-14-ssh-relay-github-release-plan.html | 12 +- 5 files changed, 183 insertions(+), 52 deletions(-) diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 3bb3a6201e9..0a2761f20a2 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -1395,19 +1395,47 @@ jobs: if ($libcryptoSha256 -ne $expectedLibcryptoSha256) { throw "Official Windows OpenSSH libcrypto.dll hash mismatch: $libcryptoSha256" } - # Why: Microsoft's immutable bundle preserves one unsigned upstream OpenSSL library. + # Why: the pinned fixture uses a distinct Microsoft-managed leaf for its upstream library. + $expectedLibcryptoSignerThumbprint = '587116075365AA15BCD8E4FA9CB31BE372B5DE51' $libcryptoSignature = Get-AuthenticodeSignature -LiteralPath $libcrypto.FullName - if ($libcryptoSignature.Status -ne 'NotSigned') { - throw "Official Windows OpenSSH libcrypto.dll signature changed: $($libcryptoSignature.Status)" + $libcryptoCertificate = $libcryptoSignature.SignerCertificate + $libcryptoThumbprint = if ($null -eq $libcryptoCertificate) { + '' + } else { + $libcryptoCertificate.Thumbprint.ToUpperInvariant() } + $libcryptoSubject = if ($null -eq $libcryptoCertificate) { + '' + } else { + $libcryptoCertificate.Subject + } + if ( + $libcryptoSignature.Status -ne 'Valid' -or + $libcryptoThumbprint -ne $expectedLibcryptoSignerThumbprint -or + $libcryptoSubject -notmatch '(^|,\s*)CN=Microsoft 3rd Party Application Component(,|$)' + ) { + throw "Official Windows OpenSSH libcrypto.dll signature identity is invalid: $($libcryptoSignature.Status), $libcryptoThumbprint, $libcryptoSubject" + } + $expectedExecutableSignerThumbprints = @( + 'F5877012FBD62FABCBDC8D8CEE9C9585BA30DF79', + '3F56A45111684D454E231CFDC4DA5C8D370F9816' + ) foreach ($nativeFile in $nativeFiles) { if ($nativeFile.Name -eq 'libcrypto.dll') { continue } $signature = Get-AuthenticodeSignature -LiteralPath $nativeFile.FullName + $certificate = $signature.SignerCertificate + $thumbprint = if ($null -eq $certificate) { + '' + } else { + $certificate.Thumbprint.ToUpperInvariant() + } + $subject = if ($null -eq $certificate) { '' } else { $certificate.Subject } if ( $signature.Status -ne 'Valid' -or - $signature.SignerCertificate.Subject -notmatch 'CN=Microsoft Corporation' + $expectedExecutableSignerThumbprints -notcontains $thumbprint -or + $subject -notmatch '(^|,\s*)CN=Microsoft Corporation(,|$)' ) { - throw "Official Windows OpenSSH fixture signature is invalid: $($nativeFile.Name)" + throw "Official Windows OpenSSH fixture signature identity is invalid: $($nativeFile.Name), $($signature.Status), $thumbprint, $subject" } } $sshd = Join-Path $openSshRoot 'sshd.exe' diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 13396714d16..01e067261c3 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -169,7 +169,13 @@ describe('SSH relay runtime artifact workflow', () => { expect(start.run).toContain("'ssh-sk-helper.exe'") expect(start.run).toContain("'sshd-session.exe'") expect(start.run).toContain('$libcryptoFiles.Count -ne 1') - expect(start.run).toContain("$libcryptoSignature.Status -ne 'NotSigned'") + expect(start.run).toContain("$libcryptoSignature.Status -ne 'Valid'") + expect(start.run).toContain('587116075365AA15BCD8E4FA9CB31BE372B5DE51') + expect(start.run).toContain('CN=Microsoft 3rd Party Application Component') + expect(start.run).toContain('F5877012FBD62FABCBDC8D8CEE9C9585BA30DF79') + expect(start.run).toContain('3F56A45111684D454E231CFDC4DA5C8D370F9816') + expect(start.run).toContain('$expectedExecutableSignerThumbprints -notcontains $thumbprint') + expect(start.run).not.toContain("'NotSigned'") expect(start.run).toContain("if ($nativeFile.Name -eq 'libcrypto.dll') { continue }") expect(start.run).toContain("$signature.Status -ne 'Valid'") expect(start.run).toContain('CN=Microsoft Corporation') 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 e211b3e7033..91eec24aeed 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 @@ -18,18 +18,27 @@ forbidden; 10/10 focused workflow contracts, 283/283 release contracts, 694 rela declared skips, typecheck, full lint/reliability/max-lines, PowerShell syntax, formatting, diff, and protected-resolver isolation pass. Fresh exact-head x64/arm64 proof is still required. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LIBCRYPTO-CI-RED-001` records exact head -`bcbd9d6b36dc8cc2e917fa88eb30f87ab1d88657`, artifact run `29538044827`, and native x64 job -`87753850628`: the archive hash passes, but the fail-closed every-PE Authenticode policy rejects the -official bundle's sole upstream `libcrypto.dll`; teardown passes before any account/service creation. -Pin that library's exact per-architecture hash and require it to be the only `NotSigned` PE while -every executable remains valid Microsoft Authenticode. Native ARM64 job `87753850663` independently -reproduces the same archive-green/`libcrypto.dll` RED and passes teardown after all prior runtime -gates, confirming one architecture-consistent correction. +`bcbd9d6b36dc8cc2e917fa88eb30f87ab1d88657`, artifact run `29538044827`, and native x64/arm64 jobs +`87753850628`/`87753850663`: both archive hashes pass, but the one-subject Authenticode policy rejects +the official bundle's `libcrypto.dll`; both teardowns pass before any account/service creation. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LIBCRYPTO-CORRECTION-LOCAL-001` records the role-based trust -correction locally green: exact 15-file native closure, both library hashes, sole `NotSigned` -library, valid Microsoft Authenticode for every executable, 10/10 workflow contracts, 283/283 -release contracts, typecheck, full lint/reliability/max-lines, and PowerShell syntax pass. Push its -exact head for fresh native x64/arm64 proof. +correction locally green under the initial hypothesis: exact 15-file native closure, both library +hashes, 10/10 workflow contracts, 283/283 release contracts, typecheck, full +lint/reliability/max-lines, and PowerShell syntax pass. Exact head +`85731b3feff90047f67b716363b1549b0d79ee2c`, artifact run `29539266437`, and x64 job +`87757772975` disprove only the guessed `NotSigned` policy: the archive/hash/closure gates pass, +`libcrypto.dll` is target-natively `Valid`, and ownership-safe teardown passes. ARM64 job +`87757772957` independently reports the same exact RED after all prior gates and also passes teardown. +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-SIGNER-AUDIT-001` records both pinned archives and all 30 PE +assessments: `libcrypto.dll` uses the expected Microsoft 3rd Party common name and exact leaf thumbprint +`587116075365AA15BCD8E4FA9CB31BE372B5DE51`; every executable uses the expected Microsoft Corporation +common name plus audited thumbprint `F5877012FBD62FABCBDC8D8CEE9C9585BA30DF79` or +`3F56A45111684D454E231CFDC4DA5C8D370F9816`. +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-EXACT-SIGNER-CORRECTION-LOCAL-001` implements that fixture-only +policy and is locally green: 10/10 focused workflow cases with one declared live skip, all three +PowerShell blocks parsed, 283/283 artifact contracts, typecheck, full lint/reliability/max-lines, +formatting, diff, and protected-resolver isolation. Commit/push the isolated correction and require +fresh exact-head native x64/arm64 proof. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown 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 3e777cff272..37fb0a26dc7 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: bounded official Windows OpenSSH fixture correction**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **ROLE-BASED LIBCRYPTO FIXTURE TRUST LOCALLY GREEN / EXACT-HEAD NATIVE PROOF NEXT — 2026-07-15, Codex implementation owner** — exact-head run `29535299209` proves x64 needs explicit host-key ownership and the hosted ARM64 image cannot install `OpenSSH.Server~~~~0.0.1.0` within the 30-minute job. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the local correction that replaces that OS-capability dependency on both architectures with one immutable official Microsoft Win32-OpenSSH release. Exact head `bcbd9d6b36dc8cc2e917fa88eb30f87ab1d88657`, artifact run `29538044827`, x64 job `87753850628`, and ARM64 job `87753850663` prove both archive pins but expose each official bundle's sole unsigned upstream `libcrypto.dll` under `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LIBCRYPTO-CI-RED-001`; both cleanups pass before any account/service creation. The HTML plan and checklist record the narrow CI-fixture-only trust rule before code changes. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LIBCRYPTO-CORRECTION-LOCAL-001` now records exact native-file closure, per-architecture library pins, sole `NotSigned` status, mandatory valid Microsoft Authenticode for every executable, and all local/static gates green. Push the isolated correction for fresh exact-head x64/arm64 proof. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Session checkpoint: **EXACT-SIGNER FIXTURE TRUST LOCALLY GREEN / EXACT-HEAD NATIVE PROOF NEXT — 2026-07-15, Codex implementation owner** — exact head `85731b3feff90047f67b716363b1549b0d79ee2c`, artifact run `29539266437`, and x64/arm64 jobs `87757772975`/`87757772957` pass their pinned archive hashes, exact 15-file native closures, pinned `libcrypto.dll` hashes, prior runtime contracts/build/smoke, and full-size cache gates before both disproving only the guessed `NotSigned` policy: target-native Authenticode reports `libcrypto.dll` as `Valid`. Both ownership-safe teardowns pass before any SSH account/service creation. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-SIGNER-AUDIT-001` audits all 30 pinned PEs; `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-EXACT-SIGNER-CORRECTION-LOCAL-001` implements the exact common-name/thumbprint fixture policy and passes focused, PowerShell syntax, 283 artifact-contract, typecheck, lint/reliability/max-lines, format, diff, and isolation gates. Commit/push the isolated correction and obtain fresh exact-head native proof. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -159,15 +159,14 @@ same change as the work it records. - Completed package: the disconnected POSIX system-SSH tree/root composition is green locally and on all six primary Node 24 native clients under E-M6-POSIX-SYSTEM-SSH-TREE-LOCAL-001 and E-M6-POSIX-SYSTEM-SSH-TREE-CI-001. It has no live/full-size SSH or product caller. -- Active package: **Role-based official portable fixture trust correction in progress — 2026-07-15, - Codex implementation owner** — the ARM64 capability install times out under - `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-ARM64-CAPABILITY-CI-RED-001`. Provision the same exact official - Microsoft Win32-OpenSSH release on x64/arm64 with pinned archive hashes, valid Microsoft - Authenticode for every executable, and a separate per-architecture hash for its sole upstream - `libcrypto.dll`, which must be the only `NotSigned` PE. Create/delete only an ownership-marked - fixed-name `sshd` service, retain the explicit key owners, and rerun local plus both native - architectures. Do not add product, settings, fallback, tuple, publication, default behavior, or - signing. +- Active package: **Exact-signer official portable fixture trust correction in progress — 2026-07-15, + Codex implementation owner** — exact-head x64/arm64 jobs `87757772975`/`87757772957` prove the + pinned archives, 15-name native closure, and per-architecture `libcrypto.dll` hashes, then fail + closed because `libcrypto.dll` is `Valid`, not `NotSigned`; both teardowns pass before account or + service creation. Require `Valid` Authenticode plus exact audited subject/thumbprint identities on + every PE, retain the exact archive/file closure and hashes, create/delete only the ownership-marked + fixed-name `sshd` service, and rerun local plus both native architectures. Do not add product, + settings, fallback, tuple, publication, default behavior, or signing. - 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 @@ -18788,7 +18787,7 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 10/10 workflow cases release publication, Apple signing, and SignPath remain absent; legacy remains the sole production/default path. -### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LIBCRYPTO-CI-RED-001 — official upstream library invalidates every-PE signing assumption +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LIBCRYPTO-CI-RED-001 — official upstream library invalidates the one-subject signing assumption - Date/owner/head/run: 2026-07-15, Codex implementation owner; exact pushed head `bcbd9d6b36dc8cc2e917fa88eb30f87ab1d88657`, artifact run `29538044827`, native Windows x64 job @@ -18812,25 +18811,22 @@ is invalid: libcrypto.dll`. The fail-closed every-PE policy correctly rejects th `4652e861c0335ee80a51306ceab75aa35c8865b235f97ce7dd5a0fd9dab44b5d`; the ARM64 library is 1,729,072 bytes with SHA-256 `7ad2b7721893c54ad6e4fec1a3477701fb48975323c2c4ac6cd0b8c972ab242a`. -- Evidence-driven plan correction: the portable server is only a loopback CI fixture, not a product - runtime asset or remote-host dependency. The HTML plan and checklist now require exact archive - plus `libcrypto.dll` hashes, exactly one library with that name, target-native `NotSigned` status - for that pinned upstream library, and valid Microsoft Authenticode for every executable. Any - additional/missing native file, different signature state, signer, or hash fails closed. This does - not weaken product runtime/native-signing/transfer/install/launch trust. -- Residual boundary: implement only this role-based fixture trust policy, rerun local/static gates, - and require fresh exact-head x64/arm64 - archive/signature/service/auth/full-size/cancellation/cleanup proof. No product/Beta/settings/ - fallback/default, tuple, publication, Apple signing, or SignPath change is authorized; legacy - remains the sole production/default path. - -### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LIBCRYPTO-CORRECTION-LOCAL-001 — role-based immutable fixture trust is locally green +- Historical interpretation: this evidence established the archive/hash/teardown boundary, but the + initial local hypothesis that `libcrypto.dll` was `NotSigned` was not target-native evidence. + Exact-head run `29539266437` later rejected that hypothesis safely before fixture side effects. +- Residual boundary: use target-native evidence plus an independent signer audit to define the + fixture-only policy, rerun local/static gates, and require fresh exact-head x64/arm64 archive/ + signature/service/auth/full-size/cancellation/cleanup proof. No product/Beta/settings/fallback/ + default, tuple, publication, Apple signing, or SignPath change is authorized; legacy remains the + sole production/default path. + +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LIBCRYPTO-CORRECTION-LOCAL-001 — rejected initial role-based fixture hypothesis was locally green - Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted workflow, oracle, HTML-plan, and checklist correction atop exact RED head `bcbd9d6b36dc8cc2e917fa88eb30f87ab1d88657` on Apple arm64, macOS 26.2 build 25C56, Node 26.5.0, pnpm 10.24.0. Native Windows execution remains required. -- Correction: require the exact 15-name PE closure in both pinned archives. Require exactly one +- Initial hypothesis: require the exact 15-name PE closure in both pinned archives. Require exactly one `libcrypto.dll`, its per-architecture SHA-256 from the independent byte audit, and target-native `NotSigned` status. Every one of the other 14 executables must retain `Valid` Microsoft Authenticode. An added/missing/duplicated PE, library signer-state change, library hash change, @@ -18851,11 +18847,101 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 10/10 workflow cases maximum RSS is 2,040,971,264 bytes and peak footprint is 97,113,848 bytes. The prior 694-case broad relay result remains anchored to `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001`; this correction changes only workflow fixture trust, its oracle, and planning content. -- Residual boundary: local contracts/syntax cannot prove the native `NotSigned`/Microsoft signature - statuses, service/auth, Windows PowerShell 5.1, full-size transfer, cancellation, collision, - connection reuse, or teardown. Format/diff/isolation-check, commit, and push the isolated - correction, then require fresh exact-head Windows x64/arm64 proof. Legacy remains the sole - production/default path; product/Beta/settings/fallback/tuple/publication/signing remain absent. +- Result/residual boundary: local contracts/syntax could not prove native signature status. + Exact-head Windows x64/arm64 run `29539266437` correctly rejected the `NotSigned` hypothesis before + fixture side effects. Preserve its archive/hash/closure gates, replace only the signer policy from + audited native evidence, and rerun both architectures. Legacy remains the sole production/default + path; product/Beta/settings/fallback/tuple/publication/signing remain absent. + +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LIBCRYPTO-EXACT-HEAD-CI-RED-001 — both native architectures reject the guessed unsigned policy + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact pushed head + `85731b3feff90047f67b716363b1549b0d79ee2c`, artifact run + [29539266437](https://github.com/stablyai/orca/actions/runs/29539266437), x64 job `87757772975` + (`windows-2022`, runner `GitHub Actions 1000058143`) and ARM64 job `87757772957` + (`windows-11-arm`, runner `GitHub Actions 1000058142`). +- Exact RED: x64 passes checkout/toolchain, 283 artifact contracts, exact Node inputs, clean-build + equality/smoke, full-size extraction/cache, pinned 5,570 KiB archive hash, exact 15-name native + closure, and x64 library hash, then fails in three seconds with `Official Windows OpenSSH +libcrypto.dll signature changed: Valid`. ARM64 independently passes the same prior gates, pinned + 4.79 MiB archive hash, closure, and ARM64 library hash, then fails in four seconds with the same + exact `Valid` result. No account, key, service, connection, or transfer is created. +- Cleanup: the x64 `always()` teardown passes in five seconds; ARM64 teardown passes in fifteen + seconds. Each removes only the ownership-marked fixture root. No live SSH or transfer claim follows. +- Interpretation: the exact pinned bytes and closure are correct; only the guessed `NotSigned` + assertion is wrong. This is a fixture-policy RED and does not touch product runtime trust or any + production/default SSH path. + +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-SIGNER-AUDIT-001 — every pinned PE leaf identity is enumerated + +- Date/owner/runner: 2026-07-15, Codex implementation owner; Apple arm64, macOS 26.2 build 25C56, + Node 26.5.0, OpenSSL from `/opt/homebrew/bin/openssl`. Diagnostic root: + `/tmp/orca-openssh-signer-audit.dk7TQQ`; no workspace source file is generated. +- Immutable inputs: bounded `curl -fL --connect-timeout 20 --max-time 300 --retry 2` downloads of + official release `10.0.0.0p2-Preview` reproduce x64 archive SHA-256 + `23f50f3458c4c5d0b12217c6a5ddfde0137210a30fa870e98b29827f7b43aba5` and ARM64 archive SHA-256 + `698c6aec31c1dd0fb996206e8741f4531a97355686b5431ef347d531b07fcd42`. +- Audit method/result: a bounded local Node PE parser extracts each WIN_CERTIFICATE PKCS#7 blob; + `openssl pkcs7 -inform DER -print_certs | openssl x509 -noout -subject -serial -fingerprint -sha1` + assesses all 15 x64 plus all 15 ARM64 PEs. Both `libcrypto.dll` files have subject + `C=US, ST=Washington, L=Redmond, O=Microsoft Corporation, CN=Microsoft 3rd Party Application +Component`, serial `330000046D55C0D43B289C0BDE00000000046D`, and SHA-1 thumbprint + `587116075365AA15BCD8E4FA9CB31BE372B5DE51`. +- Executable identities: every one of the 28 executable assessments has exact subject + `C=US, ST=Washington, L=Redmond, O=Microsoft Corporation, CN=Microsoft Corporation` and one of two + leaves: serial `330000048498E212E078A3315D000000000484`, thumbprint + `F5877012FBD62FABCBDC8D8CEE9C9585BA30DF79`; or serial + `33000004855E99EC0E592FCDD7000000000485`, thumbprint + `3F56A45111684D454E231CFDC4DA5C8D370F9816`. There are no other PE signer identities. +- Evidence-driven policy: retain exact archive hashes, exact native closure, and per-architecture + `libcrypto.dll` hashes. Require target-native `Valid` status on every PE; require the exact + third-party common name and exact thumbprint for `libcrypto.dll`; require the expected Microsoft + Corporation common name and either audited executable thumbprint for every `.exe`. Any drift fails before account, + service, key, config, SSH, or transfer. This is only loopback CI fixture trust and does not alter + product runtime/native-signing/transfer/install/launch policy. +- Residual boundary: implement workflow and oracle, rerun local/static/PowerShell gates, then require + fresh exact-head native x64/arm64 service/auth/full-size/cancellation/collision/cleanup proof. + +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-EXACT-SIGNER-CORRECTION-LOCAL-001 — audited fixture identity policy is locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted workflow, oracle, + HTML-plan, and checklist correction atop exact RED head + `85731b3feff90047f67b716363b1549b0d79ee2c` on Apple arm64, macOS 26.2 build 25C56, + Node 26.5.0, pnpm 10.24.0. Native Windows execution remains required. +- Correction: retain both immutable archive hashes, the exact 15-name PE closure, exactly one + `libcrypto.dll`, and both per-architecture library hashes. Require `Valid` target-native + Authenticode on every PE; require the expected Microsoft 3rd Party Application Component common + name and thumbprint `587116075365AA15BCD8E4FA9CB31BE372B5DE51` for the library; require the + expected Microsoft Corporation common name and thumbprint + `F5877012FBD62FABCBDC8D8CEE9C9585BA30DF79` or + `3F56A45111684D454E231CFDC4DA5C8D370F9816` for every executable. Missing certificates, invalid + status, identity drift, archive/hash/closure drift, or signer mismatch fails before any fixture + side effect. Diagnostics include bounded status/thumbprint/subject evidence. +- Focused purpose/oracle command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 10/10 workflow cases with one declared + live-suite skip in 10.19 seconds real; maximum RSS is 141,213,696 bytes and peak footprint is + 98,014,896 bytes. Independent YAML extraction and corrected job-key selection passes all three + start/measure/stop blocks through PowerShell `[scriptblock]::Create()`; the initial diagnostic + lookup used the nonexistent `build_windows` key, produced no code evidence, and was corrected to + `build-windows-runtime` before this claim. +- Release-contract command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 --reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` + passes 50 files and 283/283 cases in 24.21 seconds real; maximum RSS is 195,559,424 bytes and peak + footprint is 97,654,664 bytes. +- Static evidence: `/usr/bin/time -l pnpm typecheck` passes in 3.66 seconds real; maximum RSS is + 1,254,244,352 bytes and peak footprint is 97,130,352 bytes. `/usr/bin/time -l pnpm lint` passes in + 25.32 seconds real with only the 26 pre-existing warnings, all 41 reliability gates, the 355-entry + max-lines ratchet, localization, and bundled-guide checks; maximum RSS is 2,103,279,616 bytes and + peak footprint is 97,343,416 bytes. Targeted `oxfmt`, `git diff --check`, and exact protected-file + equality against `85731b3feff90047f67b716363b1549b0d79ee2c` pass; only the workflow, its oracle, + and three planning artifacts are modified. +- Residual boundary: local macOS evidence cannot prove Windows trust status/common-name formatting, + service/auth, Windows PowerShell 5.1, full-size transfer, cancellation, collision, connection reuse, + or teardown. Commit/push this isolated correction and require fresh exact-head x64/arm64 proof. + Legacy remains the sole production/default path; product/Beta/settings/fallback/tuple/publication/ + signing remain absent. ## Accepted Gaps diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 662022aa2d9..19f82c0183f 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1655,11 +1655,13 @@

    Prove production behavior, failure behavior, and rollback behavior.

    When the hosted ARM64 OS capability is unavailable, use one immutable official Microsoft Win32-OpenSSH release with exact per-architecture archive hashes. - Require valid Microsoft Authenticode on every executable. The release's sole - upstream native library, libcrypto.dll, is accepted only when it is - the only unsigned PE and matches its separately pinned per-architecture hash; - any added, missing, differently signed, or differently hashed native file fails - closed. + Require valid Microsoft Authenticode on every PE. The release's sole upstream + native library, libcrypto.dll, must match its separately pinned + per-architecture hash, expected Microsoft 3rd Party Application Component common + name, and exact audited leaf-certificate thumbprint. Every executable must match + the expected Microsoft Corporation common name and one of the audited leaf + thumbprints; any added, missing, differently signed, or differently hashed + native file fails closed. This trust exception provisions only the loopback CI server. Production clients From d28552f0d953e40d96a4c24cdcdb5ba7d85b7c9f Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 19:00:23 -0400 Subject: [PATCH 243/267] fix(ssh): enforce fixture ACL trustee closure Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 39 ++++++++-- .../ssh-relay-runtime-workflow.test.mjs | 5 ++ ...elease-implementation-checklist-summary.md | 14 +++- ...github-release-implementation-checklist.md | 75 ++++++++++++++++++- ...6-07-14-ssh-relay-github-release-plan.html | 6 +- 5 files changed, 130 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 0a2761f20a2..a4427b3fe85 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -1457,13 +1457,34 @@ jobs: $account = New-LocalUser -Name $fixtureUser -Password $password -PasswordNeverExpires ` -UserMayNotChangePassword -AccountNeverExpires $userSid = $account.SID.Value + $creatorSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value + $systemSid = 'S-1-5-18' + $administratorsSid = 'S-1-5-32-544' - function Set-FixtureAcl([string]$Path, [string[]]$Grants) { + function Set-FixtureAcl([string]$Path, [string[]]$Grants, [string[]]$ExpectedSids) { & icacls.exe $Path /inheritance:r | Out-Host if ($LASTEXITCODE -ne 0) { throw "Failed to remove inherited ACLs from $Path" } + # Why: key generation adds a creator ACE that /grant:r does not replace for other trustees. + & icacls.exe $Path /remove "*$creatorSid" | Out-Host + if ($LASTEXITCODE -ne 0) { throw "Failed to remove creator ACL from $Path" } $arguments = @($Path, '/grant:r') + $Grants & icacls.exe @arguments | Out-Host if ($LASTEXITCODE -ne 0) { throw "Failed to grant fixture ACLs on $Path" } + $acl = Get-Acl -LiteralPath $Path + $rules = @($acl.Access) + if (-not $acl.AreAccessRulesProtected -or @($rules | Where-Object { $_.IsInherited }).Count -ne 0) { + throw "Fixture ACL inheritance remains enabled on $Path" + } + if (@($rules | Where-Object { $_.AccessControlType -ne 'Allow' }).Count -ne 0) { + throw "Fixture ACL contains a deny rule on $Path" + } + $actualSids = @($rules | ForEach-Object { + $_.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value + } | Sort-Object -Unique) + $expected = @($ExpectedSids | Sort-Object -Unique) + if (($actualSids -join "`n") -ne ($expected -join "`n")) { + throw "Fixture ACL trustee closure changed on $Path`: $($actualSids -join ',')" + } } function Set-FixtureOwner([string]$Path, [string]$OwnerSid) { & icacls.exe $Path /setowner "*$OwnerSid" | Out-Host @@ -1471,8 +1492,13 @@ jobs: } $systemFull = '*S-1-5-18:(OI)(CI)F' $administratorsFull = '*S-1-5-32-544:(OI)(CI)F' - Set-FixtureAcl $fixture @($systemFull, $administratorsFull, "*$userSid`:RX") - Set-FixtureAcl $remoteRoot @($systemFull, $administratorsFull, "*$userSid`:(OI)(CI)M") + $fixtureTrustees = @($systemSid, $administratorsSid, $userSid) + Set-FixtureAcl $fixture @($systemFull, $administratorsFull, "*$userSid`:RX") $fixtureTrustees + Set-FixtureAcl $remoteRoot @( + $systemFull, + $administratorsFull, + "*$userSid`:(OI)(CI)M" + ) $fixtureTrustees $clientKey = Join-Path $fixture 'client-key' $hostKey = Join-Path $fixture 'host-key' @@ -1486,9 +1512,12 @@ jobs: '*S-1-5-18:F', '*S-1-5-32-544:F', "*$userSid`:R" - ) + ) $fixtureTrustees Set-FixtureOwner $authorizedKeys $userSid - Set-FixtureAcl $hostKey @('*S-1-5-18:F', '*S-1-5-32-544:F') + Set-FixtureAcl $hostKey @( + '*S-1-5-18:F', + '*S-1-5-32-544:F' + ) @($systemSid, $administratorsSid) Set-FixtureOwner $hostKey 'S-1-5-18' $port = 22224 diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 01e067261c3..f3ec13d255b 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -179,6 +179,11 @@ describe('SSH relay runtime artifact workflow', () => { expect(start.run).toContain("if ($nativeFile.Name -eq 'libcrypto.dll') { continue }") expect(start.run).toContain("$signature.Status -ne 'Valid'") expect(start.run).toContain('CN=Microsoft Corporation') + expect(start.run).toContain('[Security.Principal.WindowsIdentity]::GetCurrent().User.Value') + expect(start.run).toContain('& icacls.exe $Path /remove "*$creatorSid"') + expect(start.run).toContain('$acl.AreAccessRulesProtected') + expect(start.run).toContain('Fixture ACL trustee closure changed') + expect(start.run).toContain('@($systemSid, $administratorsSid)') expect(start.run).not.toContain('Add-WindowsCapability') expect(start.run).toContain('New-LocalUser') expect(start.run.indexOf("Join-Path $fixture 'account-owned'")).toBeLessThan( 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 91eec24aeed..33674095589 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 @@ -38,7 +38,19 @@ common name plus audited thumbprint `F5877012FBD62FABCBDC8D8CEE9C9585BA30DF79` o policy and is locally green: 10/10 focused workflow cases with one declared live skip, all three PowerShell blocks parsed, 283/283 artifact contracts, typecheck, full lint/reliability/max-lines, formatting, diff, and protected-resolver isolation. Commit/push the isolated correction and require -fresh exact-head native x64/arm64 proof. +fresh exact-head native x64/arm64 proof. Exact head +`130d57d951b5e5cfb0e1e21183ce116684924e2b`, artifact run `29540409361`, and x64 job +`87761260157` prove the complete exact-signer policy before exposing a narrower ACL RED: portable +OpenSSH rejects the host key because `icacls /grant:r` did not remove the key creator's explicit +runner-account ACE. Service start fails, and ownership-safe teardown passes. ARM64 job `87761260166` +independently reaches the same RED with creator SID +`S-1-5-21-1882319117-3219095328-2125279949-500`; its teardown also passes. Remove the known creator +SID, assert the exact allowed SID closure, and rerun. +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-ACL-CORRECTION-LOCAL-001` records that isolated +correction locally green: protected allow-only ACLs remove the creator and require exact trustee SID +closure; 10/10 focused workflow cases, all three PowerShell blocks, 283/283 artifact contracts, +typecheck, full lint/reliability/max-lines, format, diff, and protected-resolver isolation pass. Push +for fresh exact-head x64/arm64 proof. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown 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 37fb0a26dc7..b381fe1b99d 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: bounded official Windows OpenSSH fixture correction**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **EXACT-SIGNER FIXTURE TRUST LOCALLY GREEN / EXACT-HEAD NATIVE PROOF NEXT — 2026-07-15, Codex implementation owner** — exact head `85731b3feff90047f67b716363b1549b0d79ee2c`, artifact run `29539266437`, and x64/arm64 jobs `87757772975`/`87757772957` pass their pinned archive hashes, exact 15-file native closures, pinned `libcrypto.dll` hashes, prior runtime contracts/build/smoke, and full-size cache gates before both disproving only the guessed `NotSigned` policy: target-native Authenticode reports `libcrypto.dll` as `Valid`. Both ownership-safe teardowns pass before any SSH account/service creation. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-SIGNER-AUDIT-001` audits all 30 pinned PEs; `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-EXACT-SIGNER-CORRECTION-LOCAL-001` implements the exact common-name/thumbprint fixture policy and passes focused, PowerShell syntax, 283 artifact-contract, typecheck, lint/reliability/max-lines, format, diff, and isolation gates. Commit/push the isolated correction and obtain fresh exact-head native proof. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Session checkpoint: **PORTABLE HOST-KEY ACL CORRECTION LOCALLY GREEN / EXACT-HEAD PROOF NEXT — 2026-07-15, Codex implementation owner** — exact pushed head `130d57d951b5e5cfb0e1e21183ce116684924e2b`, artifact run `29540409361`, and x64/arm64 jobs `87761260157`/`87761260166` pass checkout/toolchain, 283 contracts, clean-build equality/smoke, full-size cache, both pinned archive/library hashes, exact 15-file closure, and every audited common-name/thumbprint check before independently exposing the same narrower RED. Portable OpenSSH rejects the generated host key because `icacls /grant:r` leaves each key creator's explicit runner-account ACE; both service starts fail and both ownership-safe teardowns pass. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-ACL-CORRECTION-LOCAL-001` removes the known creator SID from every protected fixture ACL, asserts protected/allow-only exact trustee SID closure before service start, and passes focused, PowerShell syntax, 283 artifact-contract, typecheck, lint/reliability/max-lines, format, diff, and isolation gates. Commit/push and obtain fresh x64/arm64 proof. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -18943,6 +18943,79 @@ config/vitest.config.ts --maxWorkers=1 --reporter=dot config/scripts/ssh-relay-r Legacy remains the sole production/default path; product/Beta/settings/fallback/tuple/publication/ signing remain absent. +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-ACL-CI-RED-001 — creator ACE survives grant replacement + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact pushed head + `130d57d951b5e5cfb0e1e21183ce116684924e2b`, artifact run + [29540409361](https://github.com/stablyai/orca/actions/runs/29540409361), x64 job `87761260157` + on `windows-2022`, runner `GitHub Actions 1000058197`; ARM64 job `87761260166` on + `windows-11-arm`, runner `GitHub Actions 1000058198`. +- Prior-boundary proof: checkout/toolchain, all 283 artifact contracts, exact Node inputs, two clean + runtime builds/equality/native smoke, full-size desktop extraction/cache, pinned archive and + `libcrypto.dll` hashes, exact 15-name native closure, `Valid` Authenticode, the exact third-party + library identity, and both exact accepted executable identities all pass. The signer correction is + therefore target-natively proven on x64. +- Exact RED: fixture account/key/config/service creation proceeds, then `Start-Service sshd` fails. + The captured portable OpenSSH diagnostic names explicit creator access for runner SID + `S-1-5-21-4137868374-391603265-4009536220-500` on `host-key`, reports `UNPROTECTED PRIVATE KEY +FILE`, ignores the key, and exits with no host keys. `icacls /inheritance:r` removed inherited + access, but `/grant:r` replaced only the named SYSTEM/Administrators grants and did not remove the + creator-specific explicit ACE added by key generation. +- Independent ARM64 RED: all the same prior runtime/cache/archive/closure/exact-signer gates pass. + `Start-Service sshd` fails in five seconds, and the portable diagnostic names its native runner's + distinct creator SID `S-1-5-21-1882319117-3219095328-2125279949-500` on `host-key`, followed by + the same `UNPROTECTED PRIVATE KEY FILE` and no-host-keys exit. This confirms an + architecture-independent ACL-construction defect rather than a binary/signer difference. +- Cleanup: the two-second `always()` teardown passes, deletes the owned fixed-name service, account, + client state, and ownership-marked fixture root, and reports no cleanup failure. ARM64's teardown + independently passes in four seconds. No SSH connection or transfer begins on either architecture. +- Evidence-driven correction: retain all signer/archive/closure gates. Remove the known current + creator SID from each protected fixture ACL before applying exact grants, then enumerate ACL + trustees by SID and fail unless the resulting set exactly matches the path's declared SYSTEM, + Administrators, and optional fixture-user closure. Perform this before config validation/service + creation. The HTML plan records this fixture-only requirement; product runtime/transfer trust is + unchanged. +- Residual boundary: implement only exact fixture ACL closure plus its workflow oracle, rerun + local/static/PowerShell gates, and require fresh exact-head x64/arm64 live + service/auth/full-size/cancellation/collision/connection-reuse/cleanup proof. Legacy remains the + sole production/default path; product/Beta/settings/fallback/tuple/publication/signing remain absent. + +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-ACL-CORRECTION-LOCAL-001 — creator-free exact fixture ACL closure is locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted workflow, oracle, + HTML-plan, and checklist correction atop exact RED head + `130d57d951b5e5cfb0e1e21183ce116684924e2b` on Apple arm64, macOS 26.2 build 25C56, + Node 26.5.0, pnpm 10.24.0. Native Windows execution remains required. +- Correction: capture the current Windows creator SID, protect each fixture DACL from inheritance, + remove every ACE for that known creator, and apply only the declared SYSTEM, Administrators, and + optional fixture-user grants. Before config validation/service creation, `Get-Acl` must report a + protected DACL, zero inherited rules, allow-only rules, and an exact sorted SID trustee set. The + host key permits only SYSTEM/Administrators; the fixture root, remote root, and `authorized_keys` + also permit only the fixture user. Existing exact owner assignments remain unchanged. +- Focused purpose/oracle command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 10/10 workflow cases with one declared + live-suite skip in 2.61 seconds real; maximum RSS is 141,869,056 bytes and peak footprint is + 97,261,400 bytes. Independent YAML extraction passes all three start/measure/stop blocks through + PowerShell `[scriptblock]::Create()`. +- Release-contract command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 --reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` + passes 50 files and 283/283 cases in 16.78 seconds real; maximum RSS is 195,395,584 bytes and peak + footprint is 98,047,736 bytes. +- Static evidence: `/usr/bin/time -l pnpm typecheck` passes in 4.02 seconds real; maximum RSS is + 1,280,770,048 bytes and peak footprint is 97,425,144 bytes. `/usr/bin/time -l pnpm lint` passes in + 20.52 seconds real with only the 26 pre-existing warnings, all 41 reliability gates, the 355-entry + max-lines ratchet, localization, and bundled-guide checks; maximum RSS is 2,103,885,824 bytes and + peak footprint is 97,015,616 bytes. Targeted formatting, `git diff --check`, and exact + protected-file equality against `130d57d951b5e5cfb0e1e21183ce116684924e2b` pass; only the fixture + workflow, its oracle, and three planning artifacts are modified. +- Residual boundary: local macOS syntax/contracts cannot prove Windows ACL mutation/translation, + service/auth, PowerShell 5.1, full-size transfer, cancellation, collision, connection reuse, or + teardown. Commit/push the isolated correction and require fresh exact-head x64/arm64 proof. Legacy + remains the sole production/default path; product/Beta/settings/fallback/tuple/publication/signing + remain absent. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 19f82c0183f..7bcff37f4e0 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1665,8 +1665,10 @@

    Prove production behavior, failure behavior, and rollback behavior.

    This trust exception provisions only the loopback CI server. Production clients - and SSH hosts perform no fixture download, and it does not weaken runtime - artifact, native-signing, transfer, install, or launch trust. + and SSH hosts perform no fixture download. Fixture host-key and authorized-key + ACL creation removes creator-specific access and asserts the exact allowed SID + closure before service start; none of this weakens runtime artifact, + native-signing, transfer, install, or launch trust. From b652c1ebcce3803443d84c89811146366cdbdd5b Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 19:29:25 -0400 Subject: [PATCH 244/267] fix(ssh): settle Windows system SSH fixture lifecycle Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 105 +++++++++++++----- .../ssh-relay-runtime-workflow.test.mjs | 9 +- ...elease-implementation-checklist-summary.md | 23 +++- ...github-release-implementation-checklist.md | 93 ++++++++++++++-- ...6-07-14-ssh-relay-github-release-plan.html | 11 +- src/main/ssh/ssh-connection.test.ts | 6 + src/main/ssh/ssh-connection.ts | 3 + 7 files changed, 208 insertions(+), 42 deletions(-) diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index a4427b3fe85..15aec733174 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -1457,19 +1457,40 @@ jobs: $account = New-LocalUser -Name $fixtureUser -Password $password -PasswordNeverExpires ` -UserMayNotChangePassword -AccountNeverExpires $userSid = $account.SID.Value - $creatorSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value $systemSid = 'S-1-5-18' $administratorsSid = 'S-1-5-32-544' - function Set-FixtureAcl([string]$Path, [string[]]$Grants, [string[]]$ExpectedSids) { - & icacls.exe $Path /inheritance:r | Out-Host - if ($LASTEXITCODE -ne 0) { throw "Failed to remove inherited ACLs from $Path" } - # Why: key generation adds a creator ACE that /grant:r does not replace for other trustees. - & icacls.exe $Path /remove "*$creatorSid" | Out-Host - if ($LASTEXITCODE -ne 0) { throw "Failed to remove creator ACL from $Path" } - $arguments = @($Path, '/grant:r') + $Grants - & icacls.exe @arguments | Out-Host - if ($LASTEXITCODE -ne 0) { throw "Failed to grant fixture ACLs on $Path" } + function New-FixtureAclRule( + [string]$Sid, + [Security.AccessControl.FileSystemRights]$Rights, + [Security.AccessControl.InheritanceFlags]$InheritanceFlags + ) { + $identity = [Security.Principal.SecurityIdentifier]::new($Sid) + return [Security.AccessControl.FileSystemAccessRule]::new( + $identity, + $Rights, + $InheritanceFlags, + [Security.AccessControl.PropagationFlags]::None, + [Security.AccessControl.AccessControlType]::Allow + ) + } + function Set-FixtureAcl( + [string]$Path, + [Security.AccessControl.FileSystemAccessRule[]]$AccessRules, + [string[]]$ExpectedSids + ) { + $item = Get-Item -LiteralPath $Path -Force + $security = if ($item.PSIsContainer) { + [Security.AccessControl.DirectorySecurity]::new() + } else { + [Security.AccessControl.FileSecurity]::new() + } + # Why: replacing the DACL atomically prevents drive-specific defaults from surviving. + $security.SetAccessRuleProtection($true, $false) + foreach ($accessRule in $AccessRules) { + [void]$security.AddAccessRule($accessRule) + } + Set-Acl -LiteralPath $Path -AclObject $security $acl = Get-Acl -LiteralPath $Path $rules = @($acl.Access) if (-not $acl.AreAccessRulesProtected -or @($rules | Where-Object { $_.IsInherited }).Count -ne 0) { @@ -1490,14 +1511,19 @@ jobs: & icacls.exe $Path /setowner "*$OwnerSid" | Out-Host if ($LASTEXITCODE -ne 0) { throw "Failed to set fixture owner on $Path" } } - $systemFull = '*S-1-5-18:(OI)(CI)F' - $administratorsFull = '*S-1-5-32-544:(OI)(CI)F' + $noInheritance = [Security.AccessControl.InheritanceFlags]::None + $childInheritance = [Security.AccessControl.InheritanceFlags]::ContainerInherit -bor ` + [Security.AccessControl.InheritanceFlags]::ObjectInherit $fixtureTrustees = @($systemSid, $administratorsSid, $userSid) - Set-FixtureAcl $fixture @($systemFull, $administratorsFull, "*$userSid`:RX") $fixtureTrustees + Set-FixtureAcl $fixture @( + (New-FixtureAclRule $systemSid FullControl $childInheritance), + (New-FixtureAclRule $administratorsSid FullControl $childInheritance), + (New-FixtureAclRule $userSid ReadAndExecute $noInheritance) + ) $fixtureTrustees Set-FixtureAcl $remoteRoot @( - $systemFull, - $administratorsFull, - "*$userSid`:(OI)(CI)M" + (New-FixtureAclRule $systemSid FullControl $childInheritance), + (New-FixtureAclRule $administratorsSid FullControl $childInheritance), + (New-FixtureAclRule $userSid Modify $childInheritance) ) $fixtureTrustees $clientKey = Join-Path $fixture 'client-key' @@ -1509,14 +1535,14 @@ jobs: $authorizedKeys = Join-Path $fixture 'authorized_keys' Copy-Item -LiteralPath "$clientKey.pub" -Destination $authorizedKeys Set-FixtureAcl $authorizedKeys @( - '*S-1-5-18:F', - '*S-1-5-32-544:F', - "*$userSid`:R" + (New-FixtureAclRule $systemSid FullControl $noInheritance), + (New-FixtureAclRule $administratorsSid FullControl $noInheritance), + (New-FixtureAclRule $userSid Read $noInheritance) ) $fixtureTrustees Set-FixtureOwner $authorizedKeys $userSid Set-FixtureAcl $hostKey @( - '*S-1-5-18:F', - '*S-1-5-32-544:F' + (New-FixtureAclRule $systemSid FullControl $noInheritance), + (New-FixtureAclRule $administratorsSid FullControl $noInheritance) ) @($systemSid, $administratorsSid) Set-FixtureOwner $hostKey 'S-1-5-18' @@ -1638,6 +1664,11 @@ jobs: $ownsFixture = Test-Path -LiteralPath (Join-Path $fixture 'fixture-owned') $ownsService = Test-Path -LiteralPath (Join-Path $fixture 'service-owned') $ownsAccount = Test-Path -LiteralPath (Join-Path $fixture 'account-owned') + $account = if ($ownsAccount) { + Get-LocalUser -Name $fixtureUser -ErrorAction SilentlyContinue + } else { $null } + $userSid = if ($account) { $account.SID.Value } else { $null } + $profilePath = Join-Path $env:SystemDrive "Users/$fixtureUser" try { $service = Get-CimInstance Win32_Service -Filter "Name='$serviceName'" -ErrorAction SilentlyContinue $servicePid = if ($service) { [int]$service.ProcessId } else { 0 } @@ -1665,14 +1696,35 @@ jobs: $log = Join-Path $fixture 'sshd.log' if (Test-Path -LiteralPath $log) { Get-Content -LiteralPath $log -Tail 200 } try { - if ($ownsAccount -and (Get-LocalUser -Name $fixtureUser -ErrorAction SilentlyContinue)) { + if ($ownsAccount -and $userSid) { + $escapedSid = $userSid.Replace("'", "''") + $profileFailure = $null + for ($attempt = 1; $attempt -le 20; $attempt += 1) { + $profile = Get-CimInstance Win32_UserProfile -Filter "SID='$escapedSid'" ` + -ErrorAction SilentlyContinue + if (-not $profile) { break } + try { + Remove-CimInstance -InputObject $profile -ErrorAction Stop + $profileFailure = $null + } catch { + $profileFailure = $_.Exception.Message + } + Start-Sleep -Milliseconds 500 + } + if (Get-CimInstance Win32_UserProfile -Filter "SID='$escapedSid'" ` + -ErrorAction SilentlyContinue) { + throw "Fixture profile did not settle within 10 seconds: $profileFailure" + } + } + } catch { $failures.Add("profile teardown: $($_.Exception.Message)") } + try { + if ($account) { Remove-LocalUser -Name $fixtureUser -ErrorAction Stop } } catch { $failures.Add("account teardown: $($_.Exception.Message)") } try { - $profile = Join-Path $env:SystemDrive "Users/$fixtureUser" - if ($ownsAccount -and (Test-Path -LiteralPath $profile)) { - Remove-Item -LiteralPath $profile -Recurse -Force -ErrorAction Stop + if ($ownsAccount -and (Test-Path -LiteralPath $profilePath)) { + Remove-Item -LiteralPath $profilePath -Recurse -Force -ErrorAction Stop } if ($ownsFixture -and (Test-Path -LiteralPath $fixture)) { Remove-Item -LiteralPath $fixture -Recurse -Force -ErrorAction Stop @@ -1681,6 +1733,9 @@ jobs: if ($ownsAccount -and (Get-LocalUser -Name $fixtureUser -ErrorAction SilentlyContinue)) { $failures.Add('fixture account remains registered') } + if ($ownsAccount -and (Test-Path -LiteralPath $profilePath)) { + $failures.Add('fixture profile directory remains') + } if ($failures.Count -gt 0) { throw ($failures -join '; ') } - name: Upload unpublished artifact evidence diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index f3ec13d255b..273c61b10d0 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -179,8 +179,10 @@ describe('SSH relay runtime artifact workflow', () => { expect(start.run).toContain("if ($nativeFile.Name -eq 'libcrypto.dll') { continue }") expect(start.run).toContain("$signature.Status -ne 'Valid'") expect(start.run).toContain('CN=Microsoft Corporation') - expect(start.run).toContain('[Security.Principal.WindowsIdentity]::GetCurrent().User.Value') - expect(start.run).toContain('& icacls.exe $Path /remove "*$creatorSid"') + expect(start.run).toContain('[Security.AccessControl.DirectorySecurity]::new()') + expect(start.run).toContain('[Security.AccessControl.FileSecurity]::new()') + expect(start.run).toContain('$security.SetAccessRuleProtection($true, $false)') + expect(start.run).toContain('[void]$security.AddAccessRule($accessRule)') expect(start.run).toContain('$acl.AreAccessRulesProtected') expect(start.run).toContain('Fixture ACL trustee closure changed') expect(start.run).toContain('@($systemSid, $administratorsSid)') @@ -215,6 +217,9 @@ describe('SSH relay runtime artifact workflow', () => { expect(stop.run).not.toContain('service-backup.json') expect(stop.run).not.toContain('sc.exe config') expect(stop.run).toContain('sc.exe delete') + expect(stop.run).toContain('Remove-CimInstance -InputObject $profile') + expect(stop.run).toContain('Fixture profile did not settle within 10 seconds') + expect(stop.run).toContain('fixture profile directory remains') expect(stop.run).toContain('Remove-LocalUser') }) 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 33674095589..3cb25f2833a 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 @@ -9,8 +9,8 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — bounded official -Windows OpenSSH fixture correction, 2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — exact-head Windows +OpenSSH live-proof diagnosis and fixture correction, 2026-07-15, Codex implementation owner.** `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain @@ -50,7 +50,24 @@ SID, assert the exact allowed SID closure, and rerun. correction locally green: protected allow-only ACLs remove the creator and require exact trustee SID closure; 10/10 focused workflow cases, all three PowerShell blocks, 283/283 artifact contracts, typecheck, full lint/reliability/max-lines, format, diff, and protected-resolver isolation pass. Push -for fresh exact-head x64/arm64 proof. +for fresh exact-head x64/arm64 proof. Exact head +`d28552f0d953e40d96a4c24cdcdb5ba7d85b7c9f` is pushed; artifact run `29541119819` is RED after +independently proving all archive/hash/closure/signer/build/cache gates. Windows x64 job +`87763404467` starts OpenSSH 10.0p2, proves PowerShell 5.1 and pinned-key authentication, then the +production system-SSH connection probe times out before transfer metrics; teardown also detects a +loaded fixture profile instead of silently leaving it. Windows ARM64 job `87763404492` stops earlier +at the exact ACL oracle because the C: fixture root retains explicit Authenticated Users and Users +ACEs in addition to the intended SYSTEM, Administrators, and fixture-user closure; ownership-safe +teardown passes. Diagnose and correct both fixture-exposed contracts, preserving connection reuse +and bounded deterministic teardown, then require fresh exact-head x64/arm64 proof. +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-EXACT-HEAD-CORRECTION-LOCAL-001` records the three bounded +corrections locally green: the no-input production probe closes native OpenSSH stdin, fixture DACLs +are atomically replaced with exact protected allow-only rules independent of drive defaults, and an +owned `Win32_UserProfile` is deleted through a bounded native lifecycle before account/filesystem +teardown. Focused tests pass 71 cases with one declared live skip, all three PowerShell blocks parse, +all 283 artifact contracts pass, typecheck and full lint/reliability/max-lines pass, formatting/diff +are clean, and the protected resolver files remain unchanged. Commit/push this isolated package and +require fresh x64/arm64 live proof before advancing. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown 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 b381fe1b99d..16d1c3936a4 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: bounded official Windows OpenSSH fixture correction**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **PORTABLE HOST-KEY ACL CORRECTION LOCALLY GREEN / EXACT-HEAD PROOF NEXT — 2026-07-15, Codex implementation owner** — exact pushed head `130d57d951b5e5cfb0e1e21183ce116684924e2b`, artifact run `29540409361`, and x64/arm64 jobs `87761260157`/`87761260166` pass checkout/toolchain, 283 contracts, clean-build equality/smoke, full-size cache, both pinned archive/library hashes, exact 15-file closure, and every audited common-name/thumbprint check before independently exposing the same narrower RED. Portable OpenSSH rejects the generated host key because `icacls /grant:r` leaves each key creator's explicit runner-account ACE; both service starts fail and both ownership-safe teardowns pass. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-ACL-CORRECTION-LOCAL-001` removes the known creator SID from every protected fixture ACL, asserts protected/allow-only exact trustee SID closure before service start, and passes focused, PowerShell syntax, 283 artifact-contract, typecheck, lint/reliability/max-lines, format, diff, and isolation gates. Commit/push and obtain fresh x64/arm64 proof. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: exact-head Windows OpenSSH live-proof diagnosis and fixture correction**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **WINDOWS ACL/PROBE/PROFILE CORRECTION LOCALLY GREEN / EXACT-HEAD PROOF NEXT — 2026-07-15, Codex implementation owner** — exact pushed head `d28552f0d953e40d96a4c24cdcdb5ba7d85b7c9f`, artifact run `29541119819`, and x64/arm64 jobs `87763404467`/`87763404492` independently pass checkout/toolchain, 283 contracts, clean-build equality/smoke, full-size cache, both pinned archive/library hashes, exact 15-file closure, and every audited signer check before exposing three narrower gaps. ARM64's C: fixture root retains explicit Authenticated Users and Users ACEs; x64's no-input production `ssh.exe` probe leaves piped stdin open and waits 30 seconds after the remote command closes; x64 teardown finds the owned profile's loaded `UsrClass.dat`. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-EXACT-HEAD-CORRECTION-LOCAL-001` atomically replaces each fixture DACL with exact protected allow-only rules, closes stdin for the no-input production probe without disabling connection reuse, and deletes the owned `Win32_UserProfile` through a bounded native lifecycle before account/filesystem cleanup. Focused, PowerShell syntax, 283-contract, typecheck, full lint/reliability/max-lines, formatting, diff, and protected-resolver isolation gates pass locally. Commit/push and require fresh x64/arm64 live proof. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -19016,6 +19016,82 @@ config/vitest.config.ts --maxWorkers=1 --reporter=dot config/scripts/ssh-relay-r remains the sole production/default path; product/Beta/settings/fallback/tuple/publication/signing remain absent. +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-EXACT-HEAD-CI-RED-001 — native proof exposes ACL replacement, reuse-handshake, and profile-lifecycle gaps + +- Date/owner/source: 2026-07-15, Codex implementation owner; exact pushed head + `d28552f0d953e40d96a4c24cdcdb5ba7d85b7c9f`, artifact run `29541119819`, native x64 job + `87763404467` (`windows-2022`, Windows Server 2022 build 20348) and native ARM64 job + `87763404492` (`windows-11-arm`). +- Shared green boundary: both jobs pass exact checkout/toolchain, all 283 runtime artifact contracts, + clean-build equality/smoke, full-size client extraction/cache measurement, both exact portable + archive and `libcrypto.dll` hashes, exact 15-file closure, and every audited Authenticode common + name/thumbprint check. This preserves all earlier build/trust evidence. +- ARM64 RED: fixture download and extraction pass, then the pre-service exact ACL oracle rejects the + fixture root. Its protected allow-only trustee set is `S-1-5-11`, `S-1-5-18`, fixture user + `S-1-5-21-1882319117-3219095328-2125279949-1001`, `S-1-5-32-544`, and `S-1-5-32-545`. + Authenticated Users and Users are undeclared explicit entries inherited from that runner volume's + creation defaults; removing only the creator SID cannot establish exact closure. The service and + transfer never start, and the ownership-safe teardown passes. +- X64 RED: the same ACL oracle passes, the official server starts as + `OpenSSH_for_Windows_10.0p2 Win32-OpenSSH-GitHub, LibreSSL 4.2.0`, Windows PowerShell is + `5.1.20348.4294`, exact host-key trust and fixture-user public-key authentication pass. The + full-size suite then fails after roughly 30 seconds at `src/main/ssh/ssh-connection.ts:750` with + `System SSH connection timed out`, before serial transfer metrics. Server logs show an + authenticated session closing/read error plus a later pre-auth reset. Preserve + `systemSshConnectionReuse: true`; connection reuse is a required contract, not a fixture option to + disable. +- X64 teardown RED: service/account cleanup runs, but direct recursive deletion rejects + `C:\Users\orca_ssh_fixture\AppData\Local\Microsoft\Windows\UsrClass.dat` because the profile is + still loaded. This is a deterministic fixture lifecycle gap. Do not ignore the locked profile or + weaken teardown; unload/remove it through a bounded native profile lifecycle and retain exact + absence assertions. +- Correction gate: replace each fixture DACL with the exact declared allow-only SID set independent + of volume defaults; isolate and prove the production system-SSH reuse handshake against Windows + OpenSSH; unload/remove the owned fixture profile boundedly before filesystem deletion. Update the + workflow oracle, parse all PowerShell blocks, rerun focused and 283-contract/static/isolation + gates, then require fresh exact-head x64/arm64 service/auth/full-size/cancellation/collision/reuse/ + teardown proof. Legacy remains the sole production/default path and SignPath remains deferred. + +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-EXACT-HEAD-CORRECTION-LOCAL-001 — exact DACL, no-input probe, and owned-profile lifecycle are locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted correction atop exact + RED head `d28552f0d953e40d96a4c24cdcdb5ba7d85b7c9f` on Apple arm64, macOS 26.2 build 25C56, + Node 26.5.0, pnpm 10.24.0. Native Windows execution remains required. +- Correction: the production system-SSH connection probe now closes its no-input child stdin after + installing listeners. This lets native Windows `ssh.exe` settle after the remote echo closes and + changes no authentication, command, RPC, retry, fallback, or mux policy. The live fixture creates + a fresh `DirectorySecurity` or `FileSecurity` DACL and atomically installs only the declared + SYSTEM, Administrators, and optional fixture-user allow rules, so C:/D: defaults cannot survive. + Teardown uses the owned account SID to retry `Remove-CimInstance` for `Win32_UserProfile` for at + most ten seconds before account and directory deletion, retaining exact absence assertions. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-connection.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 71/71 cases with the one declared native + live skip in 3.83 seconds real; maximum RSS is 162,643,968 bytes and peak footprint is 98,490,224 + bytes. Independent YAML extraction plus PowerShell `[scriptblock]::Create()` parses all three + start/measure/stop blocks. +- Release-contract command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 --reporter=dot +config/scripts/ssh-relay-runtime-*.test.mjs` passes 50 files and 283/283 cases in 83.86 seconds + real; maximum RSS is 195,739,648 bytes and peak footprint is 97,408,952 bytes. +- Static evidence: `/usr/bin/time -l pnpm typecheck` passes in 30.12 seconds real; maximum RSS is + 1,253,359,616 bytes and peak footprint is 97,130,256 bytes. The first full-lint attempt honestly + rejected the workflow oracle at 601 lines; one redundant `Get-CimInstance` string assertion was + removed while the `Remove-CimInstance`, bounded timeout, exact profile-directory, and runtime + behavior assertions remain. The rerun `/usr/bin/time -l pnpm lint` passes in 73.17 seconds real + with only the 26 pre-existing warnings, all 41 reliability gates, the 355-entry max-lines ratchet, + localization, and bundled-guide checks; maximum RSS is 2,072,281,088 bytes and peak footprint is + 98,244,440 bytes. +- Isolation evidence: targeted `oxfmt --check`, `git diff --check`, and exact protected-file equality + against the base head pass. Only the artifact workflow/oracle, system-SSH connection/probe test, + and three planning artifacts are modified; the protected resolver files remain unchanged. +- Residual boundary: local macOS proof cannot execute Windows DACL replacement, native `ssh.exe` + stdin settlement, `Win32_UserProfile` deletion, service/auth, PowerShell 5.1, full-size transfer, + cancellation, collision, or teardown. Commit/push this isolated correction and require fresh + exact-head x64/arm64 proof before closing the live Windows gate. Legacy remains the sole + production/default path; product/Beta/settings/fallback/tuple/publication/signing remain absent. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -19074,12 +19150,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the locally green role-based fixture trust correction in -`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-LIBCRYPTO-CORRECTION-LOCAL-001`, then require fresh exact-head -x64/arm64 archive/native-closure/signature, ACL, PowerShell 5.1, exact full-size serial/four-channel/ -cancellation, collision, connection-reuse, and teardown evidence. Do not check the broad Milestone -6 Windows system-SSH item yet. Do not add an Electron/startup/product importer, per-target mode -wiring, fallback, tuple enablement, release publication, or default behavior. +Commit and push the locally green isolated correction in +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-EXACT-HEAD-CORRECTION-LOCAL-001`, then require fresh exact-head +x64/arm64 archive/native-closure/signature, ACL, PowerShell 5.1, exact +full-size serial/four-channel/cancellation, collision, connection-reuse, and teardown evidence. Do +not check the broad Milestone 6 Windows system-SSH item yet. Do not add an Electron/startup/product +importer, per-target mode wiring, fallback, tuple enablement, release publication, or default +behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 7bcff37f4e0..39e6db19cf1 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1642,7 +1642,8 @@

    Prove production behavior, failure behavior, and rollback behavior.

    Full-size runtime over SFTP and POSIX/Windows system SSH; tar/no-tar; no remote hash tools; corrupted upload; bundled-Node corruption/failure; bounded memory - and concurrency; MaxSessions=1; interrupted upload; stale lock + and concurrency; no-input system-SSH probe stdin/child settlement; + MaxSessions=1; interrupted upload; stale lock Bundled Node hashes the full staged tree before sentinel write; @@ -1666,9 +1667,11 @@

    Prove production behavior, failure behavior, and rollback behavior.

    This trust exception provisions only the loopback CI server. Production clients and SSH hosts perform no fixture download. Fixture host-key and authorized-key - ACL creation removes creator-specific access and asserts the exact allowed SID - closure before service start; none of this weakens runtime artifact, - native-signing, transfer, install, or launch trust. + ACL creation replaces volume-specific defaults with the exact allowed, + protected, allow-only SID closure before service start. Owned fixture profiles + are unloaded and removed through a bounded native lifecycle before exact + teardown assertions; none of this weakens runtime artifact, native-signing, + transfer, install, or launch trust. diff --git a/src/main/ssh/ssh-connection.test.ts b/src/main/ssh/ssh-connection.test.ts index 90807615000..b0c9194d992 100644 --- a/src/main/ssh/ssh-connection.test.ts +++ b/src/main/ssh/ssh-connection.test.ts @@ -932,6 +932,11 @@ describe('SshConnection', () => { it('uses system SSH transport when ProxyUseFdpass is resolved by OpenSSH', async () => { vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig()) + let probeChannel: ReturnType | undefined + spawnSystemSshCommandMock.mockImplementationOnce(() => { + probeChannel = createSystemCommandChannel() + return probeChannel + }) const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks()) await conn.connect() @@ -947,6 +952,7 @@ describe('SshConnection', () => { resolvedConfig: expect.objectContaining({ proxyUseFdpass: true }) } ) + expect(probeChannel?.stdin.end).toHaveBeenCalledOnce() }) it('allows concurrent exec commands for system SSH with an Orca ControlMaster socket', async () => { diff --git a/src/main/ssh/ssh-connection.ts b/src/main/ssh/ssh-connection.ts index a1e55027080..a8f11275c0b 100644 --- a/src/main/ssh/ssh-connection.ts +++ b/src/main/ssh/ssh-connection.ts @@ -755,6 +755,9 @@ export class SshConnection { channel.stderr.on('data', onStderrData) channel.on('error', onError) channel.on('close', onClose) + // Why: native Windows OpenSSH keeps its input worker alive while the + // pipe is open, even though this probe can never consume stdin. + ;(channel as ClientChannel & { stdin: NodeJS.WritableStream }).stdin.end() }) } catch (err) { this.useSystemSshTransport = false From 8ae6dcf6450b5f081788357dd8fe776293fb0cfe Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 19:54:23 -0400 Subject: [PATCH 245/267] fix(ssh): detach Windows no-input probe Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 31 +++++++ ...time-windows-openssh-workflow-contract.mjs | 93 +++++++++++++++++++ .../ssh-relay-runtime-workflow.test.mjs | 81 +--------------- ...elease-implementation-checklist-summary.md | 24 ++++- ...github-release-implementation-checklist.md | 81 +++++++++++++++- ...6-07-14-ssh-relay-github-release-plan.html | 5 +- src/main/ssh/ssh-connection.test.ts | 7 +- src/main/ssh/ssh-connection.ts | 8 +- src/main/ssh/ssh-system-fallback.test.ts | 17 ++++ src/main/ssh/system-ssh-args.ts | 9 ++ 10 files changed, 269 insertions(+), 87 deletions(-) create mode 100644 config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 15aec733174..462649ee579 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -1697,9 +1697,40 @@ jobs: if (Test-Path -LiteralPath $log) { Get-Content -LiteralPath $log -Tail 200 } try { if ($ownsAccount -and $userSid) { + function Dismount-FixtureRegistryHive([string]$hiveName) { + $registryPath = "Registry::HKEY_USERS\$hiveName" + if (-not (Test-Path -LiteralPath $registryPath)) { return } + $regPath = Join-Path $env:SystemRoot 'System32/reg.exe' + $process = Start-Process -FilePath $regPath ` + -ArgumentList @('unload', "HKU\$hiveName") -PassThru -WindowStyle Hidden + try { + if (-not $process.WaitForExit(2000)) { + $process.Kill() + if (-not $process.WaitForExit(2000)) { + throw "Fixture registry hive unload did not stop: $hiveName" + } + throw "Fixture registry hive unload timed out: $hiveName" + } + if ($process.ExitCode -ne 0) { + throw "Fixture registry hive unload failed ($($process.ExitCode)): $hiveName" + } + } finally { + $process.Dispose() + } + } $escapedSid = $userSid.Replace("'", "''") $profileFailure = $null for ($attempt = 1; $attempt -le 20; $attempt += 1) { + try { + # Why: the fixture's UsrClass hive can outlive sshd; unload only + # this owned SID before asking Windows to delete its profile. + Dismount-FixtureRegistryHive "${userSid}_Classes" + Dismount-FixtureRegistryHive $userSid + } catch { + $profileFailure = $_.Exception.Message + Start-Sleep -Milliseconds 500 + continue + } $profile = Get-CimInstance Win32_UserProfile -Filter "SID='$escapedSid'" ` -ErrorAction SilentlyContinue if (-not $profile) { break } diff --git a/config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs b/config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs new file mode 100644 index 00000000000..fd0569834dd --- /dev/null +++ b/config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs @@ -0,0 +1,93 @@ +export function assertWindowsOpenSshWorkflow(workflow, expect) { + const steps = workflow.jobs['build-windows-runtime'].steps + const start = steps.find( + (step) => step.name === 'Start loopback Windows OpenSSH system-SSH fixture' + ) + const measure = steps.find( + (step) => step.name === 'Measure exact full-size runtime over Windows system SSH' + ) + const stop = steps.find( + (step) => step.name === 'Stop loopback Windows OpenSSH system-SSH fixture' + ) + + expect(start).toBeDefined() + expect(measure).toBeDefined() + expect(stop).toBeDefined() + expect(start.shell).toBe('pwsh') + expect(start.run).toContain('10.0.0.0p2-Preview') + expect(start.run).toContain('23f50f3458c4c5d0b12217c6a5ddfde0137210a30fa870e98b29827f7b43aba5') + expect(start.run).toContain('698c6aec31c1dd0fb996206e8741f4531a97355686b5431ef347d531b07fcd42') + expect(start.run).toContain('4652e861c0335ee80a51306ceab75aa35c8865b235f97ce7dd5a0fd9dab44b5d') + expect(start.run).toContain('7ad2b7721893c54ad6e4fec1a3477701fb48975323c2c4ac6cd0b8c972ab242a') + expect(start.run).toContain('--connect-timeout 20 --max-time 300 --retry 2') + expect(start.run).toContain('Get-AuthenticodeSignature') + expect(start.run).toContain('$actualNativeNames -join "`n"') + expect(start.run).toContain('$expectedNativeNames -join "`n"') + expect(start.run).toContain("'ssh-sk-helper.exe'") + expect(start.run).toContain("'sshd-session.exe'") + expect(start.run).toContain('$libcryptoFiles.Count -ne 1') + expect(start.run).toContain("$libcryptoSignature.Status -ne 'Valid'") + expect(start.run).toContain('587116075365AA15BCD8E4FA9CB31BE372B5DE51') + expect(start.run).toContain('CN=Microsoft 3rd Party Application Component') + expect(start.run).toContain('F5877012FBD62FABCBDC8D8CEE9C9585BA30DF79') + expect(start.run).toContain('3F56A45111684D454E231CFDC4DA5C8D370F9816') + expect(start.run).toContain('$expectedExecutableSignerThumbprints -notcontains $thumbprint') + expect(start.run).not.toContain("'NotSigned'") + expect(start.run).toContain("if ($nativeFile.Name -eq 'libcrypto.dll') { continue }") + expect(start.run).toContain("$signature.Status -ne 'Valid'") + expect(start.run).toContain('CN=Microsoft Corporation') + expect(start.run).toContain('[Security.AccessControl.DirectorySecurity]::new()') + expect(start.run).toContain('[Security.AccessControl.FileSecurity]::new()') + expect(start.run).toContain('$security.SetAccessRuleProtection($true, $false)') + expect(start.run).toContain('[void]$security.AddAccessRule($accessRule)') + expect(start.run).toContain('$acl.AreAccessRulesProtected') + expect(start.run).toContain('Fixture ACL trustee closure changed') + expect(start.run).toContain('@($systemSid, $administratorsSid)') + expect(start.run).not.toContain('Add-WindowsCapability') + expect(start.run).toContain('New-LocalUser') + expect(start.run.indexOf("Join-Path $fixture 'account-owned'")).toBeLessThan( + start.run.indexOf('New-LocalUser') + ) + expect(start.run).toContain('ListenAddress 127.0.0.1') + expect(start.run).toContain('PasswordAuthentication no') + expect(start.run).toContain('StrictModes yes') + expect(start.run).toContain('StrictHostKeyChecking=yes') + expect(start.run).toContain('powershell.exe') + expect(start.run).toContain('([Version]$remotePowerShellVersion).Major -ne 5') + expect(start.run).toContain('/setowner') + expect(start.run).toContain("Set-FixtureOwner $hostKey 'S-1-5-18'") + expect(start.run).toContain('Set-FixtureOwner $authorizedKeys $userSid') + expect(start.run).not.toContain('administrators_authorized_keys') + expect(start.run).toContain('New-Service') + expect(start.run.indexOf("Join-Path $fixture 'service-owned'")).toBeLessThan( + start.run.indexOf('New-Service') + ) + expect(measure.shell).toBe('pwsh') + expect(measure.run).toContain("$env:ORCA_SSH_FORCE_SYSTEM_TRANSPORT = '1'") + expect(measure.run).toContain('ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts') + expect(measure.run).toContain("Join-Path $firstOutput 'runtime'") + expect(stop.if).toBe("always() && runner.os == 'Windows'") + expect(stop.shell).toBe('pwsh') + expect(stop.run).toContain('fixture-owned') + expect(stop.run).toContain('service-owned') + expect(stop.run).toContain('account-owned') + expect(stop.run).not.toContain('service-backup.json') + expect(stop.run).not.toContain('sc.exe config') + expect(stop.run).toContain('sc.exe delete') + expect(stop.run).toContain('Dismount-FixtureRegistryHive "${userSid}_Classes"') + expect(stop.run).toContain('Dismount-FixtureRegistryHive $userSid') + expect(stop.run).toContain("Join-Path $env:SystemRoot 'System32/reg.exe'") + expect(stop.run).toContain('@(\'unload\', "HKU\\$hiveName")') + expect(stop.run).toContain('$process.WaitForExit(2000)') + expect(stop.run).toContain('$process.Kill()') + expect(stop.run.indexOf('Dismount-FixtureRegistryHive "${userSid}_Classes"')).toBeLessThan( + stop.run.indexOf('Dismount-FixtureRegistryHive $userSid') + ) + expect(stop.run.indexOf('Dismount-FixtureRegistryHive $userSid')).toBeLessThan( + stop.run.indexOf('Remove-CimInstance -InputObject $profile') + ) + expect(stop.run).toContain('Remove-CimInstance -InputObject $profile') + expect(stop.run).toContain('Fixture profile did not settle within 10 seconds') + expect(stop.run).toContain('fixture profile directory remains') + expect(stop.run).toContain('Remove-LocalUser') +} diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 273c61b10d0..d7f964b688a 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises' import { parse } from 'yaml' import { describe, expect, it } from 'vitest' +import { assertWindowsOpenSshWorkflow } from './ssh-relay-runtime-windows-openssh-workflow-contract.mjs' const workflowUrl = new URL( '../../.github/workflows/ssh-relay-runtime-artifacts.yml', @@ -142,85 +143,7 @@ describe('SSH relay runtime artifact workflow', () => { it('runs full-size Windows system SSH against loopback native OpenSSH', async () => { const workflow = parse(await readFile(workflowUrl, 'utf8')) - const steps = workflow.jobs['build-windows-runtime'].steps - const start = steps.find( - (step) => step.name === 'Start loopback Windows OpenSSH system-SSH fixture' - ) - const measure = steps.find( - (step) => step.name === 'Measure exact full-size runtime over Windows system SSH' - ) - const stop = steps.find( - (step) => step.name === 'Stop loopback Windows OpenSSH system-SSH fixture' - ) - - expect(start).toBeDefined() - expect(measure).toBeDefined() - expect(stop).toBeDefined() - expect(start.shell).toBe('pwsh') - expect(start.run).toContain('10.0.0.0p2-Preview') - expect(start.run).toContain('23f50f3458c4c5d0b12217c6a5ddfde0137210a30fa870e98b29827f7b43aba5') - expect(start.run).toContain('698c6aec31c1dd0fb996206e8741f4531a97355686b5431ef347d531b07fcd42') - expect(start.run).toContain('4652e861c0335ee80a51306ceab75aa35c8865b235f97ce7dd5a0fd9dab44b5d') - expect(start.run).toContain('7ad2b7721893c54ad6e4fec1a3477701fb48975323c2c4ac6cd0b8c972ab242a') - expect(start.run).toContain('--connect-timeout 20 --max-time 300 --retry 2') - expect(start.run).toContain('Get-AuthenticodeSignature') - expect(start.run).toContain('$actualNativeNames -join "`n"') - expect(start.run).toContain('$expectedNativeNames -join "`n"') - expect(start.run).toContain("'ssh-sk-helper.exe'") - expect(start.run).toContain("'sshd-session.exe'") - expect(start.run).toContain('$libcryptoFiles.Count -ne 1') - expect(start.run).toContain("$libcryptoSignature.Status -ne 'Valid'") - expect(start.run).toContain('587116075365AA15BCD8E4FA9CB31BE372B5DE51') - expect(start.run).toContain('CN=Microsoft 3rd Party Application Component') - expect(start.run).toContain('F5877012FBD62FABCBDC8D8CEE9C9585BA30DF79') - expect(start.run).toContain('3F56A45111684D454E231CFDC4DA5C8D370F9816') - expect(start.run).toContain('$expectedExecutableSignerThumbprints -notcontains $thumbprint') - expect(start.run).not.toContain("'NotSigned'") - expect(start.run).toContain("if ($nativeFile.Name -eq 'libcrypto.dll') { continue }") - expect(start.run).toContain("$signature.Status -ne 'Valid'") - expect(start.run).toContain('CN=Microsoft Corporation') - expect(start.run).toContain('[Security.AccessControl.DirectorySecurity]::new()') - expect(start.run).toContain('[Security.AccessControl.FileSecurity]::new()') - expect(start.run).toContain('$security.SetAccessRuleProtection($true, $false)') - expect(start.run).toContain('[void]$security.AddAccessRule($accessRule)') - expect(start.run).toContain('$acl.AreAccessRulesProtected') - expect(start.run).toContain('Fixture ACL trustee closure changed') - expect(start.run).toContain('@($systemSid, $administratorsSid)') - expect(start.run).not.toContain('Add-WindowsCapability') - expect(start.run).toContain('New-LocalUser') - expect(start.run.indexOf("Join-Path $fixture 'account-owned'")).toBeLessThan( - start.run.indexOf('New-LocalUser') - ) - expect(start.run).toContain('ListenAddress 127.0.0.1') - expect(start.run).toContain('PasswordAuthentication no') - expect(start.run).toContain('StrictModes yes') - expect(start.run).toContain('StrictHostKeyChecking=yes') - expect(start.run).toContain('powershell.exe') - expect(start.run).toContain('([Version]$remotePowerShellVersion).Major -ne 5') - expect(start.run).toContain('/setowner') - expect(start.run).toContain("Set-FixtureOwner $hostKey 'S-1-5-18'") - expect(start.run).toContain('Set-FixtureOwner $authorizedKeys $userSid') - expect(start.run).not.toContain('administrators_authorized_keys') - expect(start.run).toContain('New-Service') - expect(start.run.indexOf("Join-Path $fixture 'service-owned'")).toBeLessThan( - start.run.indexOf('New-Service') - ) - expect(measure.shell).toBe('pwsh') - expect(measure.run).toContain("$env:ORCA_SSH_FORCE_SYSTEM_TRANSPORT = '1'") - expect(measure.run).toContain('ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts') - expect(measure.run).toContain("Join-Path $firstOutput 'runtime'") - expect(stop.if).toBe("always() && runner.os == 'Windows'") - expect(stop.shell).toBe('pwsh') - expect(stop.run).toContain('fixture-owned') - expect(stop.run).toContain('service-owned') - expect(stop.run).toContain('account-owned') - expect(stop.run).not.toContain('service-backup.json') - expect(stop.run).not.toContain('sc.exe config') - expect(stop.run).toContain('sc.exe delete') - expect(stop.run).toContain('Remove-CimInstance -InputObject $profile') - expect(stop.run).toContain('Fixture profile did not settle within 10 seconds') - expect(stop.run).toContain('fixture profile directory remains') - expect(stop.run).toContain('Remove-LocalUser') + assertWindowsOpenSshWorkflow(workflow, expect) }) it('uploads only the first output after two clean builds verify and compare', async () => { 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 3cb25f2833a..a6ab90cec81 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 @@ -66,8 +66,28 @@ are atomically replaced with exact protected allow-only rules independent of dri owned `Win32_UserProfile` is deleted through a bounded native lifecycle before account/filesystem teardown. Focused tests pass 71 cases with one declared live skip, all three PowerShell blocks parse, all 283 artifact contracts pass, typecheck and full lint/reliability/max-lines pass, formatting/diff -are clean, and the protected resolver files remain unchanged. Commit/push this isolated package and -require fresh x64/arm64 live proof before advancing. +are clean, and the protected resolver files remain unchanged. Exact head +`b652c1ebcce3803443d84c89811146366cdbdd5b` is pushed. Artifact run `29542292712` preserves all +non-Windows gates and independently reaches the Windows fixture on x64, but job `87766845800` is +RED: exact ACL replacement, official OpenSSH setup, authentication, and PowerShell 5.1 pass before +the production probe times out after 32.064 seconds. Closing the Node stdin pipe is insufficient; +the no-input probe needs native OpenSSH `-n` without disabling connection reuse. Teardown also +finds the owned fixture SID's `UsrClass.dat` hive still loaded after the bounded profile-removal +loop. Add probe-only `-n`, boundedly unload only that SID's `_Classes` and primary hives before +retrying `Win32_UserProfile` removal, and require fresh x64/arm64 live proof before advancing. +ARM64 job `87766845741` independently reaches the same boundary: official OpenSSH setup passes, +the production probe times out after 34.01 seconds, and teardown catches the same owned +`UsrClass.dat` lock after the bounded profile loop. This is a shared native OpenSSH/profile +lifecycle correction, not an x64-only exception. +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-NOINPUT-PROFILE-CORRECTION-LOCAL-001` records the narrow +correction locally green: only the no-input connection probe emits OpenSSH `-n` before `--`, reuse +flags remain enabled, ordinary commands exclude `-n`, and teardown boundedly unloads only the +fixture SID's `_Classes` and primary hives before retrying native profile deletion. The Windows +workflow oracle is split by concrete domain responsibility without a max-lines bypass. Focused +tests pass 115 cases with one declared native skip, all three PowerShell blocks parse, 283/283 +artifact contracts pass, typecheck and full lint/reliability/max-lines pass, formatting/diff are +clean, and the protected resolver files remain unchanged. Commit/push this isolated package and +require fresh native x64/arm64 proof before advancing. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown 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 16d1c3936a4..e02b21f3f04 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: exact-head Windows OpenSSH live-proof diagnosis and fixture correction**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **WINDOWS ACL/PROBE/PROFILE CORRECTION LOCALLY GREEN / EXACT-HEAD PROOF NEXT — 2026-07-15, Codex implementation owner** — exact pushed head `d28552f0d953e40d96a4c24cdcdb5ba7d85b7c9f`, artifact run `29541119819`, and x64/arm64 jobs `87763404467`/`87763404492` independently pass checkout/toolchain, 283 contracts, clean-build equality/smoke, full-size cache, both pinned archive/library hashes, exact 15-file closure, and every audited signer check before exposing three narrower gaps. ARM64's C: fixture root retains explicit Authenticated Users and Users ACEs; x64's no-input production `ssh.exe` probe leaves piped stdin open and waits 30 seconds after the remote command closes; x64 teardown finds the owned profile's loaded `UsrClass.dat`. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-EXACT-HEAD-CORRECTION-LOCAL-001` atomically replaces each fixture DACL with exact protected allow-only rules, closes stdin for the no-input production probe without disabling connection reuse, and deletes the owned `Win32_UserProfile` through a bounded native lifecycle before account/filesystem cleanup. Focused, PowerShell syntax, 283-contract, typecheck, full lint/reliability/max-lines, formatting, diff, and protected-resolver isolation gates pass locally. Commit/push and require fresh x64/arm64 live proof. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Session checkpoint: **WINDOWS NATIVE NO-INPUT/PROFILE-HIVE CORRECTION LOCALLY GREEN / EXACT-HEAD PROOF NEXT — 2026-07-15, Codex implementation owner** — exact head `b652c1ebcce3803443d84c89811146366cdbdd5b`, artifact run `29542292712`, x64 job `87766845800`, and ARM64 job `87766845741` preserve all prior artifact/build/cache/hash/closure/signer gates and independently expose the same native probe/profile gaps. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-NOINPUT-PROFILE-CORRECTION-LOCAL-001` adds probe-only OpenSSH `-n` before `--` while retaining connection reuse and excluding `-n` from ordinary commands; boundedly unloads only the fixture-owned SID's `_Classes` and primary hives before retrying native profile deletion; retains exact teardown assertions; and splits the Windows workflow oracle by concrete domain responsibility without a max-lines bypass. Focused tests, all three PowerShell parses, 283 artifact contracts, typecheck, full lint/reliability/max-lines, formatting, diff, and protected-resolver isolation pass locally. Commit/push this isolated package and require fresh x64/arm64 proof. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -19092,6 +19092,85 @@ config/scripts/ssh-relay-runtime-*.test.mjs` passes 50 files and 283/283 cases i exact-head x64/arm64 proof before closing the live Windows gate. Legacy remains the sole production/default path; product/Beta/settings/fallback/tuple/publication/signing remain absent. +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-NOINPUT-PROFILE-RED-001 — exact head proves ACL and exposes native no-input/profile-hive gaps + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact pushed head + `b652c1ebcce3803443d84c89811146366cdbdd5b`, artifact run `29542292712`, native x64 job + `87766845800` (`windows-2022`, Windows Server 2022) and native ARM64 job `87766845741` + (`windows-11-arm`). +- Preserved evidence: all four non-Windows primary jobs and both Linux supplement jobs pass. The + x64 job passes exact checkout/toolchain, 283 runtime artifact contracts, clean-build identity and + smoke, full-size cache, both pinned archive/library hashes, exact 15-file closure, all audited + signer checks, exact fixture DACL replacement, official OpenSSH service/authentication, and + PowerShell 5.1 before the live transfer suite. +- X64 RED: the production no-input connection probe times out after 32.064 seconds with + `System SSH connection timed out`. Closing the spawned process's Node stdin pipe does not make + native Windows OpenSSH detach its stdin worker. Preserve `systemSshConnectionReuse`; add + OpenSSH `-n` only to the no-input probe argv before `--`, and prove argv order plus connection + behavior. +- Teardown RED: the bounded ten-second `Remove-CimInstance Win32_UserProfile` loop cannot settle + while the owned fixture profile's `UsrClass.dat` remains loaded; profile and fixture directory + deletion then fail and exact absence assertions catch both. For the fixture-owned SID only, + boundedly unload `HKU\\<SID>_Classes` and then `HKU\\<SID>` with `reg.exe unload`, retry + native profile deletion, and retain exact account/profile/directory absence assertions. +- ARM64 independent RED: official OpenSSH fixture startup passes, then the same production probe + fails with `System SSH connection timed out` after 34.01 seconds. The teardown reports + `Fixture profile did not settle within 10 seconds`, then exact filesystem deletion identifies + `C:\Users\orca_ssh_fixture\AppData\Local\Microsoft\Windows\UsrClass.dat` as used by another + process and reports that the fixture profile directory remains. The same correction is therefore + required on both native Windows architectures. +- Promotion condition: update the workflow oracle, parse every PowerShell block, pass the focused, + 283-contract, typecheck, full-lint/reliability/max-lines, formatting, diff, and protected-resolver + isolation gates, then require fresh exact-head x64/arm64 live proof. Legacy remains the sole + production/default path and SignPath remains deferred. + +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-NOINPUT-PROFILE-CORRECTION-LOCAL-001 — probe-only native no-input and owned-hive teardown are locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted correction atop exact + RED head `b652c1ebcce3803443d84c89811146366cdbdd5b` on Apple arm64, macOS 26.2 build 25C56, + Node 26.5.0, pnpm 10.24.0. Native Windows execution remains required. +- Probe correction: `SystemSshBuildArgsOptions.noInput` emits OpenSSH `-n` before the `--` + destination terminator. Only the raw cross-shell connection probe requests it; ordinary + system-SSH commands are asserted to omit it. The probe still requests Orca ControlMaster, + `ControlPersist=300`, and keepalives, so this does not evade the required connection-reuse path. + The existing explicit stdin close remains as deterministic local pipe ownership. +- Teardown correction: for the fixture-created account SID only, the workflow checks and unloads + `HKU\\<SID>_Classes` before `HKU\\<SID>` through `%SystemRoot%\\System32\\reg.exe`. + Every process wait is bounded to two seconds, a stuck process is killed and boundedly awaited, + and failure is retried inside the existing ten-second profile lifecycle. Native + `Remove-CimInstance Win32_UserProfile`, account deletion, directory deletion, and all exact + absence assertions remain mandatory. +- Structure/oracle correction: Windows OpenSSH workflow assertions moved into the concrete + `config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs` responsibility while + `ssh-relay-runtime-workflow.test.mjs` remains the caller used by every historical command. The + files are 93 and 551 lines respectively; no max-lines disable or budget bump was added. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 3 files plus one declared native live + skip, 115 passed tests plus one skip, in 4.24 seconds real; maximum RSS is 158,269,440 bytes and + peak footprint is 97,392,376 bytes. Independent YAML extraction plus PowerShell + `[scriptblock]::Create()` parses all three start/measure/stop blocks. +- Release-contract command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 --reporter=dot +config/scripts/ssh-relay-runtime-*.test.mjs` passes 50 files and 283/283 cases in 21.65 seconds + real; maximum RSS is 195,526,656 bytes and peak footprint is 98,375,440 bytes. +- Static commands: `/usr/bin/time -l pnpm typecheck` passes in 6.75 seconds real with maximum RSS + 1,826,357,248 bytes and peak footprint 98,015,040 bytes. `/usr/bin/time -l pnpm lint` passes in + 22.61 seconds real with only the 26 pre-existing warnings, all 41 reliability gates, the + 355-entry max-lines ratchet, bundled guides, and localization checks; maximum RSS is + 2,036,318,208 bytes and peak footprint is 97,425,240 bytes. +- Isolation evidence: targeted `oxfmt --check` passes all ten changed code/workflow/plan files; + `git diff --check`, an independent no-index whitespace check for the new contract module, and + exact working-tree equality to `HEAD` for `ssh-remote-node-resolution.ts` and its test pass. Only + the artifact workflow, concrete workflow oracle split, no-input argv/probe implementation and + tests, and three planning artifacts are touched. +- Residual boundary: local macOS proof cannot execute native Windows `ssh.exe -n`, registry hive + unload, profile removal, service/auth, full-size transfer, cancellation, collision, or teardown. + Commit/push this isolated correction and require fresh exact-head x64/arm64 proof before closing + the live Windows gate. Legacy remains the sole production/default path; SignPath and product + caller/Beta/settings/fallback/tuple/publication work remain deferred. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 39e6db19cf1..9140560c1eb 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1642,7 +1642,10 @@

    Prove production behavior, failure behavior, and rollback behavior.

    Full-size runtime over SFTP and POSIX/Windows system SSH; tar/no-tar; no remote hash tools; corrupted upload; bundled-Node corruption/failure; bounded memory - and concurrency; no-input system-SSH probe stdin/child settlement; + and concurrency; no-input system-SSH probe uses native OpenSSH -n + before the destination terminator and proves child settlement without disabling + connection reuse; fixture-owned Windows profile hives are boundedly unloaded + before native profile deletion; MaxSessions=1; interrupted upload; stale lock diff --git a/src/main/ssh/ssh-connection.test.ts b/src/main/ssh/ssh-connection.test.ts index b0c9194d992..035520fcc49 100644 --- a/src/main/ssh/ssh-connection.test.ts +++ b/src/main/ssh/ssh-connection.test.ts @@ -949,6 +949,7 @@ describe('SshConnection', () => { 'echo ORCA-SYSTEM-SSH-OK', { wrapCommand: false, + noInput: true, resolvedConfig: expect.objectContaining({ proxyUseFdpass: true }) } ) @@ -1002,6 +1003,7 @@ describe('SshConnection', () => { 'echo ORCA-SYSTEM-SSH-OK', expect.objectContaining({ wrapCommand: false, + noInput: true, resolvedConfig: expect.objectContaining({ proxyUseFdpass: true }) }) ) @@ -1012,6 +1014,7 @@ describe('SshConnection', () => { expect.objectContaining({ disableControlMaster: true, wrapCommand: false, + noInput: true, resolvedConfig: expect.objectContaining({ proxyUseFdpass: true }) }) ) @@ -1050,7 +1053,7 @@ describe('SshConnection', () => { expect(spawnSystemSshCommandMock).toHaveBeenCalledWith( expect.objectContaining({ proxyCommand: 'ssh -W %h:%p bastion.example.com' }), 'echo ORCA-SYSTEM-SSH-OK', - { wrapCommand: false } + { wrapCommand: false, noInput: true } ) }) @@ -1071,7 +1074,7 @@ describe('SshConnection', () => { expect(spawnSystemSshCommandMock).toHaveBeenCalledWith( expect.objectContaining({ host: '192.168.0.210' }), 'echo ORCA-SYSTEM-SSH-OK', - { wrapCommand: false } + { wrapCommand: false, noInput: true } ) }) diff --git a/src/main/ssh/ssh-connection.ts b/src/main/ssh/ssh-connection.ts index a8f11275c0b..dacc2da40c7 100644 --- a/src/main/ssh/ssh-connection.ts +++ b/src/main/ssh/ssh-connection.ts @@ -694,7 +694,8 @@ export class SshConnection { // Why: this probe runs before remote platform detection. A raw echo works // under POSIX shells, cmd.exe, and PowerShell; `/bin/sh` wrapping does not. const channel = this.spawnTrackedSystemSshCommand('echo ORCA-SYSTEM-SSH-OK', { - wrapCommand: false + wrapCommand: false, + noInput: true }) try { await new Promise((resolve, reject) => { @@ -908,7 +909,10 @@ export class SshConnection { return new Error('SSH connection attempt was cancelled') } - private spawnTrackedSystemSshCommand(command: string, options?: SshExecOptions): ClientChannel { + private spawnTrackedSystemSshCommand( + command: string, + options?: SshExecOptions & Pick + ): ClientChannel { if (options?.signal?.aborted) { throw createSshOperationAbortError() } diff --git a/src/main/ssh/ssh-system-fallback.test.ts b/src/main/ssh/ssh-system-fallback.test.ts index 324a0ed6e48..b9b6c0940d6 100644 --- a/src/main/ssh/ssh-system-fallback.test.ts +++ b/src/main/ssh/ssh-system-fallback.test.ts @@ -391,9 +391,26 @@ describe('spawnSystemSsh', () => { expect(args).toContain('ServerAliveCountMax=3') }) + it('places no-input mode before the destination without disabling connection reuse', () => { + const args = buildSshArgs(createTarget(), { + noInput: true, + resolvedConfig: createResolvedConfig() + }) + const noInputIdx = args.indexOf('-n') + const terminatorIdx = args.indexOf('--') + + expect(noInputIdx).toBeGreaterThan(-1) + expect(noInputIdx).toBeLessThan(terminatorIdx) + expect(args).toContain('ControlMaster=auto') + expect(args).toContain('ControlPersist=300') + expect(args).not.toContain('-S') + }) + it('spawns a remote command through the system ssh target', () => { spawnSystemSshCommand(createTarget({ configHost: 'fdpass-host' }), 'echo hello') + const args = spawnMock.mock.calls[0][1] as string[] + expect(args).not.toContain('-n') expect(spawnMock).toHaveBeenCalledWith( SYSTEM_SSH_PATH, expect.arrayContaining(['--', 'deploy@fdpass-host', "exec /bin/sh -c 'echo hello'"]), diff --git a/src/main/ssh/system-ssh-args.ts b/src/main/ssh/system-ssh-args.ts index 8e812a0b68b..ab9391e3629 100644 --- a/src/main/ssh/system-ssh-args.ts +++ b/src/main/ssh/system-ssh-args.ts @@ -5,6 +5,7 @@ export type SystemSshBuildArgsOptions = { resolvedConfig?: SystemSshResolvedConfig | null disableControlMaster?: boolean suppressOrcaControlMaster?: boolean + noInput?: boolean } export function buildSshArgs(target: SshTarget, options?: SystemSshBuildArgsOptions): string[] { @@ -13,6 +14,11 @@ export function buildSshArgs(target: SshTarget, options?: SystemSshBuildArgsOpti args.push('-o', 'BatchMode=no') // Forward stdin/stdout for relay communication args.push('-T') + if (options?.noInput === true) { + // Why: closing a parent pipe does not reliably detach native Windows + // OpenSSH's input worker for commands that can never consume stdin. + args.push('-n') + } // Why: ControlMaster multiplexes all SSH exec commands over a single connection, // eliminating the ~9s handshake overhead per command. Without this, each @@ -91,6 +97,9 @@ export function getSystemSshBuildArgsFromOperationOptions( if (options?.suppressOrcaControlMaster === true) { buildArgsOptions.suppressOrcaControlMaster = true } + if (options?.noInput === true) { + buildArgsOptions.noInput = true + } return Object.keys(buildArgsOptions).length === 0 ? undefined : buildArgsOptions } From e4bcee3f3561e7cfd58aa90e7d35e6c597df8692 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 20:12:04 -0400 Subject: [PATCH 246/267] fix(ssh): settle Windows fixture sessions Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 33 ++++++++ ...time-windows-openssh-workflow-contract.mjs | 7 ++ ...elease-implementation-checklist-summary.md | 22 +++++- ...github-release-implementation-checklist.md | 76 ++++++++++++++++++- ...6-07-14-ssh-relay-github-release-plan.html | 8 +- src/main/ssh/ssh-connection.test.ts | 10 ++- src/main/ssh/ssh-connection.ts | 22 +++++- 7 files changed, 169 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 462649ee579..66e0ca250ef 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -1697,6 +1697,36 @@ jobs: if (Test-Path -LiteralPath $log) { Get-Content -LiteralPath $log -Tail 200 } try { if ($ownsAccount -and $userSid) { + function Get-FixtureOwnedProcesses([string]$fixtureSid) { + return @( + Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | ForEach-Object { + $owner = Invoke-CimMethod -InputObject $_ -MethodName GetOwnerSid ` + -ErrorAction SilentlyContinue + if ($owner.Sid -eq $fixtureSid) { $_ } + } + ) + } + function Stop-FixtureOwnedProcesses([string]$fixtureSid) { + for ($attempt = 1; $attempt -le 20; $attempt += 1) { + $ownedProcesses = @(Get-FixtureOwnedProcesses $fixtureSid) + if ($ownedProcesses.Count -eq 0) { return } + foreach ($ownedProcess in $ownedProcesses) { + Write-Output "fixture_owned_process=$($ownedProcess.Name):$($ownedProcess.ProcessId)" + try { + [void](Invoke-CimMethod -InputObject $ownedProcess -MethodName Terminate ` + -ErrorAction Stop) + } catch { + # The bounded ownership recheck handles processes that exit during termination. + } + } + Start-Sleep -Milliseconds 500 + } + $remaining = @(Get-FixtureOwnedProcesses $fixtureSid) + if ($remaining.Count -gt 0) { + $identities = $remaining | ForEach-Object { "$($_.Name):$($_.ProcessId)" } + throw "Fixture account processes did not settle within 10 seconds: $($identities -join ',')" + } + } function Dismount-FixtureRegistryHive([string]$hiveName) { $registryPath = "Registry::HKEY_USERS\$hiveName" if (-not (Test-Path -LiteralPath $registryPath)) { return } @@ -1718,6 +1748,9 @@ jobs: $process.Dispose() } } + # Why: sshd's service process can exit before a fixture-user + # session descendant releases the profile registry handles. + Stop-FixtureOwnedProcesses $userSid $escapedSid = $userSid.Replace("'", "''") $profileFailure = $null for ($attempt = 1; $attempt -le 20; $attempt += 1) { diff --git a/config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs b/config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs index fd0569834dd..7340f33e80a 100644 --- a/config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs +++ b/config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs @@ -74,6 +74,13 @@ export function assertWindowsOpenSshWorkflow(workflow, expect) { expect(stop.run).not.toContain('service-backup.json') expect(stop.run).not.toContain('sc.exe config') expect(stop.run).toContain('sc.exe delete') + expect(stop.run).toContain('Get-FixtureOwnedProcesses $fixtureSid') + expect(stop.run).toContain('$owner.Sid -eq $fixtureSid') + expect(stop.run).toContain('-MethodName Terminate') + expect(stop.run).toContain('Fixture account processes did not settle within 10 seconds') + expect(stop.run.indexOf('Stop-FixtureOwnedProcesses $userSid')).toBeLessThan( + stop.run.indexOf('Dismount-FixtureRegistryHive "${userSid}_Classes"') + ) expect(stop.run).toContain('Dismount-FixtureRegistryHive "${userSid}_Classes"') expect(stop.run).toContain('Dismount-FixtureRegistryHive $userSid') expect(stop.run).toContain("Join-Path $env:SystemRoot 'System32/reg.exe'") 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 a6ab90cec81..699cbe93487 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 @@ -86,8 +86,26 @@ fixture SID's `_Classes` and primary hives before retrying native profile deleti workflow oracle is split by concrete domain responsibility without a max-lines bypass. Focused tests pass 115 cases with one declared native skip, all three PowerShell blocks parse, 283/283 artifact contracts pass, typecheck and full lint/reliability/max-lines pass, formatting/diff are -clean, and the protected resolver files remain unchanged. Commit/push this isolated package and -require fresh native x64/arm64 proof before advancing. +clean, and the protected resolver files remain unchanged. Exact head +`8ae6dcf6450b5f081788357dd8fe776293fb0cfe` is pushed; artifact run `29543292239` is in progress +with native x64 job `87769913124` and ARM64 job `87769913148` assigned. Fresh service/auth/ +full-size/cancellation/collision/reuse/teardown proof remains required before advancing. +X64 job `87769913124` is RED after preserving every earlier gate and starting official OpenSSH: +the probe still reports `System SSH connection timed out` after 31.337 seconds despite `-n`. +Teardown proves the owned `_Classes` hive exists but `reg.exe unload` returns exit 1, then catches +the same locked `UsrClass.dat`. Record native process exit/stdout-end/sentinel/channel-close state +and settle fixture-user session processes before the hive loop. ARM64 job `87769913148` +independently reproduces the same boundary: probe timeout after 31.06 seconds, `_Classes` unload +exit 1, locked `UsrClass.dat`, and retained profile directory. Do not advance the milestone on +this result. +`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIFECYCLE-DIAGNOSTIC-LOCAL-001` is locally green: timeout errors +record only sentinel/stdout-end/process-exit/channel-close state, and fixture teardown enumerates +and boundedly terminates only processes whose native owner SID exactly matches the account created +by the fixture before attempting hive unload. Focused tests pass 115 cases with one native skip, +all three PowerShell blocks parse, 283/283 artifact contracts pass, typecheck and full +lint/reliability/max-lines pass, formatting/diff are clean, and protected resolvers are unchanged. +Commit/push and require fresh x64/arm64 lifecycle evidence; do not treat this local oracle as native +proof. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown 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 e02b21f3f04..bd5c9f6c21b 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: exact-head Windows OpenSSH live-proof diagnosis and fixture correction**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **WINDOWS NATIVE NO-INPUT/PROFILE-HIVE CORRECTION LOCALLY GREEN / EXACT-HEAD PROOF NEXT — 2026-07-15, Codex implementation owner** — exact head `b652c1ebcce3803443d84c89811146366cdbdd5b`, artifact run `29542292712`, x64 job `87766845800`, and ARM64 job `87766845741` preserve all prior artifact/build/cache/hash/closure/signer gates and independently expose the same native probe/profile gaps. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-NOINPUT-PROFILE-CORRECTION-LOCAL-001` adds probe-only OpenSSH `-n` before `--` while retaining connection reuse and excluding `-n` from ordinary commands; boundedly unloads only the fixture-owned SID's `_Classes` and primary hives before retrying native profile deletion; retains exact teardown assertions; and splits the Windows workflow oracle by concrete domain responsibility without a max-lines bypass. Focused tests, all three PowerShell parses, 283 artifact contracts, typecheck, full lint/reliability/max-lines, formatting, diff, and protected-resolver isolation pass locally. Commit/push this isolated package and require fresh x64/arm64 proof. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Session checkpoint: **WINDOWS NATIVE PROCESS/SESSION LIFECYCLE DIAGNOSTIC LOCALLY GREEN / EXACT-HEAD PROOF NEXT — 2026-07-15, Codex implementation owner** — exact head `b652c1ebcce3803443d84c89811146366cdbdd5b`, artifact run `29542292712`, x64 job `87766845800`, and ARM64 job `87766845741` preserve all prior artifact/build/cache/hash/closure/signer gates and independently expose the same native probe/profile gaps. Exact corrected head `8ae6dcf6450b5f081788357dd8fe776293fb0cfe`, run `29543292239`, x64 job `87769913124`, and ARM64 job `87769913148` remain RED: official OpenSSH starts, but probes still time out after 31.337/31.06 seconds and both fixture `_Classes` hive unloads return exit 1 before exact `UsrClass.dat`/directory failures. Win32 has no Orca control socket, so this is not an Orca ControlMaster retry path. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIFECYCLE-DIAGNOSTIC-LOCAL-001` adds non-sensitive sentinel/stdout-end/process-exit/channel-close timeout evidence and bounded exact-fixture-SID process settlement before hive removal; focused, PowerShell parse, 283-contract, typecheck, full lint/reliability/max-lines, formatting, diff, and protected-resolver gates pass locally. Commit/push and require fresh x64/arm64 lifecycle evidence. Do not advance Milestone 6. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -19171,6 +19171,80 @@ config/scripts/ssh-relay-runtime-*.test.mjs` passes 50 files and 283/283 cases i the live Windows gate. Legacy remains the sole production/default path; SignPath and product caller/Beta/settings/fallback/tuple/publication work remain deferred. +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-NOINPUT-LIFECYCLE-RED-001 — native no-input option alone does not settle x64 probe or owned profile + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact pushed head + `8ae6dcf6450b5f081788357dd8fe776293fb0cfe`, artifact run `29543292239`, native x64 job + `87769913124` (`windows-2022`, Windows Server 2022) and native ARM64 job `87769913148` + (`windows-11-arm`). +- Preserved evidence: 100 files / 854 tests pass with 18 declared skips in the native contract + suite; exact build identity/smoke, full-size client cache, portable archive/library hashes and + signer closure, exact fixture ACLs, official OpenSSH 10.0p2 startup, authentication, and + PowerShell 5.1 all pass before the live transfer suite. +- Probe RED: the exact full-size test fails after 31.337 seconds with + `System SSH connection timed out`; total Vitest duration is 32.28 seconds. Probe-only `-n` did + not establish child/channel settlement. `getControlSocketPath()` deliberately returns null on + win32, so Orca does not inject ControlMaster/ControlPersist for this client and disabling reuse + would not diagnose this failure. +- Teardown RED: the owned fixture SID is + `S-1-5-21-4137868374-391603265-4009536220-1003`; bounded `reg.exe unload` sees its `_Classes` + hive but returns exit 1 throughout the ten-second retry. Exact filesystem deletion then identifies + `C:\Users\orca_ssh_fixture\AppData\Local\Microsoft\Windows\UsrClass.dat` as in use and the + profile directory remains. The service PID settlement check alone is insufficient evidence that + every fixture-user `sshd-session.exe` or descendant has exited. +- ARM64 independent RED: official OpenSSH startup passes, then the exact full-size test times out + after 31.06 seconds with the same probe error. Its fixture SID + `S-1-5-21-1882319117-3219095328-2125279949-1001` has the same `_Classes` unload exit 1, + `UsrClass.dat` lock, and retained profile directory. The lifecycle gap is therefore shared by + both native Windows architectures. +- Correction gate: first add bounded, non-sensitive lifecycle evidence for native process exit, + stdout end, sentinel observation, and channel close. Before hive unload, enumerate and boundedly + settle only processes owned by the fixture account SID, retaining exact service/account/profile/ + directory absence assertions. Update the workflow oracle, parse all PowerShell blocks, rerun the + focused/283/static/format/isolation gates, then require fresh x64/arm64 proof. Legacy remains the + sole production/default path and SignPath remains deferred. + +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIFECYCLE-DIAGNOSTIC-LOCAL-001 — native probe state and exact-SID session settlement are locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted diagnostic package + atop exact RED head `8ae6dcf6450b5f081788357dd8fe776293fb0cfe` on Apple arm64, macOS 26.2 + build 25C56, Node 26.5.0, pnpm 10.24.0. Native Windows execution remains required. +- Probe evidence: the existing fail-closed 30-second timeout now reports whether the structured + sentinel was observed, stdout ended, the native child emitted an exit code, and the channel close + remained absent. The fields are booleans/an exit code only and expose no target, identity, path, + command, or credential. The focused test proves exact values and listener cleanup without changing + the success condition or legacy/default behavior. +- Fixture correction: after service PID settlement and before registry/profile removal, teardown + enumerates `Win32_Process`, calls native `GetOwnerSid`, and acts only when the result exactly + equals the SID of the fixture-created account. Termination and ownership rechecks are bounded to + twenty 500ms attempts; remaining process names/PIDs cause failure. `_Classes`/primary hive unload, + native profile deletion, account deletion, filesystem deletion, and exact absence assertions all + remain mandatory. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 3 files plus one declared native skip, + 115 passed tests plus one skip, in 4.34 seconds real; maximum RSS is 166,199,296 bytes and peak + footprint is 98,408,208 bytes. Independent YAML extraction plus PowerShell + `[scriptblock]::Create()` parses all three start/measure/stop blocks. +- Release-contract command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 --reporter=dot +config/scripts/ssh-relay-runtime-*.test.mjs` passes 50 files and 283/283 cases in 24.94 seconds + real; maximum RSS is 195,788,800 bytes and peak footprint is 97,654,544 bytes. +- Static commands: `/usr/bin/time -l pnpm typecheck` passes in 5.02 seconds real with maximum RSS + 1,236,762,624 bytes and peak footprint 97,965,912 bytes. `/usr/bin/time -l pnpm lint` passes in + 24.59 seconds real with only the 26 pre-existing warnings, all 41 reliability gates, the + 355-entry max-lines ratchet, bundled guides, and localization checks; maximum RSS is + 1,979,744,256 bytes and peak footprint is 97,572,672 bytes. +- Isolation evidence: targeted `oxfmt --check`, `git diff --check`, and exact working-tree equality + to `HEAD` for `ssh-remote-node-resolution.ts` and its test pass. Only the artifact workflow, + purpose-named Windows workflow oracle, system-SSH probe diagnostics/test, and three planning + artifacts are touched. +- Residual boundary: local macOS proof cannot execute native Windows process ownership/termination, + `ssh.exe` lifecycle, registry hive unload, profile removal, or the live transfer. Commit/push and + require fresh exact-head x64/arm64 proof. Legacy remains the sole production/default path; + SignPath and product caller/Beta/settings/fallback/tuple/publication work remain deferred. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 9140560c1eb..6d0ab9317cc 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1642,10 +1642,10 @@

    Prove production behavior, failure behavior, and rollback behavior.

    Full-size runtime over SFTP and POSIX/Windows system SSH; tar/no-tar; no remote hash tools; corrupted upload; bundled-Node corruption/failure; bounded memory - and concurrency; no-input system-SSH probe uses native OpenSSH -n - before the destination terminator and proves child settlement without disabling - connection reuse; fixture-owned Windows profile hives are boundedly unloaded - before native profile deletion; + and concurrency; no-input system-SSH probe records bounded native process exit, + stdout-end, sentinel, and channel-close settlement rather than treating OpenSSH + -n alone as proof; fixture-owned Windows session processes settle + before profile hives are boundedly unloaded and native profile deletion begins; MaxSessions=1; interrupted upload; stale lock diff --git a/src/main/ssh/ssh-connection.test.ts b/src/main/ssh/ssh-connection.test.ts index 035520fcc49..5e2488ddc74 100644 --- a/src/main/ssh/ssh-connection.test.ts +++ b/src/main/ssh/ssh-connection.test.ts @@ -1214,12 +1214,20 @@ describe('SshConnection', () => { const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks()) try { - const connect = expect(conn.connect()).rejects.toThrow('System SSH connection timed out') + const connect = expect(conn.connect()).rejects.toThrow( + 'System SSH connection timed out (sentinel=true, stdoutEnded=true, processExit=0, channelClosed=false)' + ) + await vi.advanceTimersByTimeAsync(0) + channel.emit('data', Buffer.from('ORCA-SYSTEM-SSH-OK\r\n')) + channel.emit('end') + channel.emit('exit', 0) await vi.advanceTimersByTimeAsync(30_000) await connect expect(channel.close).toHaveBeenCalled() expect(channel.listenerCount('data')).toBe(0) + expect(channel.listenerCount('end')).toBe(0) + expect(channel.listenerCount('exit')).toBe(0) expect(channel.listenerCount('error')).toBe(1) expect(channel.listenerCount('close')).toBe(1) expect(channel.stderr.listenerCount('data')).toBe(0) diff --git a/src/main/ssh/ssh-connection.ts b/src/main/ssh/ssh-connection.ts index dacc2da40c7..1da01e2f497 100644 --- a/src/main/ssh/ssh-connection.ts +++ b/src/main/ssh/ssh-connection.ts @@ -702,9 +702,14 @@ export class SshConnection { let stdout = '' let stderr = '' let settled = false + let sentinelObserved = false + let stdoutEnded = false + let processExit: number | null | 'not-observed' = 'not-observed' const cleanup = (): void => { clearTimeout(timeout) channel.off('data', onStdoutData) + channel.off('end', onStdoutEnd) + channel.off('exit', onProcessExit) channel.stderr.off('data', onStderrData) channel.off('error', onError) channel.off('close', onClose) @@ -720,6 +725,13 @@ export class SshConnection { } const onStdoutData = (data: Buffer): void => { stdout += data.toString('utf-8') + sentinelObserved = stdout.includes('ORCA-SYSTEM-SSH-OK') + } + const onStdoutEnd = (): void => { + stdoutEnded = true + } + const onProcessExit = (code: number | null): void => { + processExit = code } const onStderrData = (data: Buffer): void => { stderr += data.toString('utf-8') @@ -748,11 +760,19 @@ export class SshConnection { const timeout = setTimeout(() => { settle(() => { channel.close() - reject(new Error('System SSH connection timed out')) + // Why: native Windows can separate process exit from inherited + // stdio closure; these booleans diagnose that boundary safely. + reject( + new Error( + `System SSH connection timed out (sentinel=${sentinelObserved}, stdoutEnded=${stdoutEnded}, processExit=${processExit}, channelClosed=false)` + ) + ) }) }, CONNECT_TIMEOUT_MS) channel.on('data', onStdoutData) + channel.on('end', onStdoutEnd) + channel.on('exit', onProcessExit) channel.stderr.on('data', onStderrData) channel.on('error', onError) channel.on('close', onClose) From aab0775272aca2b03d55d045cbaf7aefc7511bfe Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 20:28:49 -0400 Subject: [PATCH 247/267] fix(ssh): detach no-input child stdin Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 17 +++++- ...github-release-implementation-checklist.md | 58 ++++++++++++++++++- ...6-07-14-ssh-relay-github-release-plan.html | 7 ++- src/main/ssh/ssh-system-fallback.test.ts | 12 ++++ src/main/ssh/system-ssh-command.ts | 17 ++++-- 5 files changed, 100 insertions(+), 11 deletions(-) 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 699cbe93487..53de3ed671d 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 @@ -104,8 +104,21 @@ and boundedly terminates only processes whose native owner SID exactly matches t by the fixture before attempting hive unload. Focused tests pass 115 cases with one native skip, all three PowerShell blocks parse, 283/283 artifact contracts pass, typecheck and full lint/reliability/max-lines pass, formatting/diff are clean, and protected resolvers are unchanged. -Commit/push and require fresh x64/arm64 lifecycle evidence; do not treat this local oracle as native -proof. +Exact head `e4bcee3f3561e7cfd58aa90e7d35e6c597df8692` is pushed; artifact run `29544129779` is in +progress with x64 job `87772490863` and ARM64 job `87772490900`. Do not treat the local oracle as +native proof. +X64 job `87772490863` is diagnostic RED after official OpenSSH authentication and remote command +session close: `sentinel=false`, `stdoutEnded=false`, `processExit=not-observed`, and +`channelClosed=false`. The client-side no-input process must receive an OS-level ignored stdin +handle in addition to `-n`; preserve piped stdin for every payload-bearing command. ARM64 job +`87772490900` independently returns the same four-field lifecycle tuple after a 31.16-second test, +so the correction is shared across native Windows architectures. +`E-M6-WINDOWS-SYSTEM-SSH-TREE-OS-NOINPUT-LOCAL-001` is locally green: only no-input commands +spawn with stdin ignored at the OS boundary and retain `-n`; ordinary and payload commands remain +piped, and the no-input channel facade settles safely without a child stdin stream. Focused tests +pass 116 cases with one native skip, all three PowerShell blocks parse, 283/283 artifact contracts +pass, typecheck and full lint/reliability/max-lines pass, formatting/diff are clean, and protected +resolvers remain unchanged. Commit/push and require fresh x64/arm64 live proof. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown 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 bd5c9f6c21b..27a899ffc67 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: exact-head Windows OpenSSH live-proof diagnosis and fixture correction**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **WINDOWS NATIVE PROCESS/SESSION LIFECYCLE DIAGNOSTIC LOCALLY GREEN / EXACT-HEAD PROOF NEXT — 2026-07-15, Codex implementation owner** — exact head `b652c1ebcce3803443d84c89811146366cdbdd5b`, artifact run `29542292712`, x64 job `87766845800`, and ARM64 job `87766845741` preserve all prior artifact/build/cache/hash/closure/signer gates and independently expose the same native probe/profile gaps. Exact corrected head `8ae6dcf6450b5f081788357dd8fe776293fb0cfe`, run `29543292239`, x64 job `87769913124`, and ARM64 job `87769913148` remain RED: official OpenSSH starts, but probes still time out after 31.337/31.06 seconds and both fixture `_Classes` hive unloads return exit 1 before exact `UsrClass.dat`/directory failures. Win32 has no Orca control socket, so this is not an Orca ControlMaster retry path. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIFECYCLE-DIAGNOSTIC-LOCAL-001` adds non-sensitive sentinel/stdout-end/process-exit/channel-close timeout evidence and bounded exact-fixture-SID process settlement before hive removal; focused, PowerShell parse, 283-contract, typecheck, full lint/reliability/max-lines, formatting, diff, and protected-resolver gates pass locally. Commit/push and require fresh x64/arm64 lifecycle evidence. Do not advance Milestone 6. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Session checkpoint: **WINDOWS OS-LEVEL NO-INPUT CORRECTION LOCALLY GREEN / EXACT-HEAD PROOF NEXT — 2026-07-15, Codex implementation owner** — exact head `b652c1ebcce3803443d84c89811146366cdbdd5b`, artifact run `29542292712`, x64 job `87766845800`, and ARM64 job `87766845741` preserve all prior artifact/build/cache/hash/closure/signer gates and independently expose the same native probe/profile gaps. Exact corrected head `8ae6dcf6450b5f081788357dd8fe776293fb0cfe`, run `29543292239`, x64 job `87769913124`, and ARM64 job `87769913148` remain RED despite probe-only `-n`. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIFECYCLE-DIAGNOSTIC-LOCAL-001` adds non-sensitive lifecycle evidence and exact-fixture-SID process settlement; every local gate passes. Exact diagnostic head `e4bcee3f3561e7cfd58aa90e7d35e6c597df8692`, run `29544129779`, x64 job `87772490863`, and ARM64 job `87772490900` prove official OpenSSH authentication and remote command-session close while both clients record `sentinel=false`, `stdoutEnded=false`, `processExit=not-observed`, and `channelClosed=false`. Win32 has no Orca control socket, so this is not a mux path. `E-M6-WINDOWS-SYSTEM-SSH-TREE-OS-NOINPUT-LOCAL-001` gives only no-input commands an OS-level ignored stdin handle while ordinary/payload commands retain a pipe; focused, PowerShell parse, 283-contract, typecheck, full lint/reliability/max-lines, formatting, diff, and protected-resolver gates pass locally. Commit/push and require fresh x64/arm64 live proof. Do not advance Milestone 6. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -19245,6 +19245,62 @@ config/scripts/ssh-relay-runtime-*.test.mjs` passes 50 files and 283/283 cases i require fresh exact-head x64/arm64 proof. Legacy remains the sole production/default path; SignPath and product caller/Beta/settings/fallback/tuple/publication work remain deferred. +### E-M6-WINDOWS-SYSTEM-SSH-TREE-LIFECYCLE-CI-RED-001 — both native clients authenticate but never settle inherited stdio + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact pushed head + `e4bcee3f3561e7cfd58aa90e7d35e6c597df8692`, artifact run `29544129779`, native x64 job + `87772490863` (`windows-2022`) and native ARM64 job `87772490900` (`windows-11-arm`). +- Preserved evidence: both jobs pass the 100-file / 854-case native artifact suite with 18 declared + skips, exact double-build identity/smoke, full-size client cache, archive/library hash and signer + closure, ACL replacement, official OpenSSH 10.0p2 startup, pinned-key authentication, and + PowerShell 5.1 before the live transfer. +- Shared lifecycle RED: server logs prove accepted public-key authentication, a fixture-user child, + remote command-session start, and remote command-session close. X64 then times out after 31.16 + seconds and ARM64 after 31.16 seconds with the identical client observation: + `sentinel=false`, `stdoutEnded=false`, `processExit=not-observed`, `channelClosed=false`. + No fixture-SID-owned process remains by teardown, yet `_Classes` hive unload still exits 1 and + exact `UsrClass.dat`/profile-directory assertions fail. +- Correction gate: for `SystemSshBuildArgsOptions.noInput` only, spawn the native child with + OS-level ignored stdin in addition to OpenSSH `-n`; keep stdin piped for ordinary commands and + every file/tree payload. Prove ignored-vs-piped spawn options and a safe no-input channel facade, + then rerun focused/283/static/format/isolation and fresh x64/arm64 live gates. Do not weaken the + structured sentinel, process/channel settlement, profile, or directory checks. + +### E-M6-WINDOWS-SYSTEM-SSH-TREE-OS-NOINPUT-LOCAL-001 — no-input commands have no inherited OS pipe while payload commands remain piped + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted correction atop + exact lifecycle RED head `e4bcee3f3561e7cfd58aa90e7d35e6c597df8692` on Apple arm64, + macOS 26.2 build 25C56, Node 26.5.0, pnpm 10.24.0. Native Windows execution remains required. +- Correction: `spawnSystemSshCommand` uses `stdio: ['ignore', 'pipe', 'pipe']` only when + `SystemSshBuildArgsOptions.noInput` is true; it retains OpenSSH `-n`. Every ordinary command and + every file/tree payload keeps `stdio: ['pipe', 'pipe', 'pipe']`. When Node correctly exposes no + child stdin stream, the channel uses its own fail-on-write facade so the probe's ownership-ending + `stdin.end()` settles without fabricating a writable child pipe. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 3 files plus one declared native skip, + 116 passed tests plus one skip, in 5.83 seconds real; maximum RSS is 168,116,224 bytes and peak + footprint is 98,637,584 bytes. Independent YAML extraction plus PowerShell + `[scriptblock]::Create()` parses all three start/measure/stop blocks. +- Release-contract command: `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 --reporter=dot +config/scripts/ssh-relay-runtime-*.test.mjs` passes 50 files and 283/283 cases in 30.72 seconds + real; maximum RSS is 195,543,040 bytes and peak footprint is 97,769,280 bytes. +- Static commands: `/usr/bin/time -l pnpm typecheck` passes in 8.01 seconds real with maximum RSS + 1,846,231,040 bytes and peak footprint 97,752,800 bytes. `/usr/bin/time -l pnpm lint` passes in + 31.04 seconds real with only the 26 pre-existing warnings, all 41 reliability gates, the + 355-entry max-lines ratchet, bundled guides, and localization checks; maximum RSS is + 1,978,155,008 bytes and peak footprint is 97,605,488 bytes. +- Isolation evidence: targeted `oxfmt --check`, `git diff --check`, and exact working-tree equality + to `HEAD` for both protected resolver files pass. Only the no-input system-SSH process/channel + boundary and test plus the three living planning artifacts are added to the prior diagnostic + package. +- Residual boundary: local macOS cannot execute Windows `ssh.exe`, so fresh exact-head x64/arm64 + proof must show sentinel/process/channel settlement, full-size transfer metrics, cancellation, + collision, cleanup, and exact profile/directory teardown. Legacy remains the sole + production/default path; SignPath remains deferred. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 6d0ab9317cc..6eabb581669 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1643,9 +1643,10 @@

    Prove production behavior, failure behavior, and rollback behavior.

    Full-size runtime over SFTP and POSIX/Windows system SSH; tar/no-tar; no remote hash tools; corrupted upload; bundled-Node corruption/failure; bounded memory and concurrency; no-input system-SSH probe records bounded native process exit, - stdout-end, sentinel, and channel-close settlement rather than treating OpenSSH - -n alone as proof; fixture-owned Windows session processes settle - before profile hives are boundedly unloaded and native profile deletion begins; + stdout-end, sentinel, and channel-close settlement; the child receives an + OS-level ignored stdin handle as well as OpenSSH -n, because the + option alone is not proof; fixture-owned Windows session processes settle before + profile hives are boundedly unloaded and native profile deletion begins; MaxSessions=1; interrupted upload; stale lock diff --git a/src/main/ssh/ssh-system-fallback.test.ts b/src/main/ssh/ssh-system-fallback.test.ts index b9b6c0940d6..f3d95a55790 100644 --- a/src/main/ssh/ssh-system-fallback.test.ts +++ b/src/main/ssh/ssh-system-fallback.test.ts @@ -418,6 +418,18 @@ describe('spawnSystemSsh', () => { ) }) + it('spawns a no-input command without an inherited stdin pipe', () => { + spawnMock.mockReturnValueOnce({ ...mockProc, stdin: null }) + const channel = spawnSystemSshCommand(createTarget(), 'echo ready', { noInput: true }) + + expect(spawnMock).toHaveBeenCalledWith( + SYSTEM_SSH_PATH, + expect.arrayContaining(['-n', '--', 'deploy@example.com']), + expect.objectContaining({ stdio: ['ignore', 'pipe', 'pipe'] }) + ) + expect(() => channel.stdin.end()).not.toThrow() + }) + it('spawns port forwards before the ssh destination terminator', () => { spawnSystemSshPortForward(createTarget({ configHost: 'fdpass-host' }), 5173, '127.0.0.1', 3000) diff --git a/src/main/ssh/system-ssh-command.ts b/src/main/ssh/system-ssh-command.ts index 1f0ce5ed6df..1d0193509b6 100644 --- a/src/main/ssh/system-ssh-command.ts +++ b/src/main/ssh/system-ssh-command.ts @@ -62,8 +62,11 @@ export function spawnSystemSshCommand( const remoteCommand = options?.wrapCommand === false ? command : wrapRemoteCommandForPosixShell(command) + const noInput = options?.noInput === true const proc = spawn(sshPath, [...buildSshArgs(target, options), remoteCommand], { - stdio: ['pipe', 'pipe', 'pipe'], + // Why: Windows OpenSSH can keep an inherited pipe alive despite `-n`; + // no-input commands must not receive a writable OS handle at all. + stdio: [noInput ? 'ignore' : 'pipe', 'pipe', 'pipe'], windowsHide: true }) return wrapCommandProcess(proc) @@ -94,7 +97,11 @@ function wrapCommandProcess(proc: ChildProcess): SystemSshCommandChannel { proc.stdout?.resume() }, write(chunk, encoding, cb) { - proc.stdin!.write(chunk, encoding, cb) + if (!proc.stdin) { + cb(new Error('System SSH command does not accept stdin')) + return + } + proc.stdin.write(chunk, encoding, cb) } }) const channel = duplex as unknown as SystemSshCommandChannel @@ -105,7 +112,7 @@ function wrapCommandProcess(proc: ChildProcess): SystemSshCommandChannel { _process?: ChildProcess close: () => void } - mutableChannel.stdin = proc.stdin! + mutableChannel.stdin = proc.stdin ?? duplex mutableChannel.stderr = proc.stderr! mutableChannel._process = proc mutableChannel.close = () => { @@ -122,7 +129,7 @@ function wrapCommandProcess(proc: ChildProcess): SystemSshCommandChannel { proc.off('exit', onExit) proc.off('close', onClose) proc.off('error', onProcessError) - proc.stdin!.off('error', onStreamError) + proc.stdin?.off('error', onStreamError) proc.stdout!.off('error', onStreamError) } const fail = (err: Error): void => { @@ -158,7 +165,7 @@ function wrapCommandProcess(proc: ChildProcess): SystemSshCommandChannel { proc.on('exit', onExit) proc.on('close', onClose) proc.on('error', onProcessError) - proc.stdin!.on('error', onStreamError) + proc.stdin?.on('error', onStreamError) proc.stdout!.on('error', onStreamError) return channel From 4f57b885d24ad1755a5c863eaf37369208cecedb Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 20:45:55 -0400 Subject: [PATCH 248/267] fix(ssh): close no-input child handles safely Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 16 ++-- ...github-release-implementation-checklist.md | 76 +++++++++++++++++-- ...6-07-14-ssh-relay-github-release-plan.html | 12 +-- src/main/ssh/ssh-system-fallback.test.ts | 39 +++++++++- src/main/ssh/system-ssh-command.ts | 36 ++++++--- 5 files changed, 149 insertions(+), 30 deletions(-) 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 53de3ed671d..6ce2999c704 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 @@ -113,12 +113,16 @@ session close: `sentinel=false`, `stdoutEnded=false`, `processExit=not-observed` handle in addition to `-n`; preserve piped stdin for every payload-bearing command. ARM64 job `87772490900` independently returns the same four-field lifecycle tuple after a 31.16-second test, so the correction is shared across native Windows architectures. -`E-M6-WINDOWS-SYSTEM-SSH-TREE-OS-NOINPUT-LOCAL-001` is locally green: only no-input commands -spawn with stdin ignored at the OS boundary and retain `-n`; ordinary and payload commands remain -piped, and the no-input channel facade settles safely without a child stdin stream. Focused tests -pass 116 cases with one native skip, all three PowerShell blocks parse, 283/283 artifact contracts -pass, typecheck and full lint/reliability/max-lines pass, formatting/diff are clean, and protected -resolvers remain unchanged. Commit/push and require fresh x64/arm64 live proof. +`E-M6-WINDOWS-SYSTEM-SSH-TREE-OS-NOINPUT-LOCAL-001` records a locally green but native-falsified +hypothesis: giving every no-input command an ignored OS stdin handle preserves the payload paths, +but exact run `29544909117` proves the design is unsafe. Linux x64/ARM64 jobs +`87774941240`/`87774941211` observe an argument-less synthetic channel close before child close; +native Windows x64 job `87774941207` reproduces Win32-OpenSSH issues #856/#1330 by executing the +remote command but hanging with all four lifecycle fields false; ARM64 job `87774941200` +independently returns the same lifecycle RED. The corrected contract keeps POSIX stdin ignored, +gives Windows an anonymous pipe whose parent write end is destroyed immediately plus mandatory +`-n`, and uses a separate no-input `Writable` facade so ending it cannot half-close the command +channel. Exact-head proof of that correction is next. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown 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 27a899ffc67..b548fc75dbd 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: exact-head Windows OpenSSH live-proof diagnosis and fixture correction**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **WINDOWS OS-LEVEL NO-INPUT CORRECTION LOCALLY GREEN / EXACT-HEAD PROOF NEXT — 2026-07-15, Codex implementation owner** — exact head `b652c1ebcce3803443d84c89811146366cdbdd5b`, artifact run `29542292712`, x64 job `87766845800`, and ARM64 job `87766845741` preserve all prior artifact/build/cache/hash/closure/signer gates and independently expose the same native probe/profile gaps. Exact corrected head `8ae6dcf6450b5f081788357dd8fe776293fb0cfe`, run `29543292239`, x64 job `87769913124`, and ARM64 job `87769913148` remain RED despite probe-only `-n`. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIFECYCLE-DIAGNOSTIC-LOCAL-001` adds non-sensitive lifecycle evidence and exact-fixture-SID process settlement; every local gate passes. Exact diagnostic head `e4bcee3f3561e7cfd58aa90e7d35e6c597df8692`, run `29544129779`, x64 job `87772490863`, and ARM64 job `87772490900` prove official OpenSSH authentication and remote command-session close while both clients record `sentinel=false`, `stdoutEnded=false`, `processExit=not-observed`, and `channelClosed=false`. Win32 has no Orca control socket, so this is not a mux path. `E-M6-WINDOWS-SYSTEM-SSH-TREE-OS-NOINPUT-LOCAL-001` gives only no-input commands an OS-level ignored stdin handle while ordinary/payload commands retain a pipe; focused, PowerShell parse, 283-contract, typecheck, full lint/reliability/max-lines, formatting, diff, and protected-resolver gates pass locally. Commit/push and require fresh x64/arm64 live proof. Do not advance Milestone 6. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: cross-platform no-input process-handle correction and exact-head live proof**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **IGNORED-STDIN HYPOTHESIS NATIVELY FALSIFIED / PLATFORM-SPECIFIC EOF CORRECTION LOCALLY GREEN — 2026-07-15, Codex implementation owner** — exact diagnostic head `e4bcee3f3561e7cfd58aa90e7d35e6c597df8692`, run `29544129779`, x64 job `87772490863`, and ARM64 job `87772490900` prove official OpenSSH authentication and remote command-session close while both clients record `sentinel=false`, `stdoutEnded=false`, `processExit=not-observed`, and `channelClosed=false`. Exact ignored-stdin head `aab0775272aca2b03d55d045cbaf7aefc7511bfe`, artifact run `29544909117`, Linux x64/ARM64 jobs `87774941240`/`87774941211`, and Windows x64/ARM64 jobs `87774941207`/`87774941200` falsify the shared ignored-handle design. Both Linux jobs receive the sentinel but the self-referential no-input facade emits an argument-less synthetic close before the child close. Both Windows jobs instead reproduce the redirected-handle hang documented by Win32-OpenSSH issues #856 and #1330: authenticated remote command start/close, then 31.79/32.78-second timeouts with every lifecycle field false. The implementation-plan correction keeps ignored stdin on POSIX, uses an anonymous pipe with the parent write end destroyed immediately plus mandatory `-n` on Windows, and gives all no-input callers a separate fail-on-write facade. Focused 117+1, 283-contract, typecheck, full lint/reliability/max-lines, all three PowerShell parse, formatting, diff, and protected-resolver isolation gates pass locally; exact-head native proof is next. Do not advance Milestone 6. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -19268,9 +19268,9 @@ config/scripts/ssh-relay-runtime-*.test.mjs` passes 50 files and 283/283 cases i ### E-M6-WINDOWS-SYSTEM-SSH-TREE-OS-NOINPUT-LOCAL-001 — no-input commands have no inherited OS pipe while payload commands remain piped -- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted correction atop - exact lifecycle RED head `e4bcee3f3561e7cfd58aa90e7d35e6c597df8692` on Apple arm64, - macOS 26.2 build 25C56, Node 26.5.0, pnpm 10.24.0. Native Windows execution remains required. +- Date/owner/head/runner: 2026-07-15, Codex implementation owner; exact pushed head + `aab0775272aca2b03d55d045cbaf7aefc7511bfe` on Apple arm64, macOS 26.2 build 25C56, Node + 26.5.0, pnpm 10.24.0. Native Windows execution remains required. - Correction: `spawnSystemSshCommand` uses `stdio: ['ignore', 'pipe', 'pipe']` only when `SystemSshBuildArgsOptions.noInput` is true; it retains OpenSSH `-n`. Every ordinary command and every file/tree payload keeps `stdio: ['pipe', 'pipe', 'pipe']`. When Node correctly exposes no @@ -19300,6 +19300,72 @@ config/scripts/ssh-relay-runtime-*.test.mjs` passes 50 files and 283/283 cases i proof must show sentinel/process/channel settlement, full-size transfer metrics, cancellation, collision, cleanup, and exact profile/directory teardown. Legacy remains the sole production/default path; SignPath remains deferred. +- Native proof result: artifact run `29544909117` tests exact head + `aab0775272aca2b03d55d045cbaf7aefc7511bfe`; native x64 job `87774941207` and native ARM64 job + `87774941200` both complete RED at the probe and exact profile teardown gates. See + `E-M6-CROSS-PLATFORM-NOINPUT-CI-RED-001`; neither job counts as enablement proof. + +### E-M6-CROSS-PLATFORM-NOINPUT-CI-RED-001 — shared ignored stdin regresses POSIX and preserves the Win32 redirected-handle hang + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact pushed head + `aab0775272aca2b03d55d045cbaf7aefc7511bfe`, artifact run `29544909117`. +- POSIX native RED: Linux x64 job `87774941240` and Linux ARM64 job `87774941211` on + `ubuntu-24.04` pass the 283-contract suite, target-native build, full-size cache, and live SFTP, + then fail the restricted OpenSSH system-SSH probe in 620/482 milliseconds. Both receive the + remote sentinel, but ending the self-referential no-input facade finishes the channel's writable + side; once stdout ends, the duplex auto-destroys and emits `close` without the child's exit code. + The purpose-named local regression reproduces that argument-less early close before correction. +- Windows native RED: x64 job `87774941207` on `windows-2022` and ARM64 job `87774941200` on + `windows-11-arm` pass 100 files / 854 cases with 18 declared skips, target-native double-build, + full-size cache, exact fixture signatures/ACLs, and official OpenSSH 10.0p2 startup. Their remote + command sessions start and close, but the clients time out after 31.79/32.78 seconds with + `sentinel=false`, `stdoutEnded=false`, `processExit=not-observed`, and `channelClosed=false`. + Exact `_Classes` unload and `UsrClass.dat`/profile-directory checks remain RED on both runners. +- Design correction evidence: [Win32-OpenSSH #856](https://github.com/PowerShell/Win32-OpenSSH/issues/856) + identifies redirected stdin handles under `CreateProcess` and recommends an anonymous pipe whose + write end is closed; [Win32-OpenSSH #1330](https://github.com/PowerShell/Win32-OpenSSH/issues/1330) + demonstrates that explicit `NUL` redirection executes the remote command and then hangs. The + reviewed plan and checklist now require ignored stdin only on POSIX and a destroyed parent pipe + end plus mandatory `-n` on Windows. Inheriting parent stdin is rejected because packaged Electron + clients need a console-independent boundary. +- Residual gate: this run is RED and enables no tuple. Prove the corrected platform-specific child + handles on Linux x64/ARM64 and Windows x64/ARM64 before advancing. + +### E-M6-CROSS-PLATFORM-NOINPUT-EOF-LOCAL-001 — separate facade and platform-specific child EOF are locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted correction atop exact + RED head `aab0775272aca2b03d55d045cbaf7aefc7511bfe` on Apple arm64, macOS 26.2 build 25C56, + Node 26.5.0, pnpm 10.24.0. +- Correction: all no-input commands retain OpenSSH `-n`. POSIX spawns with ignored stdin. Windows + spawns with an anonymous stdin pipe and destroys the parent write stream immediately, before the + caller can execute or retain it. A separate fail-on-write `Writable` exposes safe `stdin.end()` + semantics without ending the command duplex. Ordinary commands and every file/tree payload retain + their writable pipe unchanged. +- Purpose RED/green: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-system-fallback.test.ts -t "keeps no-input command output open"` first fails + because `close` is called once without arguments before the simulated child closes, then passes + after the separate facade. The adjacent native-platform assertion requires ignored stdin on + POSIX, pipe plus immediate `destroy()` on Windows, mandatory `-n`, and no payload-path change. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with one declared native skip in + 3.74 seconds real; maximum RSS is 179,191,808 bytes and peak footprint is 97,097,416 bytes. +- Release contracts: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 --reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 50 files and + 283/283 cases in 22.72 seconds real; maximum RSS is 195,510,272 bytes and peak footprint is + 98,178,856 bytes. +- Static gates: `/usr/bin/time -l pnpm typecheck` passes in 4.37 seconds real with maximum RSS + 1,241,350,144 bytes and peak footprint 96,835,248 bytes. `/usr/bin/time -l pnpm lint` passes in + 31.43 seconds real with only 26 pre-existing warnings, all 41 reliability gates, and the 355-entry + max-lines ratchet; maximum RSS is 2,116,173,824 bytes and peak footprint is 97,572,792 bytes. +- Workflow/isolation gates: Node YAML extraction invokes `pwsh -NoProfile -NonInteractive -Command +'[scriptblock]::Create([Console]::In.ReadToEnd()) | Out-Null'` for the exact start, measure, and + stop workflow bodies; all three print `powershell_parse=pass`. Targeted `oxfmt --check`, + `git diff --check`, and exact equality to `HEAD` for both protected resolver files pass. +- Residual boundary: local macOS cannot prove Win32 pipe-handle behavior or native fixture teardown. + Exact-head Linux x64/ARM64 and Windows x64/ARM64 live proof remains mandatory. Legacy remains the + sole production/default path; product/Beta/fallback/tuple/publication and SignPath stay absent. ## Accepted Gaps diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 6eabb581669..91a6603267d 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1643,11 +1643,13 @@

    Prove production behavior, failure behavior, and rollback behavior.

    Full-size runtime over SFTP and POSIX/Windows system SSH; tar/no-tar; no remote hash tools; corrupted upload; bundled-Node corruption/failure; bounded memory and concurrency; no-input system-SSH probe records bounded native process exit, - stdout-end, sentinel, and channel-close settlement; the child receives an - OS-level ignored stdin handle as well as OpenSSH -n, because the - option alone is not proof; fixture-owned Windows session processes settle before - profile hives are boundedly unloaded and native profile deletion begins; - MaxSessions=1; interrupted upload; stale lock + stdout-end, sentinel, and channel-close settlement; POSIX no-input probes use an + OS-level ignored stdin handle plus OpenSSH -n; Windows must not map + stdin to NUL because Win32-OpenSSH issues #856/#1330 document a + redirected-handle hang, so it receives an anonymous pipe whose parent write end + is destroyed immediately plus -n; fixture-owned Windows session + processes settle before profile hives are boundedly unloaded and native profile + deletion begins; MaxSessions=1; interrupted upload; stale lock Bundled Node hashes the full staged tree before sentinel write; diff --git a/src/main/ssh/ssh-system-fallback.test.ts b/src/main/ssh/ssh-system-fallback.test.ts index f3d95a55790..f896ef847f6 100644 --- a/src/main/ssh/ssh-system-fallback.test.ts +++ b/src/main/ssh/ssh-system-fallback.test.ts @@ -164,6 +164,7 @@ describe('spawnSystemSsh', () => { write: ReturnType end: ReturnType on: ReturnType + destroy: ReturnType } stdout: { on: ReturnType } stderr: { on: ReturnType } @@ -177,7 +178,7 @@ describe('spawnSystemSsh', () => { spawnMock.mockReset() mockProc = { - stdin: { write: vi.fn(), end: vi.fn(), on: vi.fn() }, + stdin: { write: vi.fn(), end: vi.fn(), on: vi.fn(), destroy: vi.fn() }, stdout: { on: vi.fn() }, stderr: { on: vi.fn() }, pid: 12345, @@ -418,18 +419,48 @@ describe('spawnSystemSsh', () => { ) }) - it('spawns a no-input command without an inherited stdin pipe', () => { - spawnMock.mockReturnValueOnce({ ...mockProc, stdin: null }) + it('spawns a no-input command with the platform-safe child stdin boundary', () => { + if (process.platform !== 'win32') { + spawnMock.mockReturnValueOnce({ ...mockProc, stdin: null }) + } const channel = spawnSystemSshCommand(createTarget(), 'echo ready', { noInput: true }) expect(spawnMock).toHaveBeenCalledWith( SYSTEM_SSH_PATH, expect.arrayContaining(['-n', '--', 'deploy@example.com']), - expect.objectContaining({ stdio: ['ignore', 'pipe', 'pipe'] }) + expect.objectContaining({ + stdio: [process.platform === 'win32' ? 'pipe' : 'ignore', 'pipe', 'pipe'] + }) ) + if (process.platform === 'win32') { + expect(mockProc.stdin.destroy).toHaveBeenCalledOnce() + } expect(() => channel.stdin.end()).not.toThrow() }) + it('keeps no-input command output open until the child process closes', async () => { + const proc = createMockChildProcess() + if (process.platform !== 'win32') { + ;(proc as unknown as { stdin: null }).stdin = null + } + spawnMock.mockReturnValue(proc) + + const channel = spawnSystemSshCommand(createTarget(), 'echo ready', { noInput: true }) + const output: Buffer[] = [] + const onClose = vi.fn() + channel.on('data', (chunk: Buffer) => output.push(chunk)) + channel.on('close', onClose) + + channel.stdin.end() + proc.stdout.end('ORCA-SYSTEM-SSH-OK\n') + await new Promise((resolve) => setImmediate(resolve)) + + expect(Buffer.concat(output).toString('utf8')).toBe('ORCA-SYSTEM-SSH-OK\n') + expect(onClose).not.toHaveBeenCalled() + proc.emit('close', 0, null) + expect(onClose).toHaveBeenCalledWith(0, null) + }) + it('spawns port forwards before the ssh destination terminator', () => { spawnSystemSshPortForward(createTarget({ configHost: 'fdpass-host' }), 5173, '127.0.0.1', 3000) diff --git a/src/main/ssh/system-ssh-command.ts b/src/main/ssh/system-ssh-command.ts index 1d0193509b6..b2bf4f480c9 100644 --- a/src/main/ssh/system-ssh-command.ts +++ b/src/main/ssh/system-ssh-command.ts @@ -1,5 +1,5 @@ import { spawn, type ChildProcess } from 'node:child_process' -import { Duplex } from 'node:stream' +import { Duplex, Writable } from 'node:stream' import type { ClientChannel } from 'ssh2' import type { SshTarget } from '../../shared/ssh-types' import { wrapRemoteCommandForPosixShell, type SshExecOptions } from './ssh-connection-utils' @@ -63,13 +63,17 @@ export function spawnSystemSshCommand( const remoteCommand = options?.wrapCommand === false ? command : wrapRemoteCommandForPosixShell(command) const noInput = options?.noInput === true + const stdinMode = noInput && process.platform !== 'win32' ? 'ignore' : 'pipe' const proc = spawn(sshPath, [...buildSshArgs(target, options), remoteCommand], { - // Why: Windows OpenSSH can keep an inherited pipe alive despite `-n`; - // no-input commands must not receive a writable OS handle at all. - stdio: [noInput ? 'ignore' : 'pipe', 'pipe', 'pipe'], + // Why: Win32-OpenSSH can hang when stdin is mapped to NUL; give it a + // pipe whose parent end is closed immediately, as required by #856/#1330. + stdio: [stdinMode, 'pipe', 'pipe'], windowsHide: true }) - return wrapCommandProcess(proc) + if (noInput && process.platform === 'win32') { + proc.stdin?.destroy() + } + return wrapCommandProcess(proc, !noInput) } function wrapChildProcess(proc: ChildProcess): SystemSshProcess { @@ -91,13 +95,13 @@ function wrapChildProcess(proc: ChildProcess): SystemSshProcess { } } -function wrapCommandProcess(proc: ChildProcess): SystemSshCommandChannel { +function wrapCommandProcess(proc: ChildProcess, acceptsInput: boolean): SystemSshCommandChannel { const duplex = new Duplex({ read() { proc.stdout?.resume() }, write(chunk, encoding, cb) { - if (!proc.stdin) { + if (!acceptsInput || !proc.stdin) { cb(new Error('System SSH command does not accept stdin')) return } @@ -112,7 +116,15 @@ function wrapCommandProcess(proc: ChildProcess): SystemSshCommandChannel { _process?: ChildProcess close: () => void } - mutableChannel.stdin = proc.stdin ?? duplex + mutableChannel.stdin = + (acceptsInput ? proc.stdin : null) ?? + new Writable({ + // Why: ending a no-input facade must not half-close the readable command + // channel before the child reports its exit status. + write(_chunk, _encoding, cb) { + cb(new Error('System SSH command does not accept stdin')) + } + }) mutableChannel.stderr = proc.stderr! mutableChannel._process = proc mutableChannel.close = () => { @@ -129,7 +141,9 @@ function wrapCommandProcess(proc: ChildProcess): SystemSshCommandChannel { proc.off('exit', onExit) proc.off('close', onClose) proc.off('error', onProcessError) - proc.stdin?.off('error', onStreamError) + if (acceptsInput) { + proc.stdin?.off('error', onStreamError) + } proc.stdout!.off('error', onStreamError) } const fail = (err: Error): void => { @@ -165,7 +179,9 @@ function wrapCommandProcess(proc: ChildProcess): SystemSshCommandChannel { proc.on('exit', onExit) proc.on('close', onClose) proc.on('error', onProcessError) - proc.stdin?.on('error', onStreamError) + if (acceptsInput) { + proc.stdin?.on('error', onStreamError) + } proc.stdout!.on('error', onStreamError) return channel From da938a7405d0a6c760979358a6c0a38030d9acaf Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 21:08:57 -0400 Subject: [PATCH 249/267] test(ssh): qualify Windows no-input handles Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 3 + ...time-windows-openssh-workflow-contract.mjs | 5 + ...elease-implementation-checklist-summary.md | 14 +- ...github-release-implementation-checklist.md | 85 +++++++++- ...6-07-14-ssh-relay-github-release-plan.html | 11 +- ...sh-system-no-input-windows-openssh.test.ts | 158 ++++++++++++++++++ 6 files changed, 259 insertions(+), 17 deletions(-) create mode 100644 src/main/ssh/ssh-system-no-input-windows-openssh.test.ts diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 66e0ca250ef..712715a56f5 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -1644,6 +1644,9 @@ jobs: # Why: keep exact host-key trust isolated from the hosted runner's real SSH profile. $env:HOME = $env:ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_CLIENT_HOME $env:USERPROFILE = $env:ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_CLIENT_HOME + pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 --reporter=verbose ` + src/main/ssh/ssh-system-no-input-windows-openssh.test.ts + if ($LASTEXITCODE -ne 0) { throw 'Windows no-input handle diagnostic failed' } pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 --reporter=verbose ` src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts if ($LASTEXITCODE -ne 0) { throw 'full-size Windows system-SSH measurement failed' } diff --git a/config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs b/config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs index 7340f33e80a..5e749ed9cbc 100644 --- a/config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs +++ b/config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs @@ -64,7 +64,12 @@ export function assertWindowsOpenSshWorkflow(workflow, expect) { ) expect(measure.shell).toBe('pwsh') expect(measure.run).toContain("$env:ORCA_SSH_FORCE_SYSTEM_TRANSPORT = '1'") + expect(measure.run).toContain('ssh-system-no-input-windows-openssh.test.ts') expect(measure.run).toContain('ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts') + expect(measure.run.indexOf('ssh-system-no-input-windows-openssh.test.ts')).toBeLessThan( + measure.run.indexOf('ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts') + ) + expect(measure.run).toContain("throw 'Windows no-input handle diagnostic failed'") expect(measure.run).toContain("Join-Path $firstOutput 'runtime'") expect(stop.if).toBe("always() && runner.os == 'Windows'") expect(stop.shell).toBe('pwsh') 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 6ce2999c704..6bde62110ff 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 @@ -119,10 +119,16 @@ but exact run `29544909117` proves the design is unsafe. Linux x64/ARM64 jobs `87774941240`/`87774941211` observe an argument-less synthetic channel close before child close; native Windows x64 job `87774941207` reproduces Win32-OpenSSH issues #856/#1330 by executing the remote command but hanging with all four lifecycle fields false; ARM64 job `87774941200` -independently returns the same lifecycle RED. The corrected contract keeps POSIX stdin ignored, -gives Windows an anonymous pipe whose parent write end is destroyed immediately plus mandatory -`-n`, and uses a separate no-input `Writable` facade so ending it cannot half-close the command -channel. Exact-head proof of that correction is next. +independently returns the same lifecycle RED. The corrected POSIX contract uses a separate no-input +`Writable` facade so ending it cannot half-close the command channel. Exact correction head +`4f57b885d24ad1755a5c863eaf37369208cecedb`, artifact run `29545688003`, and Linux x64/ARM64 +jobs `87777401256`/`87777401218` are green through live SFTP and restricted no-tar POSIX system +SSH. Windows x64/ARM64 jobs `87777401200`/`87777401195` independently falsify the standard +anonymous-pipe EOF hypothesis with the unchanged four-field timeout and profile RED. Next compare a +console-independent overlapped pipe against inherited stdin as a diagnostic control; do not change +the production Windows handle until native evidence selects it. The disconnected bounded diagnostic +and workflow ordering are locally green under +`E-M6-WINDOWS-NOINPUT-HANDLE-DIAGNOSTIC-LOCAL-001`; native x64/ARM64 execution is next. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown 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 b548fc75dbd..a0c6408218d 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: cross-platform no-input process-handle correction and exact-head live proof**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **IGNORED-STDIN HYPOTHESIS NATIVELY FALSIFIED / PLATFORM-SPECIFIC EOF CORRECTION LOCALLY GREEN — 2026-07-15, Codex implementation owner** — exact diagnostic head `e4bcee3f3561e7cfd58aa90e7d35e6c597df8692`, run `29544129779`, x64 job `87772490863`, and ARM64 job `87772490900` prove official OpenSSH authentication and remote command-session close while both clients record `sentinel=false`, `stdoutEnded=false`, `processExit=not-observed`, and `channelClosed=false`. Exact ignored-stdin head `aab0775272aca2b03d55d045cbaf7aefc7511bfe`, artifact run `29544909117`, Linux x64/ARM64 jobs `87774941240`/`87774941211`, and Windows x64/ARM64 jobs `87774941207`/`87774941200` falsify the shared ignored-handle design. Both Linux jobs receive the sentinel but the self-referential no-input facade emits an argument-less synthetic close before the child close. Both Windows jobs instead reproduce the redirected-handle hang documented by Win32-OpenSSH issues #856 and #1330: authenticated remote command start/close, then 31.79/32.78-second timeouts with every lifecycle field false. The implementation-plan correction keeps ignored stdin on POSIX, uses an anonymous pipe with the parent write end destroyed immediately plus mandatory `-n` on Windows, and gives all no-input callers a separate fail-on-write facade. Focused 117+1, 283-contract, typecheck, full lint/reliability/max-lines, all three PowerShell parse, formatting, diff, and protected-resolver isolation gates pass locally; exact-head native proof is next. Do not advance Milestone 6. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: native Windows no-input handle qualification**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **POSIX EOF CORRECTION NATIVELY GREEN / STANDARD WINDOWS PIPE NATIVELY RED / HANDLE DIAGNOSTIC LOCALLY GREEN — 2026-07-15, Codex implementation owner** — exact correction head `4f57b885d24ad1755a5c863eaf37369208cecedb`, artifact run `29545688003`, and Linux x64/ARM64 jobs `87777401256`/`87777401218` prove the separate no-input facade through live SFTP and restricted no-tar POSIX system SSH with bounded full-size transfer, four-file concurrency, cancellation, and cleanup. Windows x64/ARM64 jobs `87777401200`/`87777401195` independently falsify the standard anonymous-pipe EOF hypothesis: official OpenSSH authenticates and the remote command closes, but clients time out after 31.36/31.82 seconds with `sentinel=false`, `stdoutEnded=false`, `processExit=not-observed`, and `channelClosed=false`; exact profile teardown remains RED. `E-M6-WINDOWS-NOINPUT-HANDLE-DIAGNOSTIC-LOCAL-001` adds a bounded, non-sensitive native comparison of a console-independent overlapped pipe and inherited-stdin upstream control before the unchanged full-size measurement; focused 117+2, 283-contract, typecheck, full lint/reliability/max-lines, all three PowerShell parse, formatting, diff, and protected-resolver gates pass locally. Commit/push and require native x64/ARM64 diagnostic output before selecting another production handle. Do not advance Milestone 6. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -19366,6 +19366,73 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with one de - Residual boundary: local macOS cannot prove Win32 pipe-handle behavior or native fixture teardown. Exact-head Linux x64/ARM64 and Windows x64/ARM64 live proof remains mandatory. Legacy remains the sole production/default path; product/Beta/fallback/tuple/publication and SignPath stay absent. +- Native proof dispatch: artifact run `29545688003` tests exact correction head + `4f57b885d24ad1755a5c863eaf37369208cecedb`; Windows x64/ARM64 jobs + `87777401200`/`87777401195` and Linux x64/ARM64 jobs `87777401256`/`87777401218` are in progress. + Do not count any job until its full live-transfer and exact teardown assertions complete. +- Native POSIX proof: Linux x64 job `87777401256` is green for 124,846,430 bytes / 34 files on + OpenSSH 9.6p1 with 991.76 ms serial transfer, 721.90 ms at four-file concurrency, 14.09 ms + cancellation settlement, and bounded 3,407,872/802,816/0-byte incremental RSS. Linux ARM64 job + `87777401218` is green for 122,865,324 bytes / 34 files with 841.39 ms serial, 597.69 ms + concurrent, 14.91 ms cancellation, and bounded 4,399,104/786,432/131,072-byte incremental RSS. + Both use only `/bin/sh`, `mkdir`, `chmod`, `cat`, and `rm`; live SFTP, collision/cancellation, + cleanup, and artifact upload also pass. + +### E-M6-WINDOWS-NOINPUT-STANDARD-PIPE-CI-RED-001 — standard anonymous-pipe EOF is not sufficient for Win32-OpenSSH + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact pushed head + `4f57b885d24ad1755a5c863eaf37369208cecedb`, artifact run `29545688003`, Windows x64 job + `87777401200` on `windows-2022`, and Windows ARM64 job `87777401195` on `windows-11-arm`. +- Preserved native evidence: both jobs pass 100 files / 854 cases with 18 declared skips, exact + target-native double-build/smoke, full-size extraction/cache, official fixture trust/ACL/startup, + and Windows PowerShell 5.1 before the live probe. The Linux x64/ARM64 jobs listed above prove the + separate channel facade itself on both native POSIX architectures. +- Shared Windows RED: destroying the parent side of a standard Node `pipe` before returning the + no-input channel does not change the native lifecycle. X64 times out after 31.36 seconds and ARM64 + after 31.82 seconds with `sentinel=false`, `stdoutEnded=false`, `processExit=not-observed`, and + `channelClosed=false`, even though server logs prove pinned-key authentication and remote command + session close. Both exact `_Classes` unload and `UsrClass.dat`/profile-directory gates remain RED. +- Course correction: standard pipes and explicit `NUL` are rejected. Before changing production + handles again, add a disconnected live diagnostic that compares Node's Windows `overlapped` pipe + with immediate parent destruction against inherited stdin as the upstream `GetStdHandle` control. + Require bounded settlement and exact lifecycle output on x64 and ARM64. Inherited stdin cannot be + selected for packaged Electron without separate no-console proof; overlapped pipe is preferred + only if both native architectures prove it. +- Residual boundary: this is a RED diagnostic, not an availability exception. Legacy stays the sole + production/default path and no tuple, fallback, Beta, publication, or SignPath behavior changes. + +### E-M6-WINDOWS-NOINPUT-HANDLE-DIAGNOSTIC-LOCAL-001 — bounded native handle comparison is locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted diagnostic atop exact + standard-pipe RED head `4f57b885d24ad1755a5c863eaf37369208cecedb` on Apple arm64, macOS + 26.2 build 25C56, Node 26.5.0, pnpm 10.24.0. +- Diagnostic boundary: before the unchanged product full-size test, the native Windows workflow runs + one purpose-named live case. It first passes parent stdin through with mandatory `-n` as the + upstream `GetStdHandle` control, then gives `ssh.exe` Node `overlapped` stdin/stdout/stderr pipes + and immediately destroys only the parent stdin writer. Each attempt records only mode, bounded + timing, sentinel/stdout-end/process-exit/channel-close/code, and stderr byte count; it terminates at + 8 seconds with a 10-second hard settlement bound. The inherited control must pass so a broken + fixture cannot falsely reject the overlapped candidate. No production handle selection changes. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two declared native skips + in 10.10 seconds real; maximum RSS is 168,984,576 bytes and peak footprint is 97,507,256 bytes. +- Release contracts: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 --reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 50 files and + 283/283 cases in 44.50 seconds real; maximum RSS is 195,706,880 bytes and peak footprint is + 97,343,296 bytes. +- Static/workflow gates: `/usr/bin/time -l pnpm typecheck` passes in 5.59 seconds real with maximum + RSS 1,235,714,048 bytes and peak footprint 97,638,088 bytes. `/usr/bin/time -l pnpm lint` passes in + 28.07 seconds real with only the 26 pre-existing warnings, all 41 reliability gates, and the + 355-entry max-lines ratchet; maximum RSS is 2,066,743,296 bytes and peak footprint 98,047,832 + bytes. Independent YAML extraction plus PowerShell `[scriptblock]::Create()` parses the exact + start/diagnostic+measure/stop blocks; the workflow oracle proves the diagnostic precedes the + full-size test and fails its step if the inherited control is invalid. +- Residual boundary: local macOS intentionally skips the live case. Require exact-head native x64 + and ARM64 output before selecting any Windows handle; keep profile teardown, full-size transfer, + default behavior, fallback, Beta, tuple publication, and SignPath separately gated. ## Accepted Gaps @@ -19425,13 +19492,13 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the locally green isolated correction in -`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-EXACT-HEAD-CORRECTION-LOCAL-001`, then require fresh exact-head -x64/arm64 archive/native-closure/signature, ACL, PowerShell 5.1, exact -full-size serial/four-channel/cancellation, collision, connection-reuse, and teardown evidence. Do -not check the broad Milestone 6 Windows system-SSH item yet. Do not add an Electron/startup/product -importer, per-target mode wiring, fallback, tuple enablement, release publication, or default -behavior. +Implement the purpose-named disconnected Windows no-input handle diagnostic required by +`E-M6-WINDOWS-NOINPUT-STANDARD-PIPE-CI-RED-001`. Compare an overlapped pipe with immediate parent +destruction against inherited stdin, keep each attempt bounded, and record sentinel/stdout-end/ +process-exit/channel-close on native x64 and ARM64. Do not change the production Windows handle or +check the broad Milestone 6 Windows system-SSH item until the diagnostic selects a console-independent +strategy. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple +enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 91a6603267d..9a1e9a8c39a 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1646,10 +1646,13 @@

    Prove production behavior, failure behavior, and rollback behavior.

    stdout-end, sentinel, and channel-close settlement; POSIX no-input probes use an OS-level ignored stdin handle plus OpenSSH -n; Windows must not map stdin to NUL because Win32-OpenSSH issues #856/#1330 document a - redirected-handle hang, so it receives an anonymous pipe whose parent write end - is destroyed immediately plus -n; fixture-owned Windows session - processes settle before profile hives are boundedly unloaded and native profile - deletion begins; MaxSessions=1; interrupted upload; stale lock + redirected-handle hang; a standard anonymous pipe with its parent write end + destroyed is also rejected by native x64/arm64 evidence; qualify a + console-independent overlapped-pipe EOF strategy against an inherited-stdin + control before selecting the Windows handle, always with -n; + fixture-owned Windows session processes settle before profile hives are + boundedly unloaded and native profile deletion begins; + MaxSessions=1; interrupted upload; stale lock Bundled Node hashes the full staged tree before sentinel write; diff --git a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts new file mode 100644 index 00000000000..94aa511423a --- /dev/null +++ b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts @@ -0,0 +1,158 @@ +import { spawn } from 'node:child_process' +import { performance } from 'node:perf_hooks' + +import { describe, expect, it } from 'vitest' + +import type { SshTarget } from '../../shared/ssh-types' +import { buildSshArgs } from './system-ssh-args' +import { findSystemSsh } from './system-ssh-binary' + +type DiagnosticMode = 'inherited' | 'overlapped' + +type DiagnosticResult = { + mode: DiagnosticMode + success: boolean + timedOut: boolean + sentinel: boolean + stdoutEnded: boolean + processExit: number | null | 'not-observed' + channelClosed: boolean + closeCode: number | null | 'not-observed' + stderrBytes: number + elapsedMs: number +} + +const host = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_HOST +const user = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_USER +const identityFile = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_IDENTITY +const port = Number.parseInt(process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_PORT ?? '', 10) +const hasLiveInput = Boolean(host && user && identityFile && Number.isInteger(port)) + +function createTarget(): SshTarget { + return { + id: 'live-windows-no-input-handle-diagnostic', + label: 'live-windows-no-input-handle-diagnostic', + host: host as string, + port, + username: user as string, + identityFile: identityFile as string, + identitiesOnly: true, + systemSshConnectionReuse: true, + source: 'manual' + } +} + +async function runDiagnostic(mode: DiagnosticMode): Promise { + const sshPath = findSystemSsh() + if (!sshPath) { + throw new Error('Native Windows OpenSSH client is unavailable') + } + const startedAt = performance.now() + const child = spawn( + sshPath, + [...buildSshArgs(createTarget(), { noInput: true }), 'echo ORCA-SYSTEM-SSH-OK'], + { + // Why: qualify a console-independent overlapped pipe against the + // upstream inherited-handle control before changing production. + stdio: + mode === 'overlapped' + ? ['overlapped', 'overlapped', 'overlapped'] + : ['inherit', 'pipe', 'pipe'], + windowsHide: true + } + ) + if (mode === 'overlapped') { + child.stdin?.destroy() + } + + return new Promise((resolve, reject) => { + let stdout = '' + let stderrBytes = 0 + let stdoutEnded = false + let processExit: number | null | 'not-observed' = 'not-observed' + let channelClosed = false + let closeCode: number | null | 'not-observed' = 'not-observed' + let timedOut = false + let settled = false + const finish = (): void => { + if (settled) { + return + } + settled = true + clearTimeout(timeout) + clearTimeout(killTimeout) + const sentinel = stdout.includes('ORCA-SYSTEM-SSH-OK') + resolve({ + mode, + success: + !timedOut && + sentinel && + stdoutEnded && + processExit === 0 && + channelClosed && + closeCode === 0, + timedOut, + sentinel, + stdoutEnded, + processExit, + channelClosed, + closeCode, + stderrBytes, + elapsedMs: performance.now() - startedAt + }) + } + const timeout = setTimeout(() => { + timedOut = true + child.kill('SIGTERM') + }, 8_000) + const killTimeout = setTimeout(() => { + timedOut = true + child.kill('SIGKILL') + child.stdout?.destroy() + child.stderr?.destroy() + child.unref() + finish() + }, 10_000) + + child.stdout?.on('data', (chunk: Buffer) => { + stdout += chunk.toString('utf8') + }) + child.stdout?.on('end', () => { + stdoutEnded = true + }) + child.stderr?.on('data', (chunk: Buffer) => { + stderrBytes += chunk.length + }) + child.on('exit', (code) => { + processExit = code + }) + child.on('close', (code) => { + channelClosed = true + closeCode = code + finish() + }) + child.on('error', (error) => { + clearTimeout(timeout) + clearTimeout(killTimeout) + reject(error) + }) + }) +} + +describe.skipIf(!hasLiveInput)('Windows OpenSSH no-input child-handle diagnostic', () => { + it( + 'compares inherited stdin with a console-independent overlapped EOF pipe', + { timeout: 30_000 }, + async () => { + expect(process.platform).toBe('win32') + const inherited = await runDiagnostic('inherited') + const overlapped = await runDiagnostic('overlapped') + console.log( + `ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ inherited, overlapped })}` + ) + + // The inherited control must settle or this fixture cannot distinguish handle strategies. + expect(inherited.success).toBe(true) + } + ) +}) From daffd545d75570f198db093c3ba054ae1fa033ba Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 21:27:18 -0400 Subject: [PATCH 250/267] test(ssh): isolate Windows no-input pipe EOF Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 17 ++-- ...github-release-implementation-checklist.md | 77 +++++++++++++++++-- ...6-07-14-ssh-relay-github-release-plan.html | 16 ++-- ...sh-system-no-input-windows-openssh.test.ts | 42 ++++------ 4 files changed, 105 insertions(+), 47 deletions(-) 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 6bde62110ff..8023221f18c 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 @@ -123,12 +123,17 @@ independently returns the same lifecycle RED. The corrected POSIX contract uses `Writable` facade so ending it cannot half-close the command channel. Exact correction head `4f57b885d24ad1755a5c863eaf37369208cecedb`, artifact run `29545688003`, and Linux x64/ARM64 jobs `87777401256`/`87777401218` are green through live SFTP and restricted no-tar POSIX system -SSH. Windows x64/ARM64 jobs `87777401200`/`87777401195` independently falsify the standard -anonymous-pipe EOF hypothesis with the unchanged four-field timeout and profile RED. Next compare a -console-independent overlapped pipe against inherited stdin as a diagnostic control; do not change -the production Windows handle until native evidence selects it. The disconnected bounded diagnostic -and workflow ordering are locally green under -`E-M6-WINDOWS-NOINPUT-HANDLE-DIAGNOSTIC-LOCAL-001`; native x64/ARM64 execution is next. +SSH. Windows x64/ARM64 jobs `87777401200`/`87777401195` reproduce the unchanged four-field timeout +with a standard pipe plus mandatory `-n`. Exact diagnostic head +`da938a7405d0a6c760979358a6c0a38030d9acaf`, artifact run `29546702916`, and Windows x64/ARM64 jobs +`87780486286`/`87780486259` prove that inherited and overlapped handles also time out when every +candidate retains `-n`; the invalid inherited control and shared `-n` mean these runs do not isolate +the child handle. `E-M6-WINDOWS-NOINPUT-HANDLE-DIAGNOSTIC-CI-RED-001` therefore requires the next +single-variable diagnostic to use the upstream-recommended standard stdin pipe with immediate EOF +and omit `-n` only on Windows. POSIX retains ignored stdin plus `-n`; production stays unchanged +until x64 and ARM64 select the same console-independent strategy. The replacement disconnected +diagnostic is locally green under `E-M6-WINDOWS-NOINPUT-PIPE-NO-N-LOCAL-001`; commit and run that +exact head next. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown 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 a0c6408218d..d29ff215318 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: native Windows no-input handle qualification**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **POSIX EOF CORRECTION NATIVELY GREEN / STANDARD WINDOWS PIPE NATIVELY RED / HANDLE DIAGNOSTIC LOCALLY GREEN — 2026-07-15, Codex implementation owner** — exact correction head `4f57b885d24ad1755a5c863eaf37369208cecedb`, artifact run `29545688003`, and Linux x64/ARM64 jobs `87777401256`/`87777401218` prove the separate no-input facade through live SFTP and restricted no-tar POSIX system SSH with bounded full-size transfer, four-file concurrency, cancellation, and cleanup. Windows x64/ARM64 jobs `87777401200`/`87777401195` independently falsify the standard anonymous-pipe EOF hypothesis: official OpenSSH authenticates and the remote command closes, but clients time out after 31.36/31.82 seconds with `sentinel=false`, `stdoutEnded=false`, `processExit=not-observed`, and `channelClosed=false`; exact profile teardown remains RED. `E-M6-WINDOWS-NOINPUT-HANDLE-DIAGNOSTIC-LOCAL-001` adds a bounded, non-sensitive native comparison of a console-independent overlapped pipe and inherited-stdin upstream control before the unchanged full-size measurement; focused 117+2, 283-contract, typecheck, full lint/reliability/max-lines, all three PowerShell parse, formatting, diff, and protected-resolver gates pass locally. Commit/push and require native x64/ARM64 diagnostic output before selecting another production handle. Do not advance Milestone 6. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Session checkpoint: **POSIX EOF CORRECTION NATIVELY GREEN / WINDOWS `-n` HANDLE DIAGNOSTIC NATIVELY RED / NO-`-n` PIPE DIAGNOSTIC LOCALLY GREEN — 2026-07-15, Codex implementation owner** — exact correction head `4f57b885d24ad1755a5c863eaf37369208cecedb`, artifact run `29545688003`, and Linux x64/ARM64 jobs `87777401256`/`87777401218` prove the separate no-input facade through live SFTP and restricted no-tar POSIX system SSH with bounded full-size transfer, four-file concurrency, cancellation, and cleanup. Windows x64/ARM64 jobs `87777401200`/`87777401195` reproduce the no-input hang with a standard pipe plus mandatory `-n`. Exact diagnostic head `da938a7405d0a6c760979358a6c0a38030d9acaf`, artifact run `29546702916`, and Windows x64/ARM64 jobs `87780486286`/`87780486259` prove that inherited and overlapped handles also time out when every candidate retains `-n`; both inherited controls are invalid on redirected hosted-runner stdin, so the runs do not isolate the child handle. Each x64 attempt reports 8.03/8.02 seconds and each ARM64 attempt 8.04/8.02 seconds with `sentinel=false`, `stdoutEnded=true`, termination-only null exit/close codes, zero stderr bytes, and exact profile teardown RED. `E-M6-WINDOWS-NOINPUT-PIPE-NO-N-LOCAL-001` replaces only the disconnected diagnostic with the upstream-recommended standard stdin pipe, immediate parent EOF, ordinary stdout/stderr, and no `-n`; focused 117+2, 283-contract, format, typecheck, full lint/reliability/max-lines, diff, and protected-resolver gates pass locally. Commit/push and require a fresh exact-head x64/ARM64 result next. Do not change production or advance Milestone 6 without both native results. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -19433,6 +19433,69 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two de - Residual boundary: local macOS intentionally skips the live case. Require exact-head native x64 and ARM64 output before selecting any Windows handle; keep profile teardown, full-size transfer, default behavior, fallback, Beta, tuple publication, and SignPath separately gated. +- Native proof result: exact diagnostic head `da938a7405d0a6c760979358a6c0a38030d9acaf`, artifact run + `29546702916`, Windows x64 job `87780486286`, and Windows ARM64 job `87780486259` are RED as + recorded by `E-M6-WINDOWS-NOINPUT-HANDLE-DIAGNOSTIC-CI-RED-001`; the workflow correctly stops + before the following unchanged product full-size result. + +### E-M6-WINDOWS-NOINPUT-HANDLE-DIAGNOSTIC-CI-RED-001 — mandatory `-n` invalidates every hosted handle control + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact pushed head + `da938a7405d0a6c760979358a6c0a38030d9acaf`, artifact run `29546702916`, Windows x64 job + `87780486286` on `windows-2022`, and Windows ARM64 job `87780486259` on `windows-11-arm`. +- Exact log command: `gh api repos/stablyai/orca/actions/jobs//logs | rg -n -C 10 +'ssh_windows_no_input_handle_diagnostic|Windows no-input handle diagnostic failed|profile +teardown:|UsrClass.dat'` for each job. +- X64 result: inherited and overlapped attempts both return `success=false`, `timedOut=true`, + `sentinel=false`, `stdoutEnded=true`, `processExit=null`, `channelClosed=true`, `closeCode=null`, + and `stderrBytes=0` after 8029.88/8021.57 milliseconds. ARM64 independently returns the same + fields after 8044.18/8015.22 milliseconds. These exit/close fields are termination settlement, + not remote-command success. +- Teardown result: both exact jobs fail `_Classes` hive unload, retain locked `UsrClass.dat`, and + retain the fixture profile directory. The workflow correctly stops before the unchanged product + full-size test; no Windows transport cell is green and no tuple is enabled. +- Interpretation: the inherited control is invalid because hosted-runner stdin is itself redirected. + More importantly, inherited, standard-pipe, and overlapped candidates all retained OpenSSH `-n`, + which routes Windows no-input handling through the same null-input boundary implicated by + Win32-OpenSSH #856/#1330. These runs cannot reject the child pipe independently of `-n`. +- Course correction: before production changes, run one bounded single-variable diagnostic using + Node's standard stdin pipe, destroy the parent writer immediately, keep stdout/stderr as ordinary + pipes, and omit `-n` only for that Windows attempt. This is the upstream-recommended pipe EOF + boundary without the confounding null-input option. Test an overlapped stdin pipe without `-n` + only if the standard pipe remains RED. POSIX keeps ignored stdin plus `-n`. +- Residual boundary: require exact-head x64 and ARM64 lifecycle and exact teardown proof. Keep the + product handle, default, legacy fallback, Beta, tuple publication, and SignPath unchanged. + +### E-M6-WINDOWS-NOINPUT-PIPE-NO-N-LOCAL-001 — unconfounded standard-pipe EOF diagnostic is locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted replacement atop exact + RED head `da938a7405d0a6c760979358a6c0a38030d9acaf` on Apple arm64, macOS 26.2 build 25C56, + Node 26.5.0, pnpm 10.24.0. +- Diagnostic boundary: the disconnected Windows live test now spawns `ssh.exe` with standard stdin, + stdout, and stderr pipes, destroys the parent stdin writer immediately, and deliberately calls + `buildSshArgs` without `noInput` so OpenSSH receives no `-n`. The 8-second timeout and 10-second + hard settlement bound, non-sensitive lifecycle fields, workflow ordering, and fail-closed step + remain unchanged. Production args, handles, no-input facade, and product full-size test are not + changed. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two expected native + skips in 4.07 seconds real; maximum RSS is 168,067,072 bytes and peak footprint 97,589,080 bytes. +- Release contracts: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 --reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 50 files and + 283/283 cases in 25.92 seconds real; maximum RSS is 195,493,888 bytes and peak footprint + 97,687,288 bytes. +- Static gates: `/usr/bin/time -l pnpm typecheck` passes in 5.73 seconds real with maximum RSS + 1,225,736,192 bytes and peak footprint 96,687,936 bytes. `/usr/bin/time -l pnpm lint` passes in + 24.85 seconds real with the same 26 pre-existing warnings, all 41 reliability gates, and the + 355-entry max-lines ratchet; maximum RSS is 2,055,323,648 bytes and peak footprint 98,260,872 + bytes. `pnpm exec oxfmt --check `, `git diff --check`, and the protected + resolver zero-diff gate pass after formatting. +- Residual boundary: local macOS skips both native live cases. Commit the diagnostic and evidence, + then require a fresh exact-head artifact run with the same successful lifecycle on Windows x64 + and ARM64 plus exact profile/directory teardown before any production handle change. ## Accepted Gaps @@ -19492,12 +19555,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Implement the purpose-named disconnected Windows no-input handle diagnostic required by -`E-M6-WINDOWS-NOINPUT-STANDARD-PIPE-CI-RED-001`. Compare an overlapped pipe with immediate parent -destruction against inherited stdin, keep each attempt bounded, and record sentinel/stdout-end/ -process-exit/channel-close on native x64 and ARM64. Do not change the production Windows handle or -check the broad Milestone 6 Windows system-SSH item until the diagnostic selects a console-independent -strategy. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple +Commit and push the disconnected no-`-n` standard-pipe diagnostic recorded by +`E-M6-WINDOWS-NOINPUT-PIPE-NO-N-LOCAL-001`, then run the exact head on Windows x64 and ARM64 and +record sentinel/stdout-end/process-exit/channel-close plus exact profile/directory teardown. Do not +change the production Windows handle or check the broad Milestone 6 Windows system-SSH item until +both architectures select the same console-independent strategy. Do not add an +Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 9a1e9a8c39a..9d12a8dc7cb 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1646,13 +1646,15 @@

    Prove production behavior, failure behavior, and rollback behavior.

    stdout-end, sentinel, and channel-close settlement; POSIX no-input probes use an OS-level ignored stdin handle plus OpenSSH -n; Windows must not map stdin to NUL because Win32-OpenSSH issues #856/#1330 document a - redirected-handle hang; a standard anonymous pipe with its parent write end - destroyed is also rejected by native x64/arm64 evidence; qualify a - console-independent overlapped-pipe EOF strategy against an inherited-stdin - control before selecting the Windows handle, always with -n; - fixture-owned Windows session processes settle before profile hives are - boundedly unloaded and native profile deletion begins; - MaxSessions=1; interrupted upload; stale lock + redirected-handle hang; native x64/arm64 evidence also proves that inherited, + standard-pipe, and overlapped-pipe handles all hang when the Windows client is + simultaneously forced through -n, so those runs do not isolate the + child handle. Qualify the upstream-recommended standard-pipe EOF with Windows + -n omitted as the next single-variable diagnostic; only test an + overlapped stdin pipe, also without -n, if the standard pipe fails. + POSIX retains ignored stdin plus -n; fixture-owned Windows session + processes settle before profile hives are boundedly unloaded and native profile + deletion begins; MaxSessions=1; interrupted upload; stale lock Bundled Node hashes the full staged tree before sentinel write; diff --git a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts index 94aa511423a..ed3bf45f24b 100644 --- a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +++ b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts @@ -7,7 +7,7 @@ import type { SshTarget } from '../../shared/ssh-types' import { buildSshArgs } from './system-ssh-args' import { findSystemSsh } from './system-ssh-binary' -type DiagnosticMode = 'inherited' | 'overlapped' +type DiagnosticMode = 'pipe-eof-no-n' type DiagnosticResult = { mode: DiagnosticMode @@ -42,28 +42,20 @@ function createTarget(): SshTarget { } } -async function runDiagnostic(mode: DiagnosticMode): Promise { +async function runDiagnostic(): Promise { const sshPath = findSystemSsh() if (!sshPath) { throw new Error('Native Windows OpenSSH client is unavailable') } + const mode: DiagnosticMode = 'pipe-eof-no-n' const startedAt = performance.now() - const child = spawn( - sshPath, - [...buildSshArgs(createTarget(), { noInput: true }), 'echo ORCA-SYSTEM-SSH-OK'], - { - // Why: qualify a console-independent overlapped pipe against the - // upstream inherited-handle control before changing production. - stdio: - mode === 'overlapped' - ? ['overlapped', 'overlapped', 'overlapped'] - : ['inherit', 'pipe', 'pipe'], - windowsHide: true - } - ) - if (mode === 'overlapped') { - child.stdin?.destroy() - } + const child = spawn(sshPath, [...buildSshArgs(createTarget()), 'echo ORCA-SYSTEM-SSH-OK'], { + // Why: isolate the upstream-recommended pipe EOF from Win32-OpenSSH's + // null-input path before changing the production no-input command. + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true + }) + child.stdin?.destroy() return new Promise((resolve, reject) => { let stdout = '' @@ -141,18 +133,14 @@ async function runDiagnostic(mode: DiagnosticMode): Promise { describe.skipIf(!hasLiveInput)('Windows OpenSSH no-input child-handle diagnostic', () => { it( - 'compares inherited stdin with a console-independent overlapped EOF pipe', - { timeout: 30_000 }, + 'qualifies a standard EOF pipe without the Windows null-input option', + { timeout: 15_000 }, async () => { expect(process.platform).toBe('win32') - const inherited = await runDiagnostic('inherited') - const overlapped = await runDiagnostic('overlapped') - console.log( - `ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ inherited, overlapped })}` - ) + const pipeEofNoN = await runDiagnostic() + console.log(`ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ pipeEofNoN })}`) - // The inherited control must settle or this fixture cannot distinguish handle strategies. - expect(inherited.success).toBe(true) + expect(pipeEofNoN.success).toBe(true) } ) }) From 1c391aa851777f36cf81af9adb85d7177d7acff6 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 21:47:26 -0400 Subject: [PATCH 251/267] test(ssh): qualify overlapped Windows stdin Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 8 ++- ...github-release-implementation-checklist.md | 68 +++++++++++++++++-- ...6-07-14-ssh-relay-github-release-plan.html | 14 ++-- ...sh-system-no-input-windows-openssh.test.ts | 12 ++-- 4 files changed, 85 insertions(+), 17 deletions(-) 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 8023221f18c..5104c3f1768 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 @@ -133,7 +133,13 @@ single-variable diagnostic to use the upstream-recommended standard stdin pipe w and omit `-n` only on Windows. POSIX retains ignored stdin plus `-n`; production stays unchanged until x64 and ARM64 select the same console-independent strategy. The replacement disconnected diagnostic is locally green under `E-M6-WINDOWS-NOINPUT-PIPE-NO-N-LOCAL-001`; commit and run that -exact head next. +exact head next. Exact pushed head `daffd545d75570f198db093c3ba054ae1fa033ba` is under test in +artifact run `29547488550`, Windows x64 job `87782833321`, and Windows ARM64 job `87782833288`. +`E-M6-WINDOWS-NOINPUT-PIPE-NO-N-CI-RED-001` records the completed native result: both architectures +time out at 8.03/8.04 seconds with no sentinel and exact profile RED. Next test only an overlapped +stdin pipe with ordinary stdout/stderr and no `-n`; production remains unchanged. That replacement +diagnostic is locally green under `E-M6-WINDOWS-NOINPUT-OVERLAPPED-NO-N-LOCAL-001`; commit and run +the exact head next. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown 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 d29ff215318..2d0d09f6b6c 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 @@ -9,7 +9,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-15
    Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: native Windows no-input handle qualification**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **POSIX EOF CORRECTION NATIVELY GREEN / WINDOWS `-n` HANDLE DIAGNOSTIC NATIVELY RED / NO-`-n` PIPE DIAGNOSTIC LOCALLY GREEN — 2026-07-15, Codex implementation owner** — exact correction head `4f57b885d24ad1755a5c863eaf37369208cecedb`, artifact run `29545688003`, and Linux x64/ARM64 jobs `87777401256`/`87777401218` prove the separate no-input facade through live SFTP and restricted no-tar POSIX system SSH with bounded full-size transfer, four-file concurrency, cancellation, and cleanup. Windows x64/ARM64 jobs `87777401200`/`87777401195` reproduce the no-input hang with a standard pipe plus mandatory `-n`. Exact diagnostic head `da938a7405d0a6c760979358a6c0a38030d9acaf`, artifact run `29546702916`, and Windows x64/ARM64 jobs `87780486286`/`87780486259` prove that inherited and overlapped handles also time out when every candidate retains `-n`; both inherited controls are invalid on redirected hosted-runner stdin, so the runs do not isolate the child handle. Each x64 attempt reports 8.03/8.02 seconds and each ARM64 attempt 8.04/8.02 seconds with `sentinel=false`, `stdoutEnded=true`, termination-only null exit/close codes, zero stderr bytes, and exact profile teardown RED. `E-M6-WINDOWS-NOINPUT-PIPE-NO-N-LOCAL-001` replaces only the disconnected diagnostic with the upstream-recommended standard stdin pipe, immediate parent EOF, ordinary stdout/stderr, and no `-n`; focused 117+2, 283-contract, format, typecheck, full lint/reliability/max-lines, diff, and protected-resolver gates pass locally. Commit/push and require a fresh exact-head x64/ARM64 result next. Do not change production or advance Milestone 6 without both native results. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Session checkpoint: **POSIX EOF CORRECTION NATIVELY GREEN / WINDOWS STANDARD PIPE WITHOUT `-n` NATIVELY RED / OVERLAPPED-STDIN DIAGNOSTIC LOCALLY GREEN — 2026-07-15, Codex implementation owner** — exact correction head `4f57b885d24ad1755a5c863eaf37369208cecedb`, artifact run `29545688003`, and Linux x64/ARM64 jobs `87777401256`/`87777401218` prove the separate no-input facade through live SFTP and restricted no-tar POSIX system SSH with bounded full-size transfer, four-file concurrency, cancellation, and cleanup. Exact diagnostic head `da938a7405d0a6c760979358a6c0a38030d9acaf`, artifact run `29546702916`, and Windows x64/ARM64 jobs `87780486286`/`87780486259` prove that mandatory `-n` confounds inherited and overlapped handles. `E-M6-WINDOWS-NOINPUT-PIPE-NO-N-CI-RED-001` records exact pushed head `daffd545d75570f198db093c3ba054ae1fa033ba`, artifact run `29547488550`, Windows x64/ARM64 jobs `87782833321`/`87782833288`, and independent standard-pipe/no-`-n` timeouts after 8.03/8.04 seconds with `sentinel=false`, `stdoutEnded=true`, termination-only null exit/close codes, zero stderr bytes, and exact profile teardown RED. The same run keeps Linux x64/ARM64 full-size live SFTP and restricted POSIX system SSH green. `E-M6-WINDOWS-NOINPUT-OVERLAPPED-NO-N-LOCAL-001` replaces only the disconnected diagnostic with stdin-only `overlapped`, ordinary stdout/stderr, immediate parent destruction, and no `-n`; focused 117+2, 283-contract, format, typecheck, full lint/reliability/max-lines, diff, and protected-resolver gates pass locally. Commit/push and require fresh exact-head x64/ARM64 proof; do not change production or advance Milestone 6 without both native results. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -19496,6 +19496,66 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two ex - Residual boundary: local macOS skips both native live cases. Commit the diagnostic and evidence, then require a fresh exact-head artifact run with the same successful lifecycle on Windows x64 and ARM64 plus exact profile/directory teardown before any production handle change. +- Native proof result: exact pushed head `daffd545d75570f198db093c3ba054ae1fa033ba`, artifact run + `29547488550`, Windows x64 job `87782833321`, and Windows ARM64 job `87782833288` are RED as + recorded by `E-M6-WINDOWS-NOINPUT-PIPE-NO-N-CI-RED-001`. + +### E-M6-WINDOWS-NOINPUT-PIPE-NO-N-CI-RED-001 — standard pipe EOF after spawn fails without `-n` + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact pushed head + `daffd545d75570f198db093c3ba054ae1fa033ba`, artifact run `29547488550`, Windows x64 job + `87782833321` on `windows-2022`, and Windows ARM64 job `87782833288` on `windows-11-arm`. +- Exact log command: `gh api repos/stablyai/orca/actions/jobs//logs | rg -n -C 12 +'ssh_windows_no_input_handle_diagnostic|Windows no-input handle diagnostic failed|profile +teardown:|UsrClass.dat'` for each job. +- Native result: x64 returns `success=false`, `timedOut=true`, `sentinel=false`, + `stdoutEnded=true`, `processExit=null`, `channelClosed=true`, `closeCode=null`, and + `stderrBytes=0` after 8033.45 milliseconds. ARM64 independently returns the same fields after + 8040.73 milliseconds. These exit/close fields are termination settlement, not remote-command + success; server logs show pinned-key authentication and command-session close. +- Teardown result: both jobs fail `_Classes` hive unload, retain locked `UsrClass.dat`, and retain + the fixture profile directory. The workflow correctly stops before the unchanged product + full-size test, so no Windows transport cell is green and no tuple is enabled. +- Preserved evidence: the same exact run keeps Linux x64 job `87782833306` and Linux ARM64 job + `87782833305` green through target-native build, full-size cache, live SFTP, restricted no-tar + POSIX system SSH, cancellation, cleanup, and artifact upload. +- Interpretation/course correction: omitting `-n` removes one confounder but Node's standard parent + pipe is closed only after `CreateProcess`; it does not reproduce the upstream pre-closed writer + boundary. Per the HTML plan, test stdin-only `overlapped` with ordinary stdout/stderr, immediate + parent destruction, and no `-n` as the next bounded disconnected diagnostic. Do not change + production unless both architectures prove it and exact teardown passes. +- Residual boundary: keep product handles/args, legacy default/fallback, Beta, tuple publication, + and SignPath unchanged. + +### E-M6-WINDOWS-NOINPUT-OVERLAPPED-NO-N-LOCAL-001 — stdin-only overlapped EOF diagnostic is locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted replacement atop exact + standard-pipe RED head `daffd545d75570f198db093c3ba054ae1fa033ba` on Apple arm64, macOS 26.2 + build 25C56, Node 26.5.0, pnpm 10.24.0. +- Diagnostic boundary: the disconnected native Windows test uses `overlapped` only for child stdin, + keeps stdout/stderr as ordinary pipes, destroys the parent stdin writer immediately, and omits + OpenSSH `-n`. The bounded lifecycle fields, 8-second timeout, 10-second hard settlement, workflow + ordering, and fail-closed step remain unchanged. Production args/handles and the product full-size + test are unchanged. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two expected native + skips in 13.33 seconds real under concurrent workspace load; maximum RSS is 173,408,256 bytes and + peak footprint 97,605,344 bytes. +- Release contracts: the same purpose-named 50-file / 283-case command recorded above passes + 283/283 in 106.82 seconds real under concurrent workspace load; maximum RSS is 167,051,264 bytes + and peak footprint 97,703,792 bytes. +- Static gates: `/usr/bin/time -l pnpm typecheck` passes in 14.16 seconds real with maximum RSS + 1,221,165,056 bytes and peak footprint 98,932,520 bytes. `/usr/bin/time -l pnpm lint` passes in + 82.69 seconds real with the same 26 pre-existing warnings, all 41 reliability gates, and the + 355-entry max-lines ratchet; maximum RSS is 2,039,726,080 bytes and peak footprint 98,162,520 + bytes. These wall times retain concurrent-workspace load rather than replacing it with estimated + figures. +- Residual boundary: format, diff, and protected-resolver gates remain required before commit. Then + require exact-head Windows x64 and ARM64 lifecycle plus exact profile/directory teardown before + any production handle change. ## Accepted Gaps @@ -19555,9 +19615,9 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the disconnected no-`-n` standard-pipe diagnostic recorded by -`E-M6-WINDOWS-NOINPUT-PIPE-NO-N-LOCAL-001`, then run the exact head on Windows x64 and ARM64 and -record sentinel/stdout-end/process-exit/channel-close plus exact profile/directory teardown. Do not +Commit and push the stdin-only overlapped/no-`-n` disconnected diagnostic recorded by +`E-M6-WINDOWS-NOINPUT-OVERLAPPED-NO-N-LOCAL-001`, then require exact-head Windows x64 and ARM64 +lifecycle plus exact profile/directory teardown. Do not change the production Windows handle or check the broad Milestone 6 Windows system-SSH item until both architectures select the same console-independent strategy. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 9d12a8dc7cb..8682852dda5 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1649,12 +1649,14 @@

    Prove production behavior, failure behavior, and rollback behavior.

    redirected-handle hang; native x64/arm64 evidence also proves that inherited, standard-pipe, and overlapped-pipe handles all hang when the Windows client is simultaneously forced through -n, so those runs do not isolate the - child handle. Qualify the upstream-recommended standard-pipe EOF with Windows - -n omitted as the next single-variable diagnostic; only test an - overlapped stdin pipe, also without -n, if the standard pipe fails. - POSIX retains ignored stdin plus -n; fixture-owned Windows session - processes settle before profile hives are boundedly unloaded and native profile - deletion begins; MaxSessions=1; interrupted upload; stale lock + child handle. Native x64/arm64 evidence further rejects the upstream-recommended + standard-pipe EOF when Windows -n is omitted because Node closes + its parent writer only after CreateProcess. Qualify an overlapped + stdin pipe with ordinary stdout/stderr and no -n as the next + disconnected diagnostic. POSIX retains ignored stdin plus -n; + fixture-owned Windows session processes settle before profile hives are + boundedly unloaded and native profile deletion begins; + MaxSessions=1; interrupted upload; stale lock Bundled Node hashes the full staged tree before sentinel write; diff --git a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts index ed3bf45f24b..9bb61af98bc 100644 --- a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +++ b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts @@ -7,7 +7,7 @@ import type { SshTarget } from '../../shared/ssh-types' import { buildSshArgs } from './system-ssh-args' import { findSystemSsh } from './system-ssh-binary' -type DiagnosticMode = 'pipe-eof-no-n' +type DiagnosticMode = 'overlapped-stdin-eof-no-n' type DiagnosticResult = { mode: DiagnosticMode @@ -47,12 +47,12 @@ async function runDiagnostic(): Promise { if (!sshPath) { throw new Error('Native Windows OpenSSH client is unavailable') } - const mode: DiagnosticMode = 'pipe-eof-no-n' + const mode: DiagnosticMode = 'overlapped-stdin-eof-no-n' const startedAt = performance.now() const child = spawn(sshPath, [...buildSshArgs(createTarget()), 'echo ORCA-SYSTEM-SSH-OK'], { - // Why: isolate the upstream-recommended pipe EOF from Win32-OpenSSH's - // null-input path before changing the production no-input command. - stdio: ['pipe', 'pipe', 'pipe'], + // Why: isolate Windows overlapped stdin without changing output capture + // or the production no-input command. + stdio: ['overlapped', 'pipe', 'pipe'], windowsHide: true }) child.stdin?.destroy() @@ -133,7 +133,7 @@ async function runDiagnostic(): Promise { describe.skipIf(!hasLiveInput)('Windows OpenSSH no-input child-handle diagnostic', () => { it( - 'qualifies a standard EOF pipe without the Windows null-input option', + 'qualifies overlapped stdin EOF without the Windows null-input option', { timeout: 15_000 }, async () => { expect(process.platform).toBe('win32') From eee686d7f6203c88f00d607711d3816aaf444c61 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 22:09:33 -0400 Subject: [PATCH 252/267] test(ssh): qualify pre-execution Windows EOF Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 18 ++++- ...github-release-implementation-checklist.md | 72 +++++++++++++++++-- ...6-07-14-ssh-relay-github-release-plan.html | 14 ++-- ...sh-system-no-input-windows-openssh.test.ts | 58 +++++++++++---- 4 files changed, 136 insertions(+), 26 deletions(-) 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 5104c3f1768..bc720b87bd2 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 @@ -9,8 +9,8 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — exact-head Windows -OpenSSH live-proof diagnosis and fixture correction, 2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — pre-execution Windows +regular-file EOF qualification, 2026-07-15, Codex implementation owner.** `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain @@ -139,7 +139,19 @@ artifact run `29547488550`, Windows x64 job `87782833321`, and Windows ARM64 job time out at 8.03/8.04 seconds with no sentinel and exact profile RED. Next test only an overlapped stdin pipe with ordinary stdout/stderr and no `-n`; production remains unchanged. That replacement diagnostic is locally green under `E-M6-WINDOWS-NOINPUT-OVERLAPPED-NO-N-LOCAL-001`; commit and run -the exact head next. +the exact head next. Exact pushed head `1c391aa851777f36cf81af9adb85d7177d7acff6` was tested in +artifact run `29548340927`, Windows x64 job `87785371486`, and Windows ARM64 job `87785371526`. +`E-M6-WINDOWS-NOINPUT-OVERLAPPED-NO-N-CI-RED-001` records the completed native result: x64/ARM64 +time out after 8035.00/8043.47 milliseconds with no sentinel, ended stdout, zero stderr, and only +termination settlement; both exact profile teardowns are RED. All four non-Windows primary jobs +and both Linux oldest-userland supplements remain green. Next qualify only a newly created +zero-length regular-file descriptor as Windows stdin, already at EOF before `CreateProcess` and +distinct from `NUL`; do not add a launcher unless this narrower boundary is falsified. Require both +architectures to pass the diagnostic, full-size live transfer, cancellation, reuse, and exact +profile teardown before changing production. `E-M6-WINDOWS-NOINPUT-REGULAR-FILE-NO-N-LOCAL-001` +records the disconnected replacement locally green: focused 117+2, all 283 release contracts, +typecheck, full lint/reliability/max-lines, and formatting pass. Commit/push this exact isolated +diagnostic and require fresh native x64/ARM64 proof; SignPath remains deferred. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown 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 2d0d09f6b6c..eb7d27ebbbc 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: native Windows no-input handle qualification**. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **POSIX EOF CORRECTION NATIVELY GREEN / WINDOWS STANDARD PIPE WITHOUT `-n` NATIVELY RED / OVERLAPPED-STDIN DIAGNOSTIC LOCALLY GREEN — 2026-07-15, Codex implementation owner** — exact correction head `4f57b885d24ad1755a5c863eaf37369208cecedb`, artifact run `29545688003`, and Linux x64/ARM64 jobs `87777401256`/`87777401218` prove the separate no-input facade through live SFTP and restricted no-tar POSIX system SSH with bounded full-size transfer, four-file concurrency, cancellation, and cleanup. Exact diagnostic head `da938a7405d0a6c760979358a6c0a38030d9acaf`, artifact run `29546702916`, and Windows x64/ARM64 jobs `87780486286`/`87780486259` prove that mandatory `-n` confounds inherited and overlapped handles. `E-M6-WINDOWS-NOINPUT-PIPE-NO-N-CI-RED-001` records exact pushed head `daffd545d75570f198db093c3ba054ae1fa033ba`, artifact run `29547488550`, Windows x64/ARM64 jobs `87782833321`/`87782833288`, and independent standard-pipe/no-`-n` timeouts after 8.03/8.04 seconds with `sentinel=false`, `stdoutEnded=true`, termination-only null exit/close codes, zero stderr bytes, and exact profile teardown RED. The same run keeps Linux x64/ARM64 full-size live SFTP and restricted POSIX system SSH green. `E-M6-WINDOWS-NOINPUT-OVERLAPPED-NO-N-LOCAL-001` replaces only the disconnected diagnostic with stdin-only `overlapped`, ordinary stdout/stderr, immediate parent destruction, and no `-n`; focused 117+2, 283-contract, format, typecheck, full lint/reliability/max-lines, diff, and protected-resolver gates pass locally. Commit/push and require fresh exact-head x64/ARM64 proof; do not change production or advance Milestone 6 without both native results. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: pre-execution Windows regular-file EOF qualification**. `E-M6-WINDOWS-NOINPUT-OVERLAPPED-NO-N-CI-RED-001` falsifies stdin-only overlapped EOF on exact pushed head `1c391aa851777f36cf81af9adb85d7177d7acff6`, artifact run `29548340927`, Windows x64 job `87785371486`, and Windows ARM64 job `87785371526`. `E-M6-WINDOWS-NOINPUT-REGULAR-FILE-NO-N-LOCAL-001` implements only a newly created zero-length regular-file descriptor that is already at EOF before `CreateProcess` and distinct from `NUL`; commit/push the isolated diagnostic and require exact native x64/ARM64 proof. Do not add a launcher unless this narrower disconnected boundary is also falsified. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **POSIX EOF CORRECTION NATIVELY GREEN / WINDOWS STANDARD AND OVERLAPPED PIPE EOF WITHOUT `-n` NATIVELY RED / REGULAR-FILE EOF DIAGNOSTIC LOCALLY GREEN — 2026-07-15, Codex implementation owner** — exact correction head `4f57b885d24ad1755a5c863eaf37369208cecedb`, artifact run `29545688003`, and Linux x64/ARM64 jobs `87777401256`/`87777401218` prove the separate no-input facade through live SFTP and restricted no-tar POSIX system SSH with bounded full-size transfer, four-file concurrency, cancellation, and cleanup. Exact diagnostic head `da938a7405d0a6c760979358a6c0a38030d9acaf`, artifact run `29546702916`, and Windows x64/ARM64 jobs `87780486286`/`87780486259` prove that mandatory `-n` confounds inherited and overlapped handles. `E-M6-WINDOWS-NOINPUT-PIPE-NO-N-CI-RED-001` records exact pushed head `daffd545d75570f198db093c3ba054ae1fa033ba`, artifact run `29547488550`, Windows x64/ARM64 jobs `87782833321`/`87782833288`, and independent standard-pipe/no-`-n` timeouts after 8.03/8.04 seconds with `sentinel=false`, `stdoutEnded=true`, termination-only null exit/close codes, zero stderr bytes, and exact profile teardown RED. `E-M6-WINDOWS-NOINPUT-OVERLAPPED-NO-N-CI-RED-001` records exact pushed head `1c391aa851777f36cf81af9adb85d7177d7acff6`, artifact run `29548340927`, Windows x64/ARM64 jobs `87785371486`/`87785371526`, and the same independent timeout shape after 8035.00/8043.47 milliseconds; both exact profile teardowns remain RED. All four non-Windows primary jobs and both Linux oldest-userland supplements remain green. `E-M6-WINDOWS-NOINPUT-REGULAR-FILE-NO-N-LOCAL-001` replaces only the disconnected diagnostic with a newly created zero-length regular-file descriptor already at EOF before `CreateProcess` and distinct from `NUL`; focused 117+2, all 283 release contracts, typecheck, full lint/reliability/max-lines, and formatting pass. Commit/push and require fresh x64/ARM64 proof; do not add a launcher unless this boundary is falsified. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -19557,6 +19557,70 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two ex require exact-head Windows x64 and ARM64 lifecycle plus exact profile/directory teardown before any production handle change. +### E-M6-WINDOWS-NOINPUT-OVERLAPPED-NO-N-CI-RED-001 — overlapped EOF after spawn fails without `-n` + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact pushed head + `1c391aa851777f36cf81af9adb85d7177d7acff6`, artifact run `29548340927`, Windows x64 job + `87785371486` on `windows-2022`, and Windows ARM64 job `87785371526` on `windows-11-arm`. +- Exact log command: `gh api repos/stablyai/orca/actions/jobs//logs | rg -n -C 15 +'ssh_windows_no_input_handle_diagnostic|Windows no-input handle diagnostic failed|profile +teardown:|UsrClass.dat|overlapped-stdin-eof-no-n|sentinel|stderrBytes'` for each job. +- Native result: x64 returns `success=false`, `timedOut=true`, `sentinel=false`, + `stdoutEnded=true`, `processExit=null`, `channelClosed=true`, `closeCode=null`, and + `stderrBytes=0` after 8035.00 milliseconds. ARM64 independently returns the same fields after + 8043.47 milliseconds. The exit/close fields are termination settlement after the timeout, not + remote-command success; both server logs show pinned-key authentication and command-session + close. +- Teardown result: both jobs fail `_Classes` hive unload, retain locked `UsrClass.dat`, and retain + the fixture profile directory. The workflow correctly stops before the unchanged product + full-size test, so no Windows transport cell is green and no tuple is enabled. +- Preserved evidence: the same run passes Darwin x64/ARM64 jobs `87785371494`/`87785371589`, Linux + x64/ARM64 jobs `87785371505`/`87785371622`, and Linux oldest-userland supplements + `87785824909`/`87785824893`. Both Linux primary jobs remain green through target-native build, + full-size cache, live SFTP, restricted no-tar POSIX system SSH, cancellation, cleanup, and + artifact upload. +- Interpretation/course correction: standard and overlapped Node pipes both expose their writer + until after `CreateProcess`, so neither proves the pre-execution EOF boundary. Before introducing + a native launcher, test a newly created zero-length regular-file descriptor as stdin: it is + already at EOF before `CreateProcess`, is distinct from `NUL`, and leaves ordinary stdout/stderr + capture unchanged. Keep the diagnostic disconnected and require both native architectures. +- Residual boundary: keep product handles/args, legacy default/fallback, Beta, tuple publication, + and SignPath unchanged. + +### E-M6-WINDOWS-NOINPUT-REGULAR-FILE-NO-N-LOCAL-001 — pre-execution regular-file EOF diagnostic is locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted replacement atop exact + overlapped RED head `1c391aa851777f36cf81af9adb85d7177d7acff6` on Apple arm64, macOS 26.2 + build 25C56, Node 26.5.0, pnpm 10.24.0. +- Diagnostic boundary: the disconnected Windows live test creates an exclusive zero-length regular + file, passes its already-at-EOF descriptor as child stdin before `CreateProcess`, closes the + parent descriptor after spawn, keeps stdout/stderr as ordinary pipes, and omits OpenSSH `-n`. + The file is distinct from `NUL`; bounded lifecycle fields, 8-second timeout, 10-second hard + settlement, explicit stdin-fixture removal evidence, fail-closed workflow order, production + args/handles, and the product full-size test remain unchanged. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two declared native + skips in 5.52 seconds real; maximum RSS is 153,796,608 bytes and peak footprint is 97,949,576 + bytes. +- Release contracts: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 --reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 50 files and + 283/283 cases in 26.00 seconds real; maximum RSS is 195,379,200 bytes and peak footprint is + 97,818,408 bytes. +- Static gates: `/usr/bin/time -l pnpm typecheck` passes in 7.28 seconds real with maximum RSS + 1,211,891,712 bytes and peak footprint 98,342,720 bytes. `/usr/bin/time -l pnpm lint` passes in + 27.13 seconds real with only the same 26 pre-existing warnings, all 41 reliability gates, and the + 355-entry max-lines ratchet; maximum RSS is 2,045,526,016 bytes and peak footprint 97,408,808 + bytes. Purpose-file formatting passes after `oxfmt`. +- Isolation gates: purpose-file formatting, `git diff --check`, and protected-resolver zero-diff + pass. The diagnostic additionally reports and requires removal of its exclusive empty-stdin + fixture on a successful native result. +- Residual boundary: local macOS skips both native live cases. Commit/push and require exact-head + Windows x64 and ARM64 lifecycle plus exact profile/directory teardown before any production + handle change. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -19615,8 +19679,8 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push the stdin-only overlapped/no-`-n` disconnected diagnostic recorded by -`E-M6-WINDOWS-NOINPUT-OVERLAPPED-NO-N-LOCAL-001`, then require exact-head Windows x64 and ARM64 +Commit and push only the disconnected zero-length regular-file stdin diagnostic recorded by +`E-M6-WINDOWS-NOINPUT-REGULAR-FILE-NO-N-LOCAL-001`, then require exact-head Windows x64 and ARM64 lifecycle plus exact profile/directory teardown. Do not change the production Windows handle or check the broad Milestone 6 Windows system-SSH item until both architectures select the same console-independent strategy. Do not add an diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 8682852dda5..73429e3ed86 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1651,11 +1651,15 @@

    Prove production behavior, failure behavior, and rollback behavior.

    simultaneously forced through -n, so those runs do not isolate the child handle. Native x64/arm64 evidence further rejects the upstream-recommended standard-pipe EOF when Windows -n is omitted because Node closes - its parent writer only after CreateProcess. Qualify an overlapped - stdin pipe with ordinary stdout/stderr and no -n as the next - disconnected diagnostic. POSIX retains ignored stdin plus -n; - fixture-owned Windows session processes settle before profile hives are - boundedly unloaded and native profile deletion begins; + its parent writer only after CreateProcess. Exact native x64/arm64 + evidence also rejects stdin-only overlapped EOF with ordinary stdout/stderr and + no -n. Before adding a launcher, qualify a newly created + zero-length regular-file descriptor as Windows stdin: it is already at EOF + before CreateProcess, is not NUL, and leaves + stdout/stderr unchanged. Keep this disconnected until both architectures pass + full live transfer and exact teardown. POSIX retains ignored stdin plus + -n; fixture-owned Windows session processes settle before profile + hives are boundedly unloaded and native profile deletion begins; MaxSessions=1; interrupted upload; stale lock diff --git a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts index 9bb61af98bc..cc903f935a3 100644 --- a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +++ b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts @@ -1,4 +1,7 @@ import { spawn } from 'node:child_process' +import { closeSync, mkdtempSync, openSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import { performance } from 'node:perf_hooks' import { describe, expect, it } from 'vitest' @@ -7,7 +10,7 @@ import type { SshTarget } from '../../shared/ssh-types' import { buildSshArgs } from './system-ssh-args' import { findSystemSsh } from './system-ssh-binary' -type DiagnosticMode = 'overlapped-stdin-eof-no-n' +type DiagnosticMode = 'regular-file-eof-no-n' type DiagnosticResult = { mode: DiagnosticMode @@ -19,6 +22,7 @@ type DiagnosticResult = { channelClosed: boolean closeCode: number | null | 'not-observed' stderrBytes: number + stdinFixtureRemoved: boolean elapsedMs: number } @@ -47,15 +51,25 @@ async function runDiagnostic(): Promise { if (!sshPath) { throw new Error('Native Windows OpenSSH client is unavailable') } - const mode: DiagnosticMode = 'overlapped-stdin-eof-no-n' + const mode: DiagnosticMode = 'regular-file-eof-no-n' const startedAt = performance.now() - const child = spawn(sshPath, [...buildSshArgs(createTarget()), 'echo ORCA-SYSTEM-SSH-OK'], { - // Why: isolate Windows overlapped stdin without changing output capture - // or the production no-input command. - stdio: ['overlapped', 'pipe', 'pipe'], - windowsHide: true - }) - child.stdin?.destroy() + const stdinDirectory = mkdtempSync(join(tmpdir(), 'orca-ssh-empty-stdin-')) + const stdinPath = join(stdinDirectory, 'stdin') + const stdinFd = openSync(stdinPath, 'wx+') + let child: ReturnType + try { + child = spawn(sshPath, [...buildSshArgs(createTarget()), 'echo ORCA-SYSTEM-SSH-OK'], { + // Why: a zero-length regular file is at EOF before CreateProcess without + // triggering Win32-OpenSSH's NUL-handle bug. + stdio: [stdinFd, 'pipe', 'pipe'], + windowsHide: true + }) + } catch (error) { + closeSync(stdinFd) + rmSync(stdinDirectory, { recursive: true, force: true }) + throw error + } + closeSync(stdinFd) return new Promise((resolve, reject) => { let stdout = '' @@ -66,6 +80,14 @@ async function runDiagnostic(): Promise { let closeCode: number | null | 'not-observed' = 'not-observed' let timedOut = false let settled = false + const removeStdinFixture = (): boolean => { + try { + rmSync(stdinDirectory, { recursive: true, force: true }) + return true + } catch { + return false + } + } const finish = (): void => { if (settled) { return @@ -73,6 +95,7 @@ async function runDiagnostic(): Promise { settled = true clearTimeout(timeout) clearTimeout(killTimeout) + const stdinFixtureRemoved = removeStdinFixture() const sentinel = stdout.includes('ORCA-SYSTEM-SSH-OK') resolve({ mode, @@ -82,7 +105,8 @@ async function runDiagnostic(): Promise { stdoutEnded && processExit === 0 && channelClosed && - closeCode === 0, + closeCode === 0 && + stdinFixtureRemoved, timedOut, sentinel, stdoutEnded, @@ -90,6 +114,7 @@ async function runDiagnostic(): Promise { channelClosed, closeCode, stderrBytes, + stdinFixtureRemoved, elapsedMs: performance.now() - startedAt }) } @@ -124,8 +149,13 @@ async function runDiagnostic(): Promise { finish() }) child.on('error', (error) => { + if (settled) { + return + } + settled = true clearTimeout(timeout) clearTimeout(killTimeout) + removeStdinFixture() reject(error) }) }) @@ -133,14 +163,14 @@ async function runDiagnostic(): Promise { describe.skipIf(!hasLiveInput)('Windows OpenSSH no-input child-handle diagnostic', () => { it( - 'qualifies overlapped stdin EOF without the Windows null-input option', + 'qualifies regular-file stdin EOF without the Windows null-input option', { timeout: 15_000 }, async () => { expect(process.platform).toBe('win32') - const pipeEofNoN = await runDiagnostic() - console.log(`ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ pipeEofNoN })}`) + const regularFileEofNoN = await runDiagnostic() + console.log(`ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ regularFileEofNoN })}`) - expect(pipeEofNoN.success).toBe(true) + expect(regularFileEofNoN.success).toBe(true) } ) }) From 1f220a7135b83ef4c5f035ab729c114e828bf5b3 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 22:43:20 -0400 Subject: [PATCH 253/267] test(ssh): add disconnected Windows no-input launcher Co-authored-by: Orca --- .../workflows/ssh-relay-runtime-artifacts.yml | 27 ++ .../build-windows-ssh-no-input-launcher.mjs | 78 ++++ ...runtime-windows-no-input-launcher.test.mjs | 239 +++++++++++ ...time-windows-openssh-workflow-contract.mjs | 10 + .../ssh-relay-runtime-workflow.test.mjs | 6 + ...elease-implementation-checklist-summary.md | 18 +- ...github-release-implementation-checklist.md | 93 +++- ...6-07-14-ssh-relay-github-release-plan.html | 20 +- .../WindowsLauncherHandleProbe.cs | 249 +++++++++++ .../OrcaSshNoInputLauncher.cs | 32 ++ .../WindowsAnonymousPipeSet.cs | 252 +++++++++++ .../WindowsCommandLine.cs | 56 +++ .../WindowsSshChildProcess.cs | 396 ++++++++++++++++++ ...sh-system-no-input-windows-openssh.test.ts | 59 +-- 14 files changed, 1479 insertions(+), 56 deletions(-) create mode 100644 config/scripts/build-windows-ssh-no-input-launcher.mjs create mode 100644 config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs create mode 100644 native/windows-ssh-no-input-launcher-test/WindowsLauncherHandleProbe.cs create mode 100644 native/windows-ssh-no-input-launcher/OrcaSshNoInputLauncher.cs create mode 100644 native/windows-ssh-no-input-launcher/WindowsAnonymousPipeSet.cs create mode 100644 native/windows-ssh-no-input-launcher/WindowsCommandLine.cs create mode 100644 native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs diff --git a/.github/workflows/ssh-relay-runtime-artifacts.yml b/.github/workflows/ssh-relay-runtime-artifacts.yml index 712715a56f5..dcf00a516f9 100644 --- a/.github/workflows/ssh-relay-runtime-artifacts.yml +++ b/.github/workflows/ssh-relay-runtime-artifacts.yml @@ -7,6 +7,7 @@ on: - 'config/patches/node-pty@1.1.0.patch' - 'config/ssh-relay-runtime-linux-builder.Containerfile' - 'config/scripts/build-relay.mjs' + - 'config/scripts/build-windows-ssh-no-input-launcher.mjs' - 'config/scripts/*ssh-relay*' - 'config/ssh-relay-node-release-keys/**' - 'config/ssh-relay-node-release-v24.18.0.json' @@ -14,7 +15,10 @@ on: - 'pnpm-lock.yaml' - 'src/main/ipc/parcel-watcher-process-entry.ts' - 'src/main/ssh/ssh-relay-runtime-*' + - 'src/main/ssh/ssh-system-no-input-windows-openssh.test.ts' - 'src/relay/**' + - 'native/windows-ssh-no-input-launcher/**' + - 'native/windows-ssh-no-input-launcher-test/**' workflow_dispatch: # Why: callers may gate on the native graph only after separate release wiring is reviewed. workflow_call: @@ -258,6 +262,8 @@ jobs: 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 + node --check config/scripts/build-windows-ssh-no-input-launcher.mjs + node --check config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs pnpm exec vitest run --config config/vitest.config.ts \ src/main/ssh/ssh-relay-artifact-schema.test.ts \ src/main/ssh/ssh-relay-manifest-signature.test.ts \ @@ -348,6 +354,7 @@ jobs: 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 \ + config/scripts/ssh-relay-runtime-windows-no-input-launcher.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 \ @@ -1000,6 +1007,10 @@ jobs: if ($LASTEXITCODE -ne 0) { throw 'runtime native signing rehearsal workflow test syntax check failed' } node --check config/scripts/ssh-relay-runtime-build-prerequisite.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime build prerequisite test syntax check failed' } + node --check config/scripts/build-windows-ssh-no-input-launcher.mjs + if ($LASTEXITCODE -ne 0) { throw 'Windows no-input launcher build syntax check failed' } + node --check config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs + if ($LASTEXITCODE -ne 0) { throw 'Windows no-input launcher test syntax check failed' } pnpm exec vitest run --config config/vitest.config.ts ` src/main/ssh/ssh-relay-artifact-schema.test.ts ` src/main/ssh/ssh-relay-manifest-signature.test.ts ` @@ -1090,6 +1101,7 @@ jobs: 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 ` + config/scripts/ssh-relay-runtime-windows-no-input-launcher.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 ` @@ -1103,6 +1115,21 @@ jobs: config/scripts/ssh-relay-runtime-workflow.test.mjs if ($LASTEXITCODE -ne 0) { throw 'runtime artifact contract tests failed' } + - name: Build disconnected Windows no-input launcher diagnostic + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $launcherRoot = Join-Path $env:RUNNER_TEMP 'orca-ssh-no-input-launcher' + $launcherPath = Join-Path $launcherRoot 'orca-ssh-no-input.exe' + node config/scripts/build-windows-ssh-no-input-launcher.mjs --output $launcherPath + if ($LASTEXITCODE -ne 0) { throw 'Windows no-input launcher build failed' } + if (-not (Test-Path -LiteralPath $launcherPath -PathType Leaf)) { + throw 'Windows no-input launcher output is missing' + } + # Why: this artifact is live-test input only; it is not part of runtime or release output. + "ORCA_SSH_WINDOWS_NO_INPUT_LAUNCHER=$launcherPath" | + Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + - name: Download exact Node release inputs shell: pwsh run: | diff --git a/config/scripts/build-windows-ssh-no-input-launcher.mjs b/config/scripts/build-windows-ssh-no-input-launcher.mjs new file mode 100644 index 00000000000..aa0e76d3af2 --- /dev/null +++ b/config/scripts/build-windows-ssh-no-input-launcher.mjs @@ -0,0 +1,78 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process' +import { existsSync, mkdirSync } from 'node:fs' +import { dirname, join, resolve } from 'node:path' + +if (process.platform !== 'win32') { + throw new Error('Windows SSH no-input launcher compilation requires a Windows host.') +} + +const repoRoot = resolve(import.meta.dirname, '../..') +const sourceRoot = join(repoRoot, 'native', 'windows-ssh-no-input-launcher') +const sourcePaths = [ + 'OrcaSshNoInputLauncher.cs', + 'WindowsAnonymousPipeSet.cs', + 'WindowsCommandLine.cs', + 'WindowsSshChildProcess.cs' +].map((name) => join(sourceRoot, name)) +const outputPath = readArg('--output') ?? join(sourceRoot, '.build', 'orca-ssh-no-input.exe') +const compilerPath = findFrameworkCompiler(process.env) + +if (!compilerPath) { + throw new Error('Unable to find the .NET Framework C# compiler for the SSH no-input launcher.') +} +for (const sourcePath of sourcePaths) { + if (!existsSync(sourcePath)) { + throw new Error(`Missing Windows SSH no-input launcher source: ${sourcePath}`) + } +} + +mkdirSync(dirname(outputPath), { recursive: true }) +const result = spawnSync( + compilerPath, + [ + '/nologo', + '/target:exe', + '/platform:anycpu', + '/optimize+', + '/warnaserror+', + `/out:${outputPath}`, + ...sourcePaths + ], + { cwd: repoRoot, stdio: 'inherit' } +) + +if (result.signal) { + process.kill(process.pid, result.signal) +} +if (result.error) { + throw result.error +} +if (result.status !== 0) { + process.exit(result.status ?? 1) +} + +function findFrameworkCompiler(env) { + const windowsDirectory = env.WINDIR ?? env.SystemRoot + if (!windowsDirectory) { + return null + } + const candidates = [ + join(windowsDirectory, 'Microsoft.NET', 'Framework64', 'v4.0.30319', 'csc.exe'), + join(windowsDirectory, 'Microsoft.NET', 'Framework', 'v4.0.30319', 'csc.exe') + ] + return candidates.find((candidate) => existsSync(candidate)) ?? null +} + +function readArg(name) { + const index = process.argv.indexOf(name) + if (index < 0) { + return undefined + } + const value = process.argv[index + 1] + if (!value || value.startsWith('--')) { + throw new Error(`${name} requires a path.`) + } + return resolve(value) +} diff --git a/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs b/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs new file mode 100644 index 00000000000..4a6006c4437 --- /dev/null +++ b/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs @@ -0,0 +1,239 @@ +import { spawn, spawnSync } from 'node:child_process' +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { performance } from 'node:perf_hooks' + +import { afterAll, beforeAll, describe, expect, it } from 'vitest' + +const projectRoot = resolve(import.meta.dirname, '../..') +const sourceRoot = join(projectRoot, 'native', 'windows-ssh-no-input-launcher') +const buildScript = join( + projectRoot, + 'config', + 'scripts', + 'build-windows-ssh-no-input-launcher.mjs' +) +const itWindows = process.platform === 'win32' ? it : it.skip +let fixtureRoot +let launcherPath +let childFixturePath +let handleProbePath + +beforeAll(() => { + if (process.platform !== 'win32') { + return + } + fixtureRoot = mkdtempSync(join(tmpdir(), 'orca ssh no-input launcher ')) + launcherPath = join(fixtureRoot, 'orca-ssh-no-input.exe') + childFixturePath = join(fixtureRoot, 'child-fixture.cjs') + handleProbePath = join(fixtureRoot, 'windows-launcher-handle-probe.exe') + writeFileSync(childFixturePath, childFixtureSource, 'utf8') + const build = spawnSync(process.execPath, [buildScript, '--output', launcherPath], { + cwd: projectRoot, + encoding: 'utf8' + }) + expect(build.status, `${build.stdout}\n${build.stderr}`).toBe(0) + + const compilerPath = findFrameworkCompiler(process.env) + expect(compilerPath).not.toBeNull() + const probeSource = join( + projectRoot, + 'native', + 'windows-ssh-no-input-launcher-test', + 'WindowsLauncherHandleProbe.cs' + ) + const probeBuild = spawnSync( + compilerPath, + [ + '/nologo', + '/target:exe', + '/platform:anycpu', + '/optimize+', + '/warnaserror+', + `/out:${handleProbePath}`, + probeSource + ], + { cwd: projectRoot, encoding: 'utf8' } + ) + expect(probeBuild.status, `${probeBuild.stdout}\n${probeBuild.stderr}`).toBe(0) +}, 20_000) + +afterAll(() => { + if (fixtureRoot) { + rmSync(fixtureRoot, { recursive: true, force: true }) + } +}) + +describe('Windows SSH no-input launcher', () => { + it('keeps the artifact source scoped to the reviewed Win32 boundary', () => { + const processSource = readFileSync(join(sourceRoot, 'WindowsSshChildProcess.cs'), 'utf8') + const pipeSource = readFileSync(join(sourceRoot, 'WindowsAnonymousPipeSet.cs'), 'utf8') + + expect(processSource).toContain('ProcThreadAttributeHandleList') + expect(processSource).toContain('IntPtr.Size * 3') + expect(processSource).toContain('CreateSuspended') + expect(processSource).toContain('CreateNoWindow') + expect(processSource).toContain('JobObjectLimitKillOnJobClose') + expect(processSource).toContain('WaitForMultipleObjects') + expect(processSource).toContain('if (processStarted && !assignedToJob)') + expect(pipeSource).toContain('CloseHandleChecked(stdinWrite)') + expect(pipeSource).toContain('PumpCompletionTimeoutMilliseconds') + expect(pipeSource).toContain('pumpFailureSignal.Set()') + }) + + it.skipIf(process.platform === 'win32')( + 'fails closed when compilation is attempted away from Windows', + () => { + const result = spawnSync(process.execPath, [buildScript], { + cwd: projectRoot, + encoding: 'utf8' + }) + + expect(result.status).not.toBe(0) + expect(result.stderr).toContain('requires a Windows host') + } + ) + + itWindows('preserves exact arguments and provides child stdin EOF before execution', () => { + const args = ['', 'space value', 'quote"value', 'trailing slash\\', 'line one\nline two', '雪'] + const result = runLauncher('arguments-and-stdin', args, { + input: Buffer.from('this input must not reach the SSH child', 'utf8') + }) + + expect(result.status, result.stderr.toString('utf8')).toBe(0) + expect(JSON.parse(result.stdout.toString('utf8'))).toEqual({ args, stdinBytes: 0 }) + }) + + itWindows('preserves binary stdout and stderr and propagates the child exit code', () => { + const binary = runLauncher('binary-output') + expect(binary.status).toBe(0) + expect(binary.stdout).toEqual(Buffer.from([0x00, 0xff, 0x10, 0x0a, 0x80])) + expect(binary.stderr).toEqual(Buffer.from([0xfe, 0x00, 0x7f, 0x0d, 0x0a])) + + const exited = runLauncher('exit-code', ['37']) + expect(exited.status).toBe(37) + }) + + itWindows('does not leak an unrelated inheritable parent handle into the child', () => { + const result = spawnSync(handleProbePath, [launcherPath], { + cwd: fixtureRoot, + encoding: 'utf8', + timeout: 15_000, + windowsHide: true + }) + + expect(result.status, result.stderr).toBe(0) + expect(result.stdout).toContain('ORCA-NO-INHERITED-HANDLE-LEAK') + }) + + itWindows( + 'kills the child job and settles boundedly when the launcher is cancelled', + { timeout: 15_000 }, + async () => { + const startedAt = performance.now() + const child = spawn(launcherPath, [process.execPath, childFixturePath, 'hold'], { + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true + }) + const childPid = await readFirstLine(child.stdout, 5_000) + expect(Number.isInteger(childPid)).toBe(true) + + const closePromise = waitForClose(child, 5_000) + expect(child.kill('SIGKILL')).toBe(true) + await closePromise + await waitForProcessExit(childPid, 5_000) + expect(performance.now() - startedAt).toBeLessThan(10_000) + } + ) +}) + +function runLauncher(mode, args = [], options = {}) { + return spawnSync(launcherPath, [process.execPath, childFixturePath, mode, ...args], { + cwd: fixtureRoot, + encoding: null, + timeout: 10_000, + windowsHide: true, + ...options + }) +} + +function findFrameworkCompiler(env) { + const windowsDirectory = env.WINDIR ?? env.SystemRoot + if (!windowsDirectory) { + return null + } + return ( + [ + join(windowsDirectory, 'Microsoft.NET', 'Framework64', 'v4.0.30319', 'csc.exe'), + join(windowsDirectory, 'Microsoft.NET', 'Framework', 'v4.0.30319', 'csc.exe') + ].find((candidate) => existsSync(candidate)) ?? null + ) +} + +function readFirstLine(stream, timeoutMs) { + return new Promise((resolveLine, reject) => { + let output = '' + const timeout = setTimeout( + () => reject(new Error('Timed out waiting for child PID.')), + timeoutMs + ) + stream.on('data', (chunk) => { + output += chunk.toString('utf8') + const newline = output.indexOf('\n') + if (newline >= 0) { + clearTimeout(timeout) + resolveLine(Number.parseInt(output.slice(0, newline), 10)) + } + }) + stream.on('error', reject) + }) +} + +function waitForClose(child, timeoutMs) { + return new Promise((resolveClose, reject) => { + const timeout = setTimeout( + () => reject(new Error('Launcher cancellation did not settle.')), + timeoutMs + ) + child.once('close', () => { + clearTimeout(timeout) + resolveClose() + }) + child.once('error', reject) + }) +} + +async function waitForProcessExit(pid, timeoutMs) { + const deadline = performance.now() + timeoutMs + while (performance.now() < deadline) { + try { + process.kill(pid, 0) + } catch { + return + } + await new Promise((resolveWait) => setTimeout(resolveWait, 25)) + } + throw new Error(`Launcher child ${pid} survived job-owned cancellation.`) +} + +const childFixtureSource = String.raw`const mode = process.argv[2] +if (mode === 'arguments-and-stdin') { + let stdinBytes = 0 + process.stdin.on('data', (chunk) => { stdinBytes += chunk.length }) + process.stdin.on('end', () => { + process.stdout.write(JSON.stringify({ args: process.argv.slice(3), stdinBytes })) + }) + process.stdin.resume() +} else if (mode === 'binary-output') { + process.stdout.write(Buffer.from([0x00, 0xff, 0x10, 0x0a, 0x80])) + process.stderr.write(Buffer.from([0xfe, 0x00, 0x7f, 0x0d, 0x0a])) +} else if (mode === 'exit-code') { + process.exit(Number.parseInt(process.argv[3], 10)) +} else if (mode === 'hold') { + process.stdout.write(String(process.pid) + '\n') + setInterval(() => {}, 1000) +} else { + process.exit(99) +} +` diff --git a/config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs b/config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs index 5e749ed9cbc..6ac02c3b88c 100644 --- a/config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs +++ b/config/scripts/ssh-relay-runtime-windows-openssh-workflow-contract.mjs @@ -3,6 +3,9 @@ export function assertWindowsOpenSshWorkflow(workflow, expect) { const start = steps.find( (step) => step.name === 'Start loopback Windows OpenSSH system-SSH fixture' ) + const launcher = steps.find( + (step) => step.name === 'Build disconnected Windows no-input launcher diagnostic' + ) const measure = steps.find( (step) => step.name === 'Measure exact full-size runtime over Windows system SSH' ) @@ -11,8 +14,15 @@ export function assertWindowsOpenSshWorkflow(workflow, expect) { ) expect(start).toBeDefined() + expect(launcher).toBeDefined() expect(measure).toBeDefined() expect(stop).toBeDefined() + expect(launcher.shell).toBe('pwsh') + expect(launcher.run).toContain('build-windows-ssh-no-input-launcher.mjs') + expect(launcher.run).toContain('ORCA_SSH_WINDOWS_NO_INPUT_LAUNCHER=') + expect(launcher.run).toContain('$env:RUNNER_TEMP') + expect(launcher.run).not.toMatch(/runtime-evidence|firstOutput|Upload|SignPath/iu) + expect(steps.indexOf(launcher)).toBeLessThan(steps.indexOf(start)) expect(start.shell).toBe('pwsh') expect(start.run).toContain('10.0.0.0p2-Preview') expect(start.run).toContain('23f50f3458c4c5d0b12217c6a5ddfde0137210a30fa870e98b29827f7b43aba5') diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index d7f964b688a..e31cd933ff1 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -178,6 +178,12 @@ describe('SSH relay runtime artifact workflow', () => { expect(windowsUploadIndex).toBeGreaterThan(windowsMeasurementIndex) expect(source).toContain('verify-ssh-relay-runtime.mjs') expect(source).toContain('ssh-relay-runtime-workflow.test.mjs') + expect( + source.match(/node --check config\/scripts\/build-windows-ssh-no-input-launcher\.mjs/g) + ).toHaveLength(2) + expect( + source.match(/config\/scripts\/ssh-relay-runtime-windows-no-input-launcher\.test\.mjs/g) + ).toHaveLength(4) for (const testName of [ 'ssh-relay-artifact-schema.test.ts', 'ssh-relay-manifest-signature.test.ts', 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 bc720b87bd2..f1402b66877 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 @@ -9,8 +9,8 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — pre-execution Windows -regular-file EOF qualification, 2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected Windows +no-input launcher native qualification, 2026-07-15, Codex implementation owner.** `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain @@ -151,7 +151,19 @@ architectures to pass the diagnostic, full-size live transfer, cancellation, reu profile teardown before changing production. `E-M6-WINDOWS-NOINPUT-REGULAR-FILE-NO-N-LOCAL-001` records the disconnected replacement locally green: focused 117+2, all 283 release contracts, typecheck, full lint/reliability/max-lines, and formatting pass. Commit/push this exact isolated -diagnostic and require fresh native x64/ARM64 proof; SignPath remains deferred. +diagnostic and require fresh native x64/ARM64 proof. `E-M6-WINDOWS-NOINPUT-REGULAR-FILE-NO-N-CI-RED-001` +records exact pushed head `eee686d7f6203c88f00d607711d3816aaf444c61`, artifact run +`29549252407`, Windows x64/ARM64 jobs `87788106340`/`87788106351`, and independent +8036.46/8042.17-millisecond timeouts despite `stdinFixtureRemoved=true`; both profile teardowns are +RED. All four non-Windows primary jobs and both Linux supplements remain green. Next implement only +a disconnected Windows launcher artifact with private anonymous stdio pipes, exact handle +inheritance, hidden child creation, bounded output pumps, exit propagation, and job-owned +cancellation. `E-M6-WINDOWS-NOINPUT-LAUNCHER-LOCAL-001` records that artifact-only package locally +green: focused 119/119, release contracts 285/285, typecheck, full lint/reliability/max-lines, +formatting, diff, and resolver isolation pass. Its Windows-native suite requires legacy-compiler, +argv/EOF/binary/exit, unrelated-handle exclusion, job cancellation, and bounded settlement proof; +all four native cases are declared skips locally. Commit/push this exact package and require x64/ +ARM64 live evidence next. Do not package or call it from production; SignPath remains deferred. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown 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 eb7d27ebbbc..4128db018ef 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: pre-execution Windows regular-file EOF qualification**. `E-M6-WINDOWS-NOINPUT-OVERLAPPED-NO-N-CI-RED-001` falsifies stdin-only overlapped EOF on exact pushed head `1c391aa851777f36cf81af9adb85d7177d7acff6`, artifact run `29548340927`, Windows x64 job `87785371486`, and Windows ARM64 job `87785371526`. `E-M6-WINDOWS-NOINPUT-REGULAR-FILE-NO-N-LOCAL-001` implements only a newly created zero-length regular-file descriptor that is already at EOF before `CreateProcess` and distinct from `NUL`; commit/push the isolated diagnostic and require exact native x64/ARM64 proof. Do not add a launcher unless this narrower disconnected boundary is also falsified. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **POSIX EOF CORRECTION NATIVELY GREEN / WINDOWS STANDARD AND OVERLAPPED PIPE EOF WITHOUT `-n` NATIVELY RED / REGULAR-FILE EOF DIAGNOSTIC LOCALLY GREEN — 2026-07-15, Codex implementation owner** — exact correction head `4f57b885d24ad1755a5c863eaf37369208cecedb`, artifact run `29545688003`, and Linux x64/ARM64 jobs `87777401256`/`87777401218` prove the separate no-input facade through live SFTP and restricted no-tar POSIX system SSH with bounded full-size transfer, four-file concurrency, cancellation, and cleanup. Exact diagnostic head `da938a7405d0a6c760979358a6c0a38030d9acaf`, artifact run `29546702916`, and Windows x64/ARM64 jobs `87780486286`/`87780486259` prove that mandatory `-n` confounds inherited and overlapped handles. `E-M6-WINDOWS-NOINPUT-PIPE-NO-N-CI-RED-001` records exact pushed head `daffd545d75570f198db093c3ba054ae1fa033ba`, artifact run `29547488550`, Windows x64/ARM64 jobs `87782833321`/`87782833288`, and independent standard-pipe/no-`-n` timeouts after 8.03/8.04 seconds with `sentinel=false`, `stdoutEnded=true`, termination-only null exit/close codes, zero stderr bytes, and exact profile teardown RED. `E-M6-WINDOWS-NOINPUT-OVERLAPPED-NO-N-CI-RED-001` records exact pushed head `1c391aa851777f36cf81af9adb85d7177d7acff6`, artifact run `29548340927`, Windows x64/ARM64 jobs `87785371486`/`87785371526`, and the same independent timeout shape after 8035.00/8043.47 milliseconds; both exact profile teardowns remain RED. All four non-Windows primary jobs and both Linux oldest-userland supplements remain green. `E-M6-WINDOWS-NOINPUT-REGULAR-FILE-NO-N-LOCAL-001` replaces only the disconnected diagnostic with a newly created zero-length regular-file descriptor already at EOF before `CreateProcess` and distinct from `NUL`; focused 117+2, all 283 release contracts, typecheck, full lint/reliability/max-lines, and formatting pass. Commit/push and require fresh x64/ARM64 proof; do not add a launcher unless this boundary is falsified. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected Windows no-input launcher native qualification**. `E-M6-WINDOWS-NOINPUT-LAUNCHER-LOCAL-001` records the unpackaged, unsigned launcher and its local source/workflow/static contracts green. Commit/push only this exact artifact-only package and require native Windows x64/ARM64 legacy-compiler, argv/EOF/binary/exit/handle-exclusion/cancellation, live diagnostic, and exact teardown proof before any product caller. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    +Session checkpoint: **DIRECT WINDOWS NODE STDIN BOUNDARIES NATIVELY RED / DISCONNECTED PRIVATE-PIPE LAUNCHER LOCALLY GREEN / NATIVE QUALIFICATION NEXT — 2026-07-15, Codex implementation owner** — `E-M6-WINDOWS-NOINPUT-REGULAR-FILE-NO-N-CI-RED-001` records exact head `eee686d7f6203c88f00d607711d3816aaf444c61`, artifact run `29549252407`, Windows x64/ARM64 jobs `87788106340`/`87788106351`, and independent 8036.46/8042.17-millisecond timeouts after the last direct-Node boundary. `E-M6-WINDOWS-NOINPUT-LAUNCHER-LOCAL-001` adds only a runner-temp launcher with private anonymous stdio pipes, pre-execution stdin EOF, exact three-handle inheritance, suspended/job-owned hidden child creation, pump-failure cancellation, bounded pump settlement, and exit propagation. Focused 119/119, release contracts 285/285, typecheck, full lint/reliability/max-lines, formatting, diff, and resolver isolation pass locally; four launcher-native and two live Windows cases are declared skips. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, 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) @@ -19621,6 +19621,88 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two de Windows x64 and ARM64 lifecycle plus exact profile/directory teardown before any production handle change. +### E-M6-WINDOWS-NOINPUT-REGULAR-FILE-NO-N-CI-RED-001 — pre-execution regular-file EOF still fails natively + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact pushed head + `eee686d7f6203c88f00d607711d3816aaf444c61`, artifact run `29549252407`, Windows x64 job + `87788106340` on `windows-2022`, and Windows ARM64 job `87788106351` on `windows-11-arm`. +- Exact log command: `gh api repos/stablyai/orca/actions/jobs//logs | rg -n -C 15 + 'ssh_windows_no_input_handle_diagnostic|Windows no-input handle diagnostic failed|profile + teardown:|UsrClass.dat|regular-file-eof-no-n|stdinFixtureRemoved|sentinel|stderrBytes'` for each + job. +- Native result: x64 returns `success=false`, `timedOut=true`, `sentinel=false`, + `stdoutEnded=true`, `processExit=null`, `channelClosed=true`, `closeCode=null`, `stderrBytes=0`, + and `stdinFixtureRemoved=true` after 8036.46 milliseconds. ARM64 independently returns the same + fields after 8042.17 milliseconds. The exit/close fields are termination settlement after the + timeout, not remote-command success; both server logs show pinned-key authentication and command- + session close. +- Teardown result: both jobs fail `_Classes` hive unload, retain locked `UsrClass.dat`, and retain + the fixture profile directory. The workflow correctly stops before the unchanged product + full-size test, so no Windows transport cell is green and no tuple is enabled. +- Preserved evidence: the same run passes Linux x64/ARM64 jobs `87788106335`/`87788106349`, Darwin + x64/ARM64 jobs `87788106347`/`87788106337`, and Linux oldest-userland supplements + `87788703378`/`87788703379`. Both Linux primary jobs remain green through target-native build, + full-size cache, live SFTP, restricted no-tar POSIX system SSH, cancellation, cleanup, and + artifact upload. +- Interpretation/course correction: a regular file proves EOF existed before `CreateProcess`, so + another direct Node stdin mode is not justified. Orca-managed ControlMaster is already absent on + Windows because `getControlSocketPath()` returns `null`; this is not the 300-second mux-persist + path. The next capability must isolate the SSH child from Node's Windows named-pipe handles with + private anonymous stdin/stdout/stderr pipes, an exact inherited-handle list, and bounded output + pumps, while retaining hidden execution, exit propagation, and cancellation-owned child lifetime. +- Residual boundary: implement and native-test only an unpackaged, unsigned, disconnected launcher + artifact. Keep product handles/args, legacy default/fallback, Beta, tuple publication, and + SignPath unchanged. + +### E-M6-WINDOWS-NOINPUT-LAUNCHER-LOCAL-001 — disconnected launcher contracts are locally green + +- Date/owner/base/runner: 2026-07-15, Codex implementation owner; uncommitted capability atop exact + regular-file RED head `eee686d7f6203c88f00d607711d3816aaf444c61` on Apple arm64, macOS 26.2 + build 25C56, Node 26.5.0, and pnpm 10.24.0. +- Artifact boundary: four purpose-named C# sources create private anonymous stdin/stdout/stderr + pipes, close the stdin writer before `CreateProcessW`, use `STARTUPINFOEX` with exactly three + inherited handles, start the hidden child suspended, assign it to a kill-on-close job before + resume, signal pump failure into the child wait, bound both pump joins to five seconds, propagate + the child exit code, and terminate any started child that failed job assignment. The build uses + the Windows .NET Framework 4 `csc.exe` and emits only to an explicit runner-temp path. +- Native contract design: the purpose-named suite compiles the launcher and a separate inheritable- + event probe on Windows, then requires exact empty/space/quote/trailing-slash/newline/Unicode argv, + zero child stdin bytes despite input supplied to the launcher, exact binary stdout/stderr, + exit-code 37 propagation, exclusion of an unrelated inheritable event, job-owned child death, + and cancellation settlement under ten seconds. On macOS the two source/fail-closed cases pass and + the four Windows-native cases are declared skips; native x64/ARM64 proof remains required. +- Workflow boundary: `.github/workflows/ssh-relay-runtime-artifacts.yml` runs syntax and source + contracts on every native family, runs all native launcher cases on both Windows architectures, + builds a separate diagnostic copy under `$RUNNER_TEMP`, and exposes only its path to the existing + live diagnostic. The workflow oracle rejects runtime-evidence, first-output, upload, and SignPath + references in that step. Runtime archives, release evidence, product imports, production system- + SSH arguments/handles, legacy fallback/default, Beta, tuple publication, and signing are unchanged. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts + --maxWorkers=1 src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts + src/main/ssh/ssh-system-no-input-windows-openssh.test.ts + src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts + config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs + config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 cases with six declared native + skips in 4.73 seconds real; maximum RSS is 167,936,000 bytes and peak footprint is 98,735,864 + bytes. +- Release contracts: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts + --maxWorkers=1 --reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 51 files and + 285/285 runnable cases with four declared Windows skips in 25.07 seconds real; maximum RSS is + 195,428,352 bytes and peak footprint is 97,638,160 bytes. One preceding quoted-glob invocation + selected no files and exited 1; it is recorded as operator error and is not evidence. +- Static gates: `/usr/bin/time -l pnpm typecheck` passes in 4.52 seconds real with maximum RSS + 1,247,133,696 bytes and peak footprint 97,982,104 bytes. `/usr/bin/time -l pnpm lint` passes in + 24.72 seconds real with only the same 26 pre-existing warnings, all 41 reliability gates, and the + 355-entry max-lines ratchet; maximum RSS is 2,030,288,896 bytes and peak footprint 97,883,872 + bytes. +- Isolation gates: purpose-file formatting, `git diff --check`, and protected-resolver zero-diff + pass. No executable can be trusted yet because local macOS cannot run legacy `csc.exe` or Win32 + handle/job semantics. +- Residual boundary: commit/push this exact artifact-only package and require Windows x64 and ARM64 + compile/native-contract/live-diagnostic evidence plus exact profile teardown. The unchanged + product full-size test is expected to remain separately gated until both architectures prove the + disconnected launcher and a subsequent product integration package is reviewed. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -19679,9 +19761,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the disconnected zero-length regular-file stdin diagnostic recorded by -`E-M6-WINDOWS-NOINPUT-REGULAR-FILE-NO-N-LOCAL-001`, then require exact-head Windows x64 and ARM64 -lifecycle plus exact profile/directory teardown. Do not +Commit and push only the disconnected launcher package recorded by +`E-M6-WINDOWS-NOINPUT-LAUNCHER-LOCAL-001`, then require exact-head Windows x64 and ARM64 legacy- +compiler, argv/EOF/binary/exit/handle-exclusion/cancellation, live diagnostic, and exact profile/ +directory teardown evidence. Do not change the production Windows handle or check the broad Milestone 6 Windows system-SSH item until both architectures select the same console-independent strategy. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 73429e3ed86..c6d07e314fd 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1653,13 +1653,19 @@

    Prove production behavior, failure behavior, and rollback behavior.

    standard-pipe EOF when Windows -n is omitted because Node closes its parent writer only after CreateProcess. Exact native x64/arm64 evidence also rejects stdin-only overlapped EOF with ordinary stdout/stderr and - no -n. Before adding a launcher, qualify a newly created - zero-length regular-file descriptor as Windows stdin: it is already at EOF - before CreateProcess, is not NUL, and leaves - stdout/stderr unchanged. Keep this disconnected until both architectures pass - full live transfer and exact teardown. POSIX retains ignored stdin plus - -n; fixture-owned Windows session processes settle before profile - hives are boundedly unloaded and native profile deletion begins; + no -n. Exact native x64/arm64 evidence further rejects a newly + created zero-length regular-file descriptor that is already at EOF before + CreateProcess; its fixture cleanup passes, but the direct client + still times out after the server closes the command session. The next bounded + artifact-only capability is a disconnected Windows launcher with private + anonymous stdin/stdout/stderr pipes, the stdin writer closed before + CreateProcessW, an exact inherited-handle list, hidden process + creation, bounded stdout/stderr pumps, exit propagation, and an owned job object + that kills the SSH child when the launcher is cancelled. Build and live-test it + on native x64/arm64 before packaging, signing, or adding a product caller. POSIX + retains ignored stdin plus -n; fixture-owned Windows session + processes settle before profile hives are boundedly unloaded and native profile + deletion begins; MaxSessions=1; interrupted upload; stale lock diff --git a/native/windows-ssh-no-input-launcher-test/WindowsLauncherHandleProbe.cs b/native/windows-ssh-no-input-launcher-test/WindowsLauncherHandleProbe.cs new file mode 100644 index 00000000000..d7197f47b87 --- /dev/null +++ b/native/windows-ssh-no-input-launcher-test/WindowsLauncherHandleProbe.cs @@ -0,0 +1,249 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +internal static class WindowsLauncherHandleProbe +{ + private const uint CreateNoWindow = 0x08000000; + private const uint WaitObject0 = 0x00000000; + private const uint WaitTimeout = 0x00000102; + + private static int Main(string[] args) + { + try + { + if (args.Length == 2 && args[0] == "probe") + { + return ProbeHandle(args[1]); + } + if (args.Length != 1) + { + Console.Error.WriteLine("Usage: handle-probe.exe "); + return 2; + } + return LaunchWithUnrelatedInheritableHandle(Path.GetFullPath(args[0])); + } + catch (Exception error) + { + Console.Error.WriteLine("Windows launcher handle probe failed: {0}", error.Message); + return 1; + } + } + + private static int ProbeHandle(string rawHandle) + { + long value = Int64.Parse(rawHandle, CultureInfo.InvariantCulture); + // A reused numeric handle in this process cannot signal the parent's event, so the + // parent-side observation distinguishes real inheritance without depending on CLR handles. + SetEvent(new IntPtr(value)); + return 0; + } + + private static int LaunchWithUnrelatedInheritableHandle(string launcherPath) + { + SecurityAttributes security = new SecurityAttributes(); + security.Length = Marshal.SizeOf(typeof(SecurityAttributes)); + security.InheritHandle = true; + IntPtr unrelatedHandle = CreateEvent(ref security, false, false, null); + if (unrelatedHandle == IntPtr.Zero) + { + throw LastWin32("CreateEventW failed."); + } + + ProcessInformation process = new ProcessInformation(); + try + { + string probePath = typeof(WindowsLauncherHandleProbe).Assembly.Location; + string[] launcherArgs = new string[] { + probePath, + "probe", + unrelatedHandle.ToInt64().ToString(CultureInfo.InvariantCulture) + }; + StringBuilder commandLine = BuildCommandLine(launcherPath, launcherArgs); + StartupInfo startup = new StartupInfo(); + startup.Size = Marshal.SizeOf(typeof(StartupInfo)); + if (!CreateProcess( + launcherPath, + commandLine, + IntPtr.Zero, + IntPtr.Zero, + true, + CreateNoWindow, + IntPtr.Zero, + null, + ref startup, + out process + )) + { + throw LastWin32("CreateProcessW failed for the launcher."); + } + + uint wait = WaitForSingleObject(process.Process, 10000); + if (wait == WaitTimeout) + { + TerminateProcess(process.Process, 74); + throw new TimeoutException("Handle probe launcher did not settle within 10 seconds."); + } + if (wait != WaitObject0) + { + throw LastWin32("Handle probe launcher wait failed."); + } + uint exitCode; + if (!GetExitCodeProcess(process.Process, out exitCode)) + { + throw LastWin32("GetExitCodeProcess failed for the launcher."); + } + if (exitCode != 0) + { + return unchecked((int)exitCode); + } + uint unrelatedWait = WaitForSingleObject(unrelatedHandle, 0); + if (unrelatedWait == WaitObject0) + { + Console.Error.WriteLine("Unrelated parent event was inherited by the launcher child."); + return 73; + } + if (unrelatedWait != WaitTimeout) + { + throw LastWin32("Unable to inspect the unrelated parent event."); + } + Console.WriteLine("ORCA-NO-INHERITED-HANDLE-LEAK"); + return 0; + } + finally + { + if (process.Thread != IntPtr.Zero) + { + CloseHandle(process.Thread); + } + if (process.Process != IntPtr.Zero) + { + CloseHandle(process.Process); + } + CloseHandle(unrelatedHandle); + } + } + + private static StringBuilder BuildCommandLine(string executablePath, string[] args) + { + StringBuilder commandLine = new StringBuilder(Quote(executablePath)); + foreach (string arg in args) + { + commandLine.Append(' '); + commandLine.Append(Quote(arg)); + } + return commandLine; + } + + private static string Quote(string value) + { + StringBuilder quoted = new StringBuilder("\""); + int backslashCount = 0; + foreach (char character in value) + { + if (character == '\\') + { + backslashCount += 1; + } + else + { + quoted.Append('\\', character == '"' ? backslashCount * 2 + 1 : backslashCount); + quoted.Append(character); + backslashCount = 0; + } + } + quoted.Append('\\', backslashCount * 2); + quoted.Append('"'); + return quoted.ToString(); + } + + private static Win32Exception LastWin32(string message) + { + return new Win32Exception(Marshal.GetLastWin32Error(), message); + } + + [StructLayout(LayoutKind.Sequential)] + private struct SecurityAttributes + { + internal int Length; + internal IntPtr SecurityDescriptor; + [MarshalAs(UnmanagedType.Bool)] internal bool InheritHandle; + } + + [StructLayout(LayoutKind.Sequential)] + private struct StartupInfo + { + internal int Size; + internal string Reserved; + internal string Desktop; + internal string Title; + internal uint X; + internal uint Y; + internal uint XSize; + internal uint YSize; + internal uint XCountChars; + internal uint YCountChars; + internal uint FillAttribute; + internal uint Flags; + internal short ShowWindow; + internal short Reserved2; + internal IntPtr Reserved2Pointer; + internal IntPtr StandardInput; + internal IntPtr StandardOutput; + internal IntPtr StandardError; + } + + [StructLayout(LayoutKind.Sequential)] + private struct ProcessInformation + { + internal IntPtr Process; + internal IntPtr Thread; + internal uint ProcessId; + internal uint ThreadId; + } + + [DllImport("kernel32.dll", EntryPoint = "CreateEventW", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern IntPtr CreateEvent( + ref SecurityAttributes eventAttributes, + [MarshalAs(UnmanagedType.Bool)] bool manualReset, + [MarshalAs(UnmanagedType.Bool)] bool initialState, + string name + ); + + [DllImport("kernel32.dll", EntryPoint = "CreateProcessW", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CreateProcess( + string applicationName, + StringBuilder commandLine, + IntPtr processAttributes, + IntPtr threadAttributes, + [MarshalAs(UnmanagedType.Bool)] bool inheritHandles, + uint creationFlags, + IntPtr environment, + string currentDirectory, + ref StartupInfo startupInfo, + out ProcessInformation processInformation + ); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetEvent(IntPtr handle); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetExitCodeProcess(IntPtr process, out uint exitCode); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool TerminateProcess(IntPtr process, uint exitCode); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseHandle(IntPtr handle); +} diff --git a/native/windows-ssh-no-input-launcher/OrcaSshNoInputLauncher.cs b/native/windows-ssh-no-input-launcher/OrcaSshNoInputLauncher.cs new file mode 100644 index 00000000000..29a4cab4117 --- /dev/null +++ b/native/windows-ssh-no-input-launcher/OrcaSshNoInputLauncher.cs @@ -0,0 +1,32 @@ +using System; +using System.IO; + +internal static class OrcaSshNoInputLauncher +{ + private static int Main(string[] args) + { + try + { + if (args.Length < 1) + { + Console.Error.WriteLine("Usage: orca-ssh-no-input.exe [arguments...]"); + return 2; + } + string executablePath = Path.GetFullPath(args[0]); + if (!File.Exists(executablePath)) + { + Console.Error.WriteLine("Unable to locate the SSH executable at \"{0}\".", executablePath); + return 2; + } + + string[] childArgs = new string[args.Length - 1]; + Array.Copy(args, 1, childArgs, 0, childArgs.Length); + return WindowsSshChildProcess.Run(executablePath, childArgs); + } + catch (Exception error) + { + Console.Error.WriteLine("Unable to start the no-input SSH child: {0}", error.Message); + return 1; + } + } +} diff --git a/native/windows-ssh-no-input-launcher/WindowsAnonymousPipeSet.cs b/native/windows-ssh-no-input-launcher/WindowsAnonymousPipeSet.cs new file mode 100644 index 00000000000..0a6efc8bd1f --- /dev/null +++ b/native/windows-ssh-no-input-launcher/WindowsAnonymousPipeSet.cs @@ -0,0 +1,252 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.Win32.SafeHandles; + +internal sealed class WindowsAnonymousPipeSet : IDisposable +{ + private const uint HandleFlagInherit = 0x00000001; + private const int PumpCompletionTimeoutMilliseconds = 5000; + + private IntPtr stdinRead; + private IntPtr stdoutWrite; + private IntPtr stderrWrite; + private SafeFileHandle stdoutRead; + private SafeFileHandle stderrRead; + private Thread stdoutPump; + private Thread stderrPump; + private Exception pumpFailure; + private readonly ManualResetEvent pumpFailureSignal = new ManualResetEvent(false); + + private WindowsAnonymousPipeSet() + { + } + + internal IntPtr StdinRead { get { return stdinRead; } } + internal IntPtr StdoutWrite { get { return stdoutWrite; } } + internal IntPtr StderrWrite { get { return stderrWrite; } } + internal IntPtr PumpFailureHandle + { + get { return pumpFailureSignal.SafeWaitHandle.DangerousGetHandle(); } + } + + internal static WindowsAnonymousPipeSet Create() + { + WindowsAnonymousPipeSet pipes = new WindowsAnonymousPipeSet(); + try + { + SecurityAttributes security = new SecurityAttributes(); + security.Length = Marshal.SizeOf(typeof(SecurityAttributes)); + security.InheritHandle = true; + + IntPtr stdinWrite; + CreateCheckedPipe(out pipes.stdinRead, out stdinWrite, security); + SetNonInheritable(stdinWrite); + // Why: Win32-OpenSSH must see EOF before CreateProcessW, not after startup. + CloseHandleChecked(stdinWrite); + + IntPtr stdoutReadHandle; + CreateCheckedPipe(out stdoutReadHandle, out pipes.stdoutWrite, security); + SetNonInheritable(stdoutReadHandle); + pipes.stdoutRead = new SafeFileHandle(stdoutReadHandle, true); + + IntPtr stderrReadHandle; + CreateCheckedPipe(out stderrReadHandle, out pipes.stderrWrite, security); + SetNonInheritable(stderrReadHandle); + pipes.stderrRead = new SafeFileHandle(stderrReadHandle, true); + return pipes; + } + catch + { + pipes.Dispose(); + throw; + } + } + + internal void CloseChildEndsInParent() + { + CloseOwnedHandle(ref stdinRead); + CloseOwnedHandle(ref stdoutWrite); + CloseOwnedHandle(ref stderrWrite); + } + + internal void StartPumps() + { + SafeFileHandle stdoutHandle = stdoutRead; + stdoutRead = null; + stdoutPump = StartPump(stdoutHandle, Console.OpenStandardOutput(), "stdout"); + + SafeFileHandle stderrHandle = stderrRead; + stderrRead = null; + stderrPump = StartPump(stderrHandle, Console.OpenStandardError(), "stderr"); + } + + internal void CompletePumps() + { + Stopwatch deadline = Stopwatch.StartNew(); + if (!JoinPump(stdoutPump, deadline) || !JoinPump(stderrPump, deadline)) + { + throw new TimeoutException("SSH output pumps did not settle within 5 seconds."); + } + ThrowIfPumpFailed(); + } + + internal void ThrowIfPumpFailed() + { + Exception failure; + lock (this) + { + failure = pumpFailure; + } + if (failure != null) + { + throw new IOException("Unable to relay SSH output.", failure); + } + } + + public void Dispose() + { + CloseChildEndsInParent(); + if (stdoutRead != null) + { + stdoutRead.Dispose(); + stdoutRead = null; + } + if (stderrRead != null) + { + stderrRead.Dispose(); + stderrRead = null; + } + Stopwatch deadline = Stopwatch.StartNew(); + JoinPump(stdoutPump, deadline); + JoinPump(stderrPump, deadline); + pumpFailureSignal.Dispose(); + } + + private Thread StartPump(SafeFileHandle sourceHandle, Stream destination, string name) + { + Thread thread = new Thread(delegate() + { + try + { + using (FileStream source = new FileStream(sourceHandle, FileAccess.Read, 4096, false)) + { + source.CopyTo(destination); + destination.Flush(); + } + } + catch (Exception error) + { + RecordPumpFailure(error); + } + }); + thread.Name = "Orca SSH " + name + " pump"; + thread.IsBackground = true; + try + { + thread.Start(); + return thread; + } + catch + { + sourceHandle.Dispose(); + throw; + } + } + + private void RecordPumpFailure(Exception error) + { + lock (this) + { + if (pumpFailure == null) + { + pumpFailure = error; + } + } + try + { + pumpFailureSignal.Set(); + } + catch (ObjectDisposedException) + { + // The launcher is already leaving after its bounded disposal wait. + } + } + + private static bool JoinPump(Thread pump, Stopwatch deadline) + { + if (pump == null) + { + return true; + } + int remaining = PumpCompletionTimeoutMilliseconds - (int)deadline.ElapsedMilliseconds; + return remaining > 0 && pump.Join(remaining); + } + + private static void CreateCheckedPipe( + out IntPtr readHandle, + out IntPtr writeHandle, + SecurityAttributes security + ) + { + if (!CreatePipe(out readHandle, out writeHandle, ref security, 0)) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "CreatePipe failed."); + } + } + + private static void SetNonInheritable(IntPtr handle) + { + if (!SetHandleInformation(handle, HandleFlagInherit, 0)) + { + int error = Marshal.GetLastWin32Error(); + CloseHandle(handle); + throw new Win32Exception(error, "SetHandleInformation failed."); + } + } + + private static void CloseHandleChecked(IntPtr handle) + { + if (!CloseHandle(handle)) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "CloseHandle failed."); + } + } + + private static void CloseOwnedHandle(ref IntPtr handle) + { + if (handle != IntPtr.Zero) + { + CloseHandle(handle); + handle = IntPtr.Zero; + } + } + + [StructLayout(LayoutKind.Sequential)] + private struct SecurityAttributes + { + internal int Length; + internal IntPtr SecurityDescriptor; + [MarshalAs(UnmanagedType.Bool)] internal bool InheritHandle; + } + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CreatePipe( + out IntPtr readPipe, + out IntPtr writePipe, + ref SecurityAttributes pipeAttributes, + uint size + ); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetHandleInformation(IntPtr handle, uint mask, uint flags); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseHandle(IntPtr handle); +} diff --git a/native/windows-ssh-no-input-launcher/WindowsCommandLine.cs b/native/windows-ssh-no-input-launcher/WindowsCommandLine.cs new file mode 100644 index 00000000000..84663ec75ca --- /dev/null +++ b/native/windows-ssh-no-input-launcher/WindowsCommandLine.cs @@ -0,0 +1,56 @@ +using System; +using System.Text; + +internal static class WindowsCommandLine +{ + internal static StringBuilder Build(string executablePath, string[] args) + { + StringBuilder commandLine = new StringBuilder(Quote(executablePath)); + foreach (string arg in args) + { + commandLine.Append(' '); + commandLine.Append(Quote(arg)); + } + return commandLine; + } + + private static string Quote(string value) + { + bool requiresQuotes = value.Length == 0; + for (int index = 0; index < value.Length && !requiresQuotes; index += 1) + { + requiresQuotes = value[index] == '"' || Char.IsWhiteSpace(value[index]); + } + if (!requiresQuotes) + { + return value; + } + + StringBuilder quoted = new StringBuilder("\""); + int backslashCount = 0; + foreach (char character in value) + { + if (character == '\\') + { + backslashCount += 1; + continue; + } + + if (character == '"') + { + quoted.Append('\\', backslashCount * 2 + 1); + quoted.Append('"'); + } + else + { + quoted.Append('\\', backslashCount); + quoted.Append(character); + } + backslashCount = 0; + } + + quoted.Append('\\', backslashCount * 2); + quoted.Append('"'); + return quoted.ToString(); + } +} diff --git a/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs b/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs new file mode 100644 index 00000000000..3a93b9ceb83 --- /dev/null +++ b/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs @@ -0,0 +1,396 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +internal static class WindowsSshChildProcess +{ + private const uint CreateSuspended = 0x00000004; + private const uint ExtendedStartupInfoPresent = 0x00080000; + private const uint CreateNoWindow = 0x08000000; + private const uint StartfUseStdHandles = 0x00000100; + private const uint JobObjectLimitKillOnJobClose = 0x00002000; + private const int JobObjectExtendedLimitInformation = 9; + private const int ProcThreadAttributeHandleList = 0x00020002; + private const uint Infinite = 0xffffffff; + private const uint WaitObject0 = 0x00000000; + private const uint WaitFailed = 0xffffffff; + + internal static int Run(string executablePath, string[] args) + { + using (WindowsAnonymousPipeSet pipes = WindowsAnonymousPipeSet.Create()) + { + IntPtr job = IntPtr.Zero; + IntPtr attributeList = IntPtr.Zero; + IntPtr inheritedHandles = IntPtr.Zero; + ProcessInformation process = new ProcessInformation(); + bool processStarted = false; + bool assignedToJob = false; + try + { + job = CreateKillOnCloseJob(); + StartupInfoEx startup = CreateStartupInfo(pipes, out attributeList, out inheritedHandles); + StringBuilder commandLine = WindowsCommandLine.Build(executablePath, args); + uint flags = CreateSuspended | ExtendedStartupInfoPresent | CreateNoWindow; + if (!CreateProcess( + executablePath, + commandLine, + IntPtr.Zero, + IntPtr.Zero, + true, + flags, + IntPtr.Zero, + null, + ref startup, + out process + )) + { + throw LastWin32("CreateProcessW failed."); + } + processStarted = true; + pipes.CloseChildEndsInParent(); + + if (!AssignProcessToJobObject(job, process.Process)) + { + throw LastWin32("AssignProcessToJobObject failed."); + } + assignedToJob = true; + pipes.StartPumps(); + if (ResumeThread(process.Thread) == UInt32.MaxValue) + { + throw LastWin32("ResumeThread failed."); + } + + IntPtr[] waitHandles = new IntPtr[] { process.Process, pipes.PumpFailureHandle }; + uint waitResult = WaitForMultipleObjects( + (uint)waitHandles.Length, + waitHandles, + false, + Infinite + ); + if (waitResult == WaitObject0 + 1) + { + pipes.ThrowIfPumpFailed(); + throw new IOException("SSH output pump failed without an error detail."); + } + if (waitResult == WaitFailed) + { + throw LastWin32("WaitForMultipleObjects failed."); + } + if (waitResult != WaitObject0) + { + throw new Win32Exception("Unexpected child-process wait result."); + } + uint exitCode; + if (!GetExitCodeProcess(process.Process, out exitCode)) + { + throw LastWin32("GetExitCodeProcess failed."); + } + // Why: descendants must not retain output writers and turn normal completion into + // an unbounded pump wait after the SSH process itself exits. + CloseOwnedHandle(ref job); + pipes.CompletePumps(); + return unchecked((int)exitCode); + } + finally + { + pipes.CloseChildEndsInParent(); + if (process.Thread != IntPtr.Zero) + { + CloseHandle(process.Thread); + } + if (process.Process != IntPtr.Zero) + { + if (processStarted && !assignedToJob) + { + // A suspended child outside the job would otherwise survive setup failure. + TerminateProcess(process.Process, 1); + } + CloseHandle(process.Process); + } + // Why: closing the owned job also kills a still-running SSH child on cancellation. + CloseOwnedHandle(ref job); + if (attributeList != IntPtr.Zero) + { + DeleteProcThreadAttributeList(attributeList); + Marshal.FreeHGlobal(attributeList); + } + if (inheritedHandles != IntPtr.Zero) + { + Marshal.FreeHGlobal(inheritedHandles); + } + } + } + } + + private static StartupInfoEx CreateStartupInfo( + WindowsAnonymousPipeSet pipes, + out IntPtr attributeList, + out IntPtr inheritedHandles + ) + { + IntPtr attributeBytes = IntPtr.Zero; + InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0, ref attributeBytes); + if (attributeBytes == IntPtr.Zero) + { + throw LastWin32("Unable to size the process attribute list."); + } + + attributeList = Marshal.AllocHGlobal(attributeBytes); + inheritedHandles = IntPtr.Zero; + try + { + if (!InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeBytes)) + { + throw LastWin32("InitializeProcThreadAttributeList failed."); + } + + inheritedHandles = Marshal.AllocHGlobal(IntPtr.Size * 3); + Marshal.WriteIntPtr(inheritedHandles, 0, pipes.StdinRead); + Marshal.WriteIntPtr(inheritedHandles, IntPtr.Size, pipes.StdoutWrite); + Marshal.WriteIntPtr(inheritedHandles, IntPtr.Size * 2, pipes.StderrWrite); + if (!UpdateProcThreadAttribute( + attributeList, + 0, + new IntPtr(ProcThreadAttributeHandleList), + inheritedHandles, + new IntPtr(IntPtr.Size * 3), + IntPtr.Zero, + IntPtr.Zero + )) + { + throw LastWin32("UpdateProcThreadAttribute failed."); + } + + StartupInfoEx startup = new StartupInfoEx(); + startup.StartupInfo.Size = Marshal.SizeOf(typeof(StartupInfoEx)); + startup.StartupInfo.Flags = StartfUseStdHandles; + startup.StartupInfo.StandardInput = pipes.StdinRead; + startup.StartupInfo.StandardOutput = pipes.StdoutWrite; + startup.StartupInfo.StandardError = pipes.StderrWrite; + startup.AttributeList = attributeList; + return startup; + } + catch + { + if (attributeList != IntPtr.Zero) + { + DeleteProcThreadAttributeList(attributeList); + Marshal.FreeHGlobal(attributeList); + attributeList = IntPtr.Zero; + } + if (inheritedHandles != IntPtr.Zero) + { + Marshal.FreeHGlobal(inheritedHandles); + inheritedHandles = IntPtr.Zero; + } + throw; + } + } + + private static IntPtr CreateKillOnCloseJob() + { + IntPtr job = CreateJobObject(IntPtr.Zero, null); + if (job == IntPtr.Zero) + { + throw LastWin32("CreateJobObject failed."); + } + JobObjectExtendedLimitInformation limits = new JobObjectExtendedLimitInformation(); + limits.BasicLimitInformation.LimitFlags = JobObjectLimitKillOnJobClose; + int size = Marshal.SizeOf(typeof(JobObjectExtendedLimitInformation)); + IntPtr buffer = Marshal.AllocHGlobal(size); + try + { + Marshal.StructureToPtr(limits, buffer, false); + if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, buffer, (uint)size)) + { + int error = Marshal.GetLastWin32Error(); + CloseHandle(job); + throw new Win32Exception(error, "SetInformationJobObject failed."); + } + return job; + } + finally + { + Marshal.FreeHGlobal(buffer); + } + } + + private static Win32Exception LastWin32(string message) + { + return new Win32Exception(Marshal.GetLastWin32Error(), message); + } + + private static void CloseOwnedHandle(ref IntPtr handle) + { + if (handle != IntPtr.Zero) + { + CloseHandle(handle); + handle = IntPtr.Zero; + } + } + + [StructLayout(LayoutKind.Sequential)] + private struct StartupInfo + { + internal int Size; + internal string Reserved; + internal string Desktop; + internal string Title; + internal uint X; + internal uint Y; + internal uint XSize; + internal uint YSize; + internal uint XCountChars; + internal uint YCountChars; + internal uint FillAttribute; + internal uint Flags; + internal short ShowWindow; + internal short Reserved2; + internal IntPtr Reserved2Pointer; + internal IntPtr StandardInput; + internal IntPtr StandardOutput; + internal IntPtr StandardError; + } + + [StructLayout(LayoutKind.Sequential)] + private struct StartupInfoEx + { + internal StartupInfo StartupInfo; + internal IntPtr AttributeList; + } + + [StructLayout(LayoutKind.Sequential)] + private struct ProcessInformation + { + internal IntPtr Process; + internal IntPtr Thread; + internal uint ProcessId; + internal uint ThreadId; + } + + [StructLayout(LayoutKind.Sequential)] + private struct JobObjectBasicLimitInformation + { + internal long PerProcessUserTimeLimit; + internal long PerJobUserTimeLimit; + internal uint LimitFlags; + internal UIntPtr MinimumWorkingSetSize; + internal UIntPtr MaximumWorkingSetSize; + internal uint ActiveProcessLimit; + internal UIntPtr Affinity; + internal uint PriorityClass; + internal uint SchedulingClass; + } + + [StructLayout(LayoutKind.Sequential)] + private struct IoCounters + { + internal ulong ReadOperationCount; + internal ulong WriteOperationCount; + internal ulong OtherOperationCount; + internal ulong ReadTransferCount; + internal ulong WriteTransferCount; + internal ulong OtherTransferCount; + } + + [StructLayout(LayoutKind.Sequential)] + private struct JobObjectExtendedLimitInformation + { + internal JobObjectBasicLimitInformation BasicLimitInformation; + internal IoCounters IoInfo; + internal UIntPtr ProcessMemoryLimit; + internal UIntPtr JobMemoryLimit; + internal UIntPtr PeakProcessMemoryUsed; + internal UIntPtr PeakJobMemoryUsed; + } + + [DllImport( + "kernel32.dll", + EntryPoint = "CreateProcessW", + CharSet = CharSet.Unicode, + SetLastError = true + )] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CreateProcess( + string applicationName, + StringBuilder commandLine, + IntPtr processAttributes, + IntPtr threadAttributes, + [MarshalAs(UnmanagedType.Bool)] bool inheritHandles, + uint creationFlags, + IntPtr environment, + string currentDirectory, + ref StartupInfoEx startupInfo, + out ProcessInformation processInformation + ); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool InitializeProcThreadAttributeList( + IntPtr attributeList, + int attributeCount, + int flags, + ref IntPtr size + ); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool UpdateProcThreadAttribute( + IntPtr attributeList, + uint flags, + IntPtr attribute, + IntPtr value, + IntPtr size, + IntPtr previousValue, + IntPtr returnSize + ); + + [DllImport("kernel32.dll")] + private static extern void DeleteProcThreadAttributeList(IntPtr attributeList); + + [DllImport( + "kernel32.dll", + EntryPoint = "CreateJobObjectW", + CharSet = CharSet.Unicode, + SetLastError = true + )] + private static extern IntPtr CreateJobObject(IntPtr jobAttributes, string name); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetInformationJobObject( + IntPtr job, + int informationClass, + IntPtr information, + uint informationLength + ); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern uint ResumeThread(IntPtr thread); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern uint WaitForMultipleObjects( + uint count, + [In] IntPtr[] handles, + [MarshalAs(UnmanagedType.Bool)] bool waitAll, + uint milliseconds + ); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetExitCodeProcess(IntPtr process, out uint exitCode); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool TerminateProcess(IntPtr process, uint exitCode); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseHandle(IntPtr handle); +} diff --git a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts index cc903f935a3..b11cb29a9f6 100644 --- a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +++ b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts @@ -1,7 +1,4 @@ import { spawn } from 'node:child_process' -import { closeSync, mkdtempSync, openSync, rmSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' import { performance } from 'node:perf_hooks' import { describe, expect, it } from 'vitest' @@ -10,7 +7,7 @@ import type { SshTarget } from '../../shared/ssh-types' import { buildSshArgs } from './system-ssh-args' import { findSystemSsh } from './system-ssh-binary' -type DiagnosticMode = 'regular-file-eof-no-n' +type DiagnosticMode = 'private-pipe-launcher-no-n' type DiagnosticResult = { mode: DiagnosticMode @@ -22,15 +19,15 @@ type DiagnosticResult = { channelClosed: boolean closeCode: number | null | 'not-observed' stderrBytes: number - stdinFixtureRemoved: boolean elapsedMs: number } const host = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_HOST const user = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_USER const identityFile = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_IDENTITY +const launcherPath = process.env.ORCA_SSH_WINDOWS_NO_INPUT_LAUNCHER const port = Number.parseInt(process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_PORT ?? '', 10) -const hasLiveInput = Boolean(host && user && identityFile && Number.isInteger(port)) +const hasLiveInput = Boolean(host && user && identityFile && launcherPath && Number.isInteger(port)) function createTarget(): SshTarget { return { @@ -51,25 +48,15 @@ async function runDiagnostic(): Promise { if (!sshPath) { throw new Error('Native Windows OpenSSH client is unavailable') } - const mode: DiagnosticMode = 'regular-file-eof-no-n' + const mode: DiagnosticMode = 'private-pipe-launcher-no-n' const startedAt = performance.now() - const stdinDirectory = mkdtempSync(join(tmpdir(), 'orca-ssh-empty-stdin-')) - const stdinPath = join(stdinDirectory, 'stdin') - const stdinFd = openSync(stdinPath, 'wx+') - let child: ReturnType - try { - child = spawn(sshPath, [...buildSshArgs(createTarget()), 'echo ORCA-SYSTEM-SSH-OK'], { - // Why: a zero-length regular file is at EOF before CreateProcess without - // triggering Win32-OpenSSH's NUL-handle bug. - stdio: [stdinFd, 'pipe', 'pipe'], - windowsHide: true - }) - } catch (error) { - closeSync(stdinFd) - rmSync(stdinDirectory, { recursive: true, force: true }) - throw error - } - closeSync(stdinFd) + // Why: the disconnected launcher creates private child pipes so Win32-OpenSSH receives EOF + // before CreateProcessW without depending on Node's Windows named-pipe behavior. + const child = spawn( + launcherPath as string, + [sshPath, ...buildSshArgs(createTarget()), 'echo ORCA-SYSTEM-SSH-OK'], + { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true } + ) return new Promise((resolve, reject) => { let stdout = '' @@ -80,14 +67,6 @@ async function runDiagnostic(): Promise { let closeCode: number | null | 'not-observed' = 'not-observed' let timedOut = false let settled = false - const removeStdinFixture = (): boolean => { - try { - rmSync(stdinDirectory, { recursive: true, force: true }) - return true - } catch { - return false - } - } const finish = (): void => { if (settled) { return @@ -95,7 +74,6 @@ async function runDiagnostic(): Promise { settled = true clearTimeout(timeout) clearTimeout(killTimeout) - const stdinFixtureRemoved = removeStdinFixture() const sentinel = stdout.includes('ORCA-SYSTEM-SSH-OK') resolve({ mode, @@ -105,8 +83,7 @@ async function runDiagnostic(): Promise { stdoutEnded && processExit === 0 && channelClosed && - closeCode === 0 && - stdinFixtureRemoved, + closeCode === 0, timedOut, sentinel, stdoutEnded, @@ -114,7 +91,6 @@ async function runDiagnostic(): Promise { channelClosed, closeCode, stderrBytes, - stdinFixtureRemoved, elapsedMs: performance.now() - startedAt }) } @@ -155,7 +131,6 @@ async function runDiagnostic(): Promise { settled = true clearTimeout(timeout) clearTimeout(killTimeout) - removeStdinFixture() reject(error) }) }) @@ -163,14 +138,16 @@ async function runDiagnostic(): Promise { describe.skipIf(!hasLiveInput)('Windows OpenSSH no-input child-handle diagnostic', () => { it( - 'qualifies regular-file stdin EOF without the Windows null-input option', + 'qualifies private-pipe stdin EOF without the Windows null-input option', { timeout: 15_000 }, async () => { expect(process.platform).toBe('win32') - const regularFileEofNoN = await runDiagnostic() - console.log(`ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ regularFileEofNoN })}`) + const privatePipeLauncherNoN = await runDiagnostic() + console.log( + `ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ privatePipeLauncherNoN })}` + ) - expect(regularFileEofNoN.success).toBe(true) + expect(privatePipeLauncherNoN.success).toBe(true) } ) }) From 6a716de14884f9c08c1e08140f87849f137622e2 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 22:52:26 -0400 Subject: [PATCH 254/267] fix(ssh): disambiguate launcher job info class Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 7 +++- ...github-release-implementation-checklist.md | 34 ++++++++++++++++--- .../WindowsSshChildProcess.cs | 9 +++-- 3 files changed, 43 insertions(+), 7 deletions(-) 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 f1402b66877..4a3ae4325c6 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 @@ -10,7 +10,7 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected Windows -no-input launcher native qualification, 2026-07-15, Codex implementation owner.** +launcher compiler correction, 2026-07-15, Codex implementation owner.** `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain @@ -164,6 +164,11 @@ formatting, diff, and resolver isolation pass. Its Windows-native suite requires argv/EOF/binary/exit, unrelated-handle exclusion, job cancellation, and bounded settlement proof; all four native cases are declared skips locally. Commit/push this exact package and require x64/ ARM64 live evidence next. Do not package or call it from production; SignPath remains deferred. +`E-M6-WINDOWS-NOINPUT-LAUNCHER-COMPILE-CI-RED-001` records exact head `1f220a713`, run +`29550660572`, and independent x64/ARM64 jobs `87792250731`/`87792250710`: both legacy compilers +reject one constant/structure identifier collision before emitting an executable. The isolated +correction renames only the integer information-class constant; focused 12/12 and release contracts +285/285 pass locally. Commit/push and rerun both architectures next. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown 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 4128db018ef..c6d688faf93 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 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected Windows no-input launcher native qualification**. `E-M6-WINDOWS-NOINPUT-LAUNCHER-LOCAL-001` records the unpackaged, unsigned launcher and its local source/workflow/static contracts green. Commit/push only this exact artifact-only package and require native Windows x64/ARM64 legacy-compiler, argv/EOF/binary/exit/handle-exclusion/cancellation, live diagnostic, and exact teardown proof before any product caller. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain an explicit late gate and are not on this package's critical path.
    -Session checkpoint: **DIRECT WINDOWS NODE STDIN BOUNDARIES NATIVELY RED / DISCONNECTED PRIVATE-PIPE LAUNCHER LOCALLY GREEN / NATIVE QUALIFICATION NEXT — 2026-07-15, Codex implementation owner** — `E-M6-WINDOWS-NOINPUT-REGULAR-FILE-NO-N-CI-RED-001` records exact head `eee686d7f6203c88f00d607711d3816aaf444c61`, artifact run `29549252407`, Windows x64/ARM64 jobs `87788106340`/`87788106351`, and independent 8036.46/8042.17-millisecond timeouts after the last direct-Node boundary. `E-M6-WINDOWS-NOINPUT-LAUNCHER-LOCAL-001` adds only a runner-temp launcher with private anonymous stdio pipes, pre-execution stdin EOF, exact three-handle inheritance, suspended/job-owned hidden child creation, pump-failure cancellation, bounded pump settlement, and exit propagation. Focused 119/119, release contracts 285/285, typecheck, full lint/reliability/max-lines, formatting, diff, and resolver isolation pass locally; four launcher-native and two live Windows cases are declared skips. Production runtime bytes still cross authenticated SSH and no product remote downloads anything. Do not connect a product caller, change legacy upload/fallback/default behavior, enable a tuple, publish an artifact, or perform SignPath work. Merging to `main` remains prohibited, SignPath is deferred, and legacy remains the production default.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected Windows launcher compiler correction**. `E-M6-WINDOWS-NOINPUT-LAUNCHER-COMPILE-CI-RED-001` records exact head `1f220a7135b83ef4c5f035ab729c114e828bf5b3`, run `29550660572`, and the same `CS0102` identifier collision from Windows x64/ARM64 jobs `87792250731`/`87792250710`. Rename only the integer job-information-class constant, re-prove local contracts, and require fresh native x64/ARM64 evidence. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain deferred.
    +Session checkpoint: **LAUNCHER LOCAL CONTRACTS GREEN / X64 AND ARM64 LEGACY COMPILERS RED / ONE-IDENTIFIER CORRECTION ACTIVE — 2026-07-15, Codex implementation owner** — local focused 119/119, release contracts 285/285, typecheck, full lint/reliability/max-lines, formatting, diff, and resolver isolation pass. Both native jobs independently reject the constant/structure name collision before producing an executable or reaching live SSH; the runner-temp build, live fixture, full-size test, upload, production caller, legacy behavior, Beta, tuple publication, and SignPath remain untouched. Rename only `JobObjectExtendedLimitInformation` constant to `JobObjectExtendedLimitInformationClass`, commit/push, and rerun both native architectures.
    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) @@ -19703,6 +19703,32 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two de product full-size test is expected to remain separately gated until both architectures prove the disconnected launcher and a subsequent product integration package is reviewed. +### E-M6-WINDOWS-NOINPUT-LAUNCHER-COMPILE-CI-RED-001 — native compiler rejects a name collision + +- Date/owner/head/run: 2026-07-15, Codex implementation owner; exact pushed head + `1f220a7135b83ef4c5f035ab729c114e828bf5b3`, artifact run `29550660572`, Windows x64 job + `87792250731` on `windows-2022`, and Windows ARM64 job `87792250710` on `windows-11-arm`. +- Exact log command: `gh api repos/stablyai/orca/actions/jobs//logs | rg -n -C 8 + 'WindowsSshChildProcess.cs\\(14|error CS0102|Failed Suites|runtime-windows-no-input-launcher'` + for each job. +- Native result: both architectures reach the purpose-named contract suite and the .NET Framework + compiler independently returns `CS0102`: the integer job-information-class constant and the + marshalled structure both used `JobObjectExtendedLimitInformation`. X64 otherwise records 100 + passing test files, 854 passing cases, and 24 declared skips before failing the one launcher + suite; ARM64 records the same single compiler failure shape. No executable or Win32 behavior was + tested, so no native capability is claimed. +- Workflow containment: both jobs fail before the separate runner-temp build, Node input download, + runtime build, live OpenSSH fixture, full-size test, or artifact upload. The always-run teardown + settles without a created fixture. Linux x64/ARM64 and Darwin ARM64 remain green in the same run; + Darwin x64 was still running when the isolated correction was prepared. +- Correction: rename only the integer constant to + `JobObjectExtendedLimitInformationClass`; the structure, P/Invoke layout, handle behavior, + packaging boundary, production behavior, and every signing/default/fallback decision remain + unchanged. Focused launcher/workflow contracts pass 12 runnable cases with four declared Windows + skips; the exact release-contract command passes 51 files and 285/285 runnable cases with four + declared Windows skips. Syntax, `git diff --check`, and protected-resolver zero-diff pass. Commit/ + push the correction and require both native architectures from the corrected exact head. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -19761,8 +19787,8 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the disconnected launcher package recorded by -`E-M6-WINDOWS-NOINPUT-LAUNCHER-LOCAL-001`, then require exact-head Windows x64 and ARM64 legacy- +Commit and push only the one-identifier compiler correction recorded by +`E-M6-WINDOWS-NOINPUT-LAUNCHER-COMPILE-CI-RED-001`, then require exact-head Windows x64 and ARM64 legacy- compiler, argv/EOF/binary/exit/handle-exclusion/cancellation, live diagnostic, and exact profile/ directory teardown evidence. Do not change the production Windows handle or check the broad Milestone 6 Windows system-SSH item until diff --git a/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs b/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs index 3a93b9ceb83..c7327e593b1 100644 --- a/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs +++ b/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs @@ -11,7 +11,7 @@ internal static class WindowsSshChildProcess private const uint CreateNoWindow = 0x08000000; private const uint StartfUseStdHandles = 0x00000100; private const uint JobObjectLimitKillOnJobClose = 0x00002000; - private const int JobObjectExtendedLimitInformation = 9; + private const int JobObjectExtendedLimitInformationClass = 9; private const int ProcThreadAttributeHandleList = 0x00020002; private const uint Infinite = 0xffffffff; private const uint WaitObject0 = 0x00000000; @@ -203,7 +203,12 @@ private static IntPtr CreateKillOnCloseJob() try { Marshal.StructureToPtr(limits, buffer, false); - if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, buffer, (uint)size)) + if (!SetInformationJobObject( + job, + JobObjectExtendedLimitInformationClass, + buffer, + (uint)size + )) { int error = Marshal.GetLastWin32Error(); CloseHandle(job); From 5841b12bc94ecd1027e507e4b741e09fcb0a093d Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 23:19:03 -0400 Subject: [PATCH 255/267] test(ssh): add private-console Windows diagnostic Co-authored-by: Orca --- .../build-windows-ssh-no-input-launcher.mjs | 3 +- ...runtime-windows-no-input-launcher.test.mjs | 70 ++++- ...elease-implementation-checklist-summary.md | 27 +- ...github-release-implementation-checklist.md | 110 ++++++-- ...6-07-14-ssh-relay-github-release-plan.html | 18 +- .../WindowsConsoleEofProbe.cs | 65 +++++ .../WindowsPrivateConsoleInput.cs | 251 ++++++++++++++++++ ...onymousPipeSet.cs => WindowsSshChildIo.cs} | 46 ++-- .../WindowsSshChildProcess.cs | 16 +- ...sh-system-no-input-windows-openssh.test.ts | 16 +- 10 files changed, 550 insertions(+), 72 deletions(-) create mode 100644 native/windows-ssh-no-input-launcher-test/WindowsConsoleEofProbe.cs create mode 100644 native/windows-ssh-no-input-launcher/WindowsPrivateConsoleInput.cs rename native/windows-ssh-no-input-launcher/{WindowsAnonymousPipeSet.cs => WindowsSshChildIo.cs} (86%) diff --git a/config/scripts/build-windows-ssh-no-input-launcher.mjs b/config/scripts/build-windows-ssh-no-input-launcher.mjs index aa0e76d3af2..2e8b40bd1f6 100644 --- a/config/scripts/build-windows-ssh-no-input-launcher.mjs +++ b/config/scripts/build-windows-ssh-no-input-launcher.mjs @@ -12,8 +12,9 @@ const repoRoot = resolve(import.meta.dirname, '../..') const sourceRoot = join(repoRoot, 'native', 'windows-ssh-no-input-launcher') const sourcePaths = [ 'OrcaSshNoInputLauncher.cs', - 'WindowsAnonymousPipeSet.cs', 'WindowsCommandLine.cs', + 'WindowsPrivateConsoleInput.cs', + 'WindowsSshChildIo.cs', 'WindowsSshChildProcess.cs' ].map((name) => join(sourceRoot, name)) const outputPath = readArg('--output') ?? join(sourceRoot, '.build', 'orca-ssh-no-input.exe') diff --git a/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs b/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs index 4a6006c4437..d4d26fa5319 100644 --- a/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs +++ b/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs @@ -19,6 +19,7 @@ let fixtureRoot let launcherPath let childFixturePath let handleProbePath +let consoleEofProbePath beforeAll(() => { if (process.platform !== 'win32') { @@ -28,6 +29,7 @@ beforeAll(() => { launcherPath = join(fixtureRoot, 'orca-ssh-no-input.exe') childFixturePath = join(fixtureRoot, 'child-fixture.cjs') handleProbePath = join(fixtureRoot, 'windows-launcher-handle-probe.exe') + consoleEofProbePath = join(fixtureRoot, 'windows-console-eof-probe.exe') writeFileSync(childFixturePath, childFixtureSource, 'utf8') const build = spawnSync(process.execPath, [buildScript, '--output', launcherPath], { cwd: projectRoot, @@ -57,6 +59,27 @@ beforeAll(() => { { cwd: projectRoot, encoding: 'utf8' } ) expect(probeBuild.status, `${probeBuild.stdout}\n${probeBuild.stderr}`).toBe(0) + + const eofProbeSource = join( + projectRoot, + 'native', + 'windows-ssh-no-input-launcher-test', + 'WindowsConsoleEofProbe.cs' + ) + const eofProbeBuild = spawnSync( + compilerPath, + [ + '/nologo', + '/target:exe', + '/platform:anycpu', + '/optimize+', + '/warnaserror+', + `/out:${consoleEofProbePath}`, + eofProbeSource + ], + { cwd: projectRoot, encoding: 'utf8' } + ) + expect(eofProbeBuild.status, `${eofProbeBuild.stdout}\n${eofProbeBuild.stderr}`).toBe(0) }, 20_000) afterAll(() => { @@ -68,16 +91,28 @@ afterAll(() => { describe('Windows SSH no-input launcher', () => { it('keeps the artifact source scoped to the reviewed Win32 boundary', () => { const processSource = readFileSync(join(sourceRoot, 'WindowsSshChildProcess.cs'), 'utf8') - const pipeSource = readFileSync(join(sourceRoot, 'WindowsAnonymousPipeSet.cs'), 'utf8') + const consoleSource = readFileSync(join(sourceRoot, 'WindowsPrivateConsoleInput.cs'), 'utf8') + const pipeSource = readFileSync(join(sourceRoot, 'WindowsSshChildIo.cs'), 'utf8') expect(processSource).toContain('ProcThreadAttributeHandleList') expect(processSource).toContain('IntPtr.Size * 3') expect(processSource).toContain('CreateSuspended') - expect(processSource).toContain('CreateNoWindow') + expect(processSource).not.toContain('CreateNoWindow') expect(processSource).toContain('JobObjectLimitKillOnJobClose') expect(processSource).toContain('WaitForMultipleObjects') expect(processSource).toContain('if (processStarted && !assignedToJob)') - expect(pipeSource).toContain('CloseHandleChecked(stdinWrite)') + expect(consoleSource).toContain('AllocConsole()') + expect(consoleSource).toContain('"CONIN$"') + expect(consoleSource).toContain('ShowWindow(window, SwHide)') + expect(consoleSource).toContain('WriteConsoleInput') + expect(consoleSource).toContain('console.QueueEndOfInput()') + expect(processSource).toContain('WindowsPrivateConsoleInput.Create()') + expect(consoleSource.indexOf('console.QueueEndOfInput()')).toBeLessThan( + consoleSource.indexOf('return console;') + ) + expect(processSource.indexOf('WindowsPrivateConsoleInput.Create()')).toBeLessThan( + processSource.indexOf('CreateProcess(') + ) expect(pipeSource).toContain('PumpCompletionTimeoutMilliseconds') expect(pipeSource).toContain('pumpFailureSignal.Set()') }) @@ -95,14 +130,29 @@ describe('Windows SSH no-input launcher', () => { } ) - itWindows('preserves exact arguments and provides child stdin EOF before execution', () => { + itWindows('preserves exact arguments and qualifies private-console stdin', () => { const args = ['', 'space value', 'quote"value', 'trailing slash\\', 'line one\nline two', '雪'] const result = runLauncher('arguments-and-stdin', args, { input: Buffer.from('this input must not reach the SSH child', 'utf8') }) expect(result.status, result.stderr.toString('utf8')).toBe(0) - expect(JSON.parse(result.stdout.toString('utf8'))).toEqual({ args, stdinBytes: 0 }) + expect(JSON.parse(result.stdout.toString('utf8'))).toEqual({ + args, + stdinIsTTY: true + }) + }) + + itWindows('queues cooked private-console EOF before child execution', () => { + const result = spawnSync(launcherPath, [consoleEofProbePath], { + cwd: fixtureRoot, + encoding: 'utf8', + timeout: 10_000, + windowsHide: true + }) + + expect(result.status, result.stderr).toBe(0) + expect(result.stdout).toBe('ORCA-PRIVATE-CONSOLE-EOF bytes=0') }) itWindows('preserves binary stdout and stderr and propagates the child exit code', () => { @@ -219,12 +269,10 @@ async function waitForProcessExit(pid, timeoutMs) { const childFixtureSource = String.raw`const mode = process.argv[2] if (mode === 'arguments-and-stdin') { - let stdinBytes = 0 - process.stdin.on('data', (chunk) => { stdinBytes += chunk.length }) - process.stdin.on('end', () => { - process.stdout.write(JSON.stringify({ args: process.argv.slice(3), stdinBytes })) - }) - process.stdin.resume() + process.stdout.write(JSON.stringify({ + args: process.argv.slice(3), + stdinIsTTY: process.stdin.isTTY === true + })) } else if (mode === 'binary-output') { process.stdout.write(Buffer.from([0x00, 0xff, 0x10, 0x0a, 0x80])) process.stderr.write(Buffer.from([0xfe, 0x00, 0x7f, 0x0d, 0x0a])) 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 4a3ae4325c6..80118bfb76b 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-15 +Last updated: 2026-07-16 Use this file to track the project. The [detailed evidence ledger](./2026-07-14-ssh-relay-github-release-implementation-checklist.md) @@ -9,8 +9,8 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected Windows -launcher compiler correction, 2026-07-15, Codex implementation owner.** +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected private- +console Windows diagnostic, 2026-07-16, Codex implementation owner.** `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain @@ -168,7 +168,26 @@ ARM64 live evidence next. Do not package or call it from production; SignPath re `29550660572`, and independent x64/ARM64 jobs `87792250731`/`87792250710`: both legacy compilers reject one constant/structure identifier collision before emitting an executable. The isolated correction renames only the integer information-class constant; focused 12/12 and release contracts -285/285 pass locally. Commit/push and rerun both architectures next. +285/285 pass locally. Corrected exact head `6a716de14`, run `29551003702`, and x64/ARM64 jobs +`87793308016`/`87793308044` then pass legacy compilation and every native launcher contract plus the +complete 101-file/859-case artifact contract step, but the live diagnostic still times out after +8024.0679/8031.6544 milliseconds. Both observe ended stdout, a closed SSH channel, no sentinel or +process exit, and zero stderr; both exact profile teardowns remain RED on locked `UsrClass.dat`. +`E-M6-WINDOWS-NOINPUT-LAUNCHER-LIVE-CI-RED-001` therefore falsifies Node's pipe type and broad +inherited-handle leakage. Next test only a hidden launcher-owned real-console stdin with console EOF +queued before child creation while retaining private output pipes, exact handle inheritance, job +ownership, and bounded cancellation. Keep it runner-temp-only and disconnected from product; +SignPath remains deferred. +`E-M6-WINDOWS-PRIVATE-CONSOLE-LOCAL-001` records the replacement candidate locally green. It +captures launcher output before detaching, allocates and hides a private console, queues Ctrl+Z plus +Enter into `CONIN$` before child creation, and shares that console with the suspended/job-owned SSH +child while retaining private output pipes and the exact three-handle list. A dedicated native probe +requires character-device stdin, successful `GetConsoleMode`, and a zero-byte cooked `ReadFile`. +Focused 119/119 with seven native skips, all 285 release contracts with five native skips, typecheck, +full lint/reliability/max-lines, formatting, diff, and protected-resolver isolation pass. Commit/push +this artifact-only package and require fresh x64/ARM64 compiler, console/EOF, launcher, live SSH, and +exact teardown proof; no product, Beta, fallback, tuple, publication, default, or SignPath work is +included. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown 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 c6d688faf93..f7376f6f840 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-15
    -Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-15, Codex implementation owner: disconnected Windows launcher compiler correction**. `E-M6-WINDOWS-NOINPUT-LAUNCHER-COMPILE-CI-RED-001` records exact head `1f220a7135b83ef4c5f035ab729c114e828bf5b3`, run `29550660572`, and the same `CS0102` identifier collision from Windows x64/ARM64 jobs `87792250731`/`87792250710`. Rename only the integer job-information-class constant, re-prove local contracts, and require fresh native x64/ARM64 evidence. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain deferred.
    -Session checkpoint: **LAUNCHER LOCAL CONTRACTS GREEN / X64 AND ARM64 LEGACY COMPILERS RED / ONE-IDENTIFIER CORRECTION ACTIVE — 2026-07-15, Codex implementation owner** — local focused 119/119, release contracts 285/285, typecheck, full lint/reliability/max-lines, formatting, diff, and resolver isolation pass. Both native jobs independently reject the constant/structure name collision before producing an executable or reaching live SSH; the runner-temp build, live fixture, full-size test, upload, production caller, legacy behavior, Beta, tuple publication, and SignPath remain untouched. Rename only `JobObjectExtendedLimitInformation` constant to `JobObjectExtendedLimitInformationClass`, commit/push, and rerun both native architectures.
    +Last updated: 2026-07-16
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-16, Codex implementation owner: disconnected private-console Windows diagnostic native proof**. `E-M6-WINDOWS-PRIVATE-CONSOLE-LOCAL-001` records the unpackaged, unsigned, runner-temp diagnostic locally green. Commit/push only this artifact-only package and require fresh Windows x64/ARM64 legacy-compiler, real-console qualification, pre-execution cooked EOF, argv/binary/exit/handle-exclusion/cancellation, live lifecycle, and exact teardown proof. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain deferred.
    +Session checkpoint: **PRIVATE CONSOLE CONTRACTS LOCALLY GREEN / X64 AND ARM64 NATIVE PROOF REQUIRED — 2026-07-16, Codex implementation owner** — the launcher captures its caller output streams, detaches from any inherited console, allocates and hides a private console, queues Ctrl+Z plus Enter into `CONIN$` before child creation, and shares that console with the suspended/job-owned child while retaining private stdout/stderr pipes and the exact three-handle list. A dedicated native probe requires character-device stdin, successful `GetConsoleMode`, and zero-byte cooked `ReadFile`. Local focused 119/119 with seven native skips, release contracts 285/285 with five native skips, typecheck, full lint/reliability/max-lines, formatting, diff, and resolver isolation pass. No product caller, full-size transfer claim, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
    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) @@ -19627,8 +19627,8 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two de `eee686d7f6203c88f00d607711d3816aaf444c61`, artifact run `29549252407`, Windows x64 job `87788106340` on `windows-2022`, and Windows ARM64 job `87788106351` on `windows-11-arm`. - Exact log command: `gh api repos/stablyai/orca/actions/jobs//logs | rg -n -C 15 - 'ssh_windows_no_input_handle_diagnostic|Windows no-input handle diagnostic failed|profile - teardown:|UsrClass.dat|regular-file-eof-no-n|stdinFixtureRemoved|sentinel|stderrBytes'` for each +'ssh_windows_no_input_handle_diagnostic|Windows no-input handle diagnostic failed|profile +teardown:|UsrClass.dat|regular-file-eof-no-n|stdinFixtureRemoved|sentinel|stderrBytes'` for each job. - Native result: x64 returns `success=false`, `timedOut=true`, `sentinel=false`, `stdoutEnded=true`, `processExit=null`, `channelClosed=true`, `closeCode=null`, `stderrBytes=0`, @@ -19678,15 +19678,15 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two de references in that step. Runtime archives, release evidence, product imports, production system- SSH arguments/handles, legacy fallback/default, Beta, tuple publication, and signing are unchanged. - Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts - --maxWorkers=1 src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts - src/main/ssh/ssh-system-no-input-windows-openssh.test.ts - src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts - config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs - config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 cases with six declared native +--maxWorkers=1 src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 cases with six declared native skips in 4.73 seconds real; maximum RSS is 167,936,000 bytes and peak footprint is 98,735,864 bytes. - Release contracts: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts - --maxWorkers=1 --reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 51 files and +--maxWorkers=1 --reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 51 files and 285/285 runnable cases with four declared Windows skips in 25.07 seconds real; maximum RSS is 195,428,352 bytes and peak footprint is 97,638,160 bytes. One preceding quoted-glob invocation selected no files and exited 1; it is recorded as operator error and is not evidence. @@ -19709,7 +19709,7 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two de `1f220a7135b83ef4c5f035ab729c114e828bf5b3`, artifact run `29550660572`, Windows x64 job `87792250731` on `windows-2022`, and Windows ARM64 job `87792250710` on `windows-11-arm`. - Exact log command: `gh api repos/stablyai/orca/actions/jobs//logs | rg -n -C 8 - 'WindowsSshChildProcess.cs\\(14|error CS0102|Failed Suites|runtime-windows-no-input-launcher'` +'WindowsSshChildProcess.cs\\(14|error CS0102|Failed Suites|runtime-windows-no-input-launcher'` for each job. - Native result: both architectures reach the purpose-named contract suite and the .NET Framework compiler independently returns `CS0102`: the integer job-information-class constant and the @@ -19729,6 +19729,84 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 117 cases with two de declared Windows skips. Syntax, `git diff --check`, and protected-resolver zero-diff pass. Commit/ push the correction and require both native architectures from the corrected exact head. +### E-M6-WINDOWS-NOINPUT-LAUNCHER-LIVE-CI-RED-001 — private redirected pipes do not settle Win32-OpenSSH + +- Date/owner/head/run: 2026-07-16, Codex implementation owner; exact pushed head + `6a716de14884f9c08c1e08140f87849f137622e2`, artifact run `29551003702`, Windows x64 job + `87793308016` on `windows-2022`, and Windows ARM64 job `87793308044` on `windows-11-arm`. + GitHub records the jobs from 2026-07-17 02:53Z through 03:03Z. +- Native launcher proof: both architectures compile with the legacy .NET Framework compiler and + pass exact argv quoting, pre-execution stdin EOF, binary stdout/stderr, exit-code propagation, + unrelated inherited-handle exclusion, job-owned cancellation, and bounded settlement. Each full + artifact contract step passes 101 files, 859 runnable cases, and 19 declared skips. Both jobs + build and verify the unchanged full runtime before starting the disconnected live fixture. +- Live x64 result: `elapsedMs=8024.0679`, `success=false`, `timedOut=true`, `sentinel=false`, + `stdoutEnded=true`, `processExit=null`, `channelClosed=true`, `closeCode=null`, and + `stderrBytes=0`. +- Live ARM64 result: `elapsedMs=8031.6544` with the same remaining lifecycle fields. Both official + server logs prove authentication and remote command-session closure. The launcher remains alive + until the bounded diagnostic terminates it, so this is not transfer or enablement evidence. +- Teardown result: both exact owned-profile teardowns remain RED because `UsrClass.dat` is locked. + The workflow correctly stops before the unchanged product full-size Windows transfer and upload. + All non-Windows primary jobs and both Linux oldest-userland supplements pass in the same run. +- Interpretation: Node's Windows pipe type and broad inherited-handle leakage are falsified as the + cause. Win32-OpenSSH issue #1330 separately reports the same hang when all standard handles are + redirected, including cases where `-n`/NUL and `-T` do not help, while a real console stdin + behaves differently. Do not add sentinel-based forced success or kill-after-output: trustworthy + remote exit-status framing would be a separate reviewed protocol contract. +- Next bounded diagnostic: allocate a launcher-owned private console, hide it, open `CONIN$`, queue + console EOF input before `CreateProcessW`, and share that console with the SSH child. Retain + private stdout/stderr pipes, the exact three-handle inheritance list, suspended job assignment, + bounded pumps, exit propagation, and cancellation ownership. First prove the precise console EOF + semantics with a native purpose-named probe, then require the live x64 and ARM64 lifecycle and + exact teardown. Keep the executable runner-temp-only and disconnected from product packaging. + +### E-M6-WINDOWS-PRIVATE-CONSOLE-LOCAL-001 — private real-console candidate is locally contract-green + +- Date/owner/base/runner: 2026-07-16, Codex implementation owner; uncommitted capability atop exact + private-pipe RED head `6a716de14884f9c08c1e08140f87849f137622e2` on Apple arm64, macOS 26.2 + build 25C56, Node 26.5.0, and pnpm 10.24.0. +- Source audit: PowerShell/openssh-portable exact source `e98b11e5de035cbd0eecbddc95a87da42f14671f` + initializes Win32 standard handles as synchronous external I/O, classifies console input as + `FILE_TYPE_CHAR`, and routes it through the dedicated synchronous console `ReadFile` path in + `contrib/win32/win32compat/w32fd.c`, `fileio.c`, and `termio.c`. Orca's existing `-T` in + `system-ssh-args.ts` prevents real-console stdin from requesting a remote PTY. +- Artifact boundary: the launcher captures its caller's stdout/stderr streams before detaching from + any inherited console, allocates a private console, hides any console window, opens inheritable + `CONIN$`, selects processed line input, flushes it, and queues Ctrl+Z key-down/up plus Enter key- + down/up before `CreateProcessW`. The child shares that private console; `CREATE_NO_WINDOW` is + deliberately absent because it would detach the child from the console. Private anonymous stdout/ + stderr pipes, exact three-handle inheritance, suspended job assignment, bounded pumps, exit + propagation, and kill-on-job-close cancellation remain intact. +- Native contract design: a purpose-named legacy-compiler probe rejects non-character stdin or + failed `GetConsoleMode`, selects cooked input, directly calls `ReadFile`, and requires zero bytes. + The existing suite separately requires console-qualified stdin plus exact argv, binary output, + exit propagation, unrelated inherited-handle exclusion, and bounded job-owned cancellation. + Five Windows-native cases are declared skips locally; neither the console semantics nor the live + OpenSSH result is claimed without x64 and ARM64 runners. +- Focused command: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases with seven declared + native/live skips in 3.27 seconds real; maximum RSS is 171,180,032 bytes and peak footprint is + 96,999,352 bytes. +- Release contracts: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +--reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 51 files and 285/285 runnable + cases with five declared Windows skips in 16.17 seconds. One prior quoted-glob invocation selected + no files and exited 1; it is operator error and not evidence. +- Static gates: `/usr/bin/time -l pnpm typecheck` passes in 3.55 seconds real with maximum RSS + 1,257,357,312 bytes and peak footprint 97,507,040 bytes. `/usr/bin/time -l pnpm lint` passes in + 19.82 seconds real with the same 26 existing warnings, all 41 reliability gates, and the 355-entry + max-lines ratchet; maximum RSS is 2,041,757,696 bytes and peak footprint 97,179,360 bytes. Purpose- + file formatting, syntax, `git diff --check`, and protected-resolver zero-diff pass. +- Residual boundary: commit/push this exact artifact-only package and require native x64/ARM64 + legacy compilation plus all purpose-named contracts before interpreting the live result. A green + live diagnostic still would not authorize product packaging/caller, full-size transfer, fallback, + Beta, tuple enablement, publication, default behavior, or signing. A RED native probe or live + result must be recorded before selecting another mechanism such as ConPTY. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -19787,10 +19865,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the one-identifier compiler correction recorded by -`E-M6-WINDOWS-NOINPUT-LAUNCHER-COMPILE-CI-RED-001`, then require exact-head Windows x64 and ARM64 legacy- -compiler, argv/EOF/binary/exit/handle-exclusion/cancellation, live diagnostic, and exact profile/ -directory teardown evidence. Do not +Commit and push only the private real-console stdin diagnostic recorded by +`E-M6-WINDOWS-PRIVATE-CONSOLE-LOCAL-001`, then require exact-head Windows x64 and ARM64 legacy- +compiler, console qualification/EOF, argv/binary/exit/handle-exclusion/cancellation, live +diagnostic, and exact profile/directory teardown evidence. Do not change the production Windows handle or check the broad Milestone 6 Windows system-SSH item until both architectures select the same console-independent strategy. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index c6d07e314fd..ec3a4995eed 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1657,15 +1657,21 @@

    Prove production behavior, failure behavior, and rollback behavior.

    created zero-length regular-file descriptor that is already at EOF before CreateProcess; its fixture cleanup passes, but the direct client still times out after the server closes the command session. The next bounded - artifact-only capability is a disconnected Windows launcher with private + artifact-only capability was a disconnected Windows launcher with private anonymous stdin/stdout/stderr pipes, the stdin writer closed before CreateProcessW, an exact inherited-handle list, hidden process creation, bounded stdout/stderr pumps, exit propagation, and an owned job object - that kills the SSH child when the launcher is cancelled. Build and live-test it - on native x64/arm64 before packaging, signing, or adding a product caller. POSIX - retains ignored stdin plus -n; fixture-owned Windows session - processes settle before profile hives are boundedly unloaded and native profile - deletion begins; + that kills the SSH child when the launcher is cancelled. Exact native x64/arm64 + evidence proves that launcher compiles and passes all of those contracts, but + Win32-OpenSSH still remains alive after authenticated remote command-session + closure. This falsifies Node's pipe type and broad inherited-handle leakage. The + next artifact-only diagnostic uses a hidden launcher-owned private real console, + queues console EOF before child creation, and shares that console with the SSH + child while retaining private output pipes, exact handle inheritance, job + ownership, and bounded cancellation. Build and live-test it on native x64/arm64 + before packaging, signing, or adding a product caller. POSIX retains ignored + stdin plus -n; fixture-owned Windows session processes settle + before profile hives are boundedly unloaded and native profile deletion begins; MaxSessions=1; interrupted upload; stale lock diff --git a/native/windows-ssh-no-input-launcher-test/WindowsConsoleEofProbe.cs b/native/windows-ssh-no-input-launcher-test/WindowsConsoleEofProbe.cs new file mode 100644 index 00000000000..d2d1b06a03f --- /dev/null +++ b/native/windows-ssh-no-input-launcher-test/WindowsConsoleEofProbe.cs @@ -0,0 +1,65 @@ +using System; +using System.Runtime.InteropServices; + +internal static class WindowsConsoleEofProbe +{ + private const int StandardInputHandle = -10; + private const uint FileTypeChar = 0x0002; + private const uint EnableProcessedInput = 0x0001; + private const uint EnableLineInput = 0x0002; + + private static int Main() + { + IntPtr input = GetStdHandle(StandardInputHandle); + uint mode; + if (input == IntPtr.Zero || input == new IntPtr(-1) || GetFileType(input) != FileTypeChar) + { + Console.Error.WriteLine("stdin is not a character-device handle"); + return 2; + } + if (!GetConsoleMode(input, out mode)) + { + Console.Error.WriteLine("stdin is not a console input handle"); + return 3; + } + if (!SetConsoleMode(input, mode | EnableProcessedInput | EnableLineInput)) + { + Console.Error.WriteLine("unable to select cooked console input"); + return 4; + } + + byte[] buffer = new byte[1]; + uint bytesRead; + if (!ReadFile(input, buffer, (uint)buffer.Length, out bytesRead, IntPtr.Zero)) + { + Console.Error.WriteLine("console ReadFile failed: {0}", Marshal.GetLastWin32Error()); + return 5; + } + Console.Out.Write("ORCA-PRIVATE-CONSOLE-EOF bytes={0}", bytesRead); + return bytesRead == 0 ? 0 : 6; + } + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr GetStdHandle(int standardHandle); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern uint GetFileType(IntPtr file); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetConsoleMode(IntPtr console, out uint mode); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetConsoleMode(IntPtr console, uint mode); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool ReadFile( + IntPtr file, + [Out] byte[] buffer, + uint bytesToRead, + out uint bytesRead, + IntPtr overlapped + ); +} diff --git a/native/windows-ssh-no-input-launcher/WindowsPrivateConsoleInput.cs b/native/windows-ssh-no-input-launcher/WindowsPrivateConsoleInput.cs new file mode 100644 index 00000000000..b1da1beff1b --- /dev/null +++ b/native/windows-ssh-no-input-launcher/WindowsPrivateConsoleInput.cs @@ -0,0 +1,251 @@ +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; + +internal sealed class WindowsPrivateConsoleInput : IDisposable +{ + private const uint EnableProcessedInput = 0x0001; + private const uint EnableLineInput = 0x0002; + private const uint GenericRead = 0x80000000; + private const uint GenericWrite = 0x40000000; + private const uint FileShareRead = 0x00000001; + private const uint FileShareWrite = 0x00000002; + private const uint OpenExisting = 3; + private const uint HandleFlagInherit = 0x00000001; + private const short KeyEvent = 0x0001; + private const ushort VirtualKeyZ = 0x5a; + private const ushort VirtualKeyReturn = 0x0d; + private const uint MapVirtualKeyToScanCode = 0; + private const uint LeftCtrlPressed = 0x0008; + private const int SwHide = 0; + private const int ErrorInvalidHandle = 6; + + private IntPtr inputHandle; + private bool ownsConsole; + + private WindowsPrivateConsoleInput() + { + } + + internal IntPtr InputHandle { get { return inputHandle; } } + + internal static WindowsPrivateConsoleInput Create() + { + WindowsPrivateConsoleInput console = new WindowsPrivateConsoleInput(); + try + { + console.DetachFromInheritedConsole(); + if (!AllocConsole()) + { + throw LastWin32("AllocConsole failed."); + } + console.ownsConsole = true; + HideConsoleWindow(); + + console.inputHandle = CreateFile( + "CONIN$", + GenericRead | GenericWrite, + FileShareRead | FileShareWrite, + IntPtr.Zero, + OpenExisting, + 0, + IntPtr.Zero + ); + if (console.inputHandle == new IntPtr(-1)) + { + console.inputHandle = IntPtr.Zero; + throw LastWin32("Unable to open the private console input buffer."); + } + if (!SetConsoleMode(console.inputHandle, EnableProcessedInput | EnableLineInput)) + { + throw LastWin32("SetConsoleMode failed for private console input."); + } + if (!SetHandleInformation( + console.inputHandle, + HandleFlagInherit, + HandleFlagInherit + )) + { + throw LastWin32("Unable to make private console input inheritable."); + } + if (!FlushConsoleInputBuffer(console.inputHandle)) + { + throw LastWin32("FlushConsoleInputBuffer failed."); + } + console.QueueEndOfInput(); + return console; + } + catch + { + console.Dispose(); + throw; + } + } + + public void Dispose() + { + if (inputHandle != IntPtr.Zero) + { + CloseHandle(inputHandle); + inputHandle = IntPtr.Zero; + } + if (ownsConsole) + { + FreeConsole(); + ownsConsole = false; + } + } + + private void QueueEndOfInput() + { + // Why: Win32 console EOF is Ctrl+Z followed by Enter in cooked input mode; queue it before + // child creation so the SSH input worker cannot race launcher startup. + InputRecord[] records = new InputRecord[] + { + CreateKeyRecord(true, VirtualKeyZ, '\u001a', LeftCtrlPressed), + CreateKeyRecord(false, VirtualKeyZ, '\u001a', LeftCtrlPressed), + CreateKeyRecord(true, VirtualKeyReturn, '\r', 0), + CreateKeyRecord(false, VirtualKeyReturn, '\r', 0) + }; + uint written; + if (!WriteConsoleInput(inputHandle, records, (uint)records.Length, out written)) + { + throw LastWin32("WriteConsoleInputW failed."); + } + if (written != (uint)records.Length) + { + throw new Win32Exception("WriteConsoleInputW queued an incomplete EOF sequence."); + } + } + + private void DetachFromInheritedConsole() + { + if (!FreeConsole()) + { + int error = Marshal.GetLastWin32Error(); + if (error != ErrorInvalidHandle) + { + throw new Win32Exception(error, "FreeConsole failed."); + } + } + } + + private static void HideConsoleWindow() + { + IntPtr window = GetConsoleWindow(); + if (window == IntPtr.Zero) + { + return; + } + ShowWindow(window, SwHide); + if (IsWindowVisible(window)) + { + throw new Win32Exception("The private SSH console window remained visible."); + } + } + + private static InputRecord CreateKeyRecord( + bool keyDown, + ushort virtualKey, + char character, + uint controlState + ) + { + InputRecord record = new InputRecord(); + record.EventType = KeyEvent; + record.KeyEvent = new KeyEventRecord(); + record.KeyEvent.KeyDown = keyDown; + record.KeyEvent.RepeatCount = 1; + record.KeyEvent.VirtualKeyCode = virtualKey; + record.KeyEvent.VirtualScanCode = (ushort)MapVirtualKey(virtualKey, MapVirtualKeyToScanCode); + record.KeyEvent.UnicodeChar = character; + record.KeyEvent.ControlKeyState = controlState; + return record; + } + + private static Win32Exception LastWin32(string message) + { + return new Win32Exception(Marshal.GetLastWin32Error(), message); + } + + [StructLayout(LayoutKind.Explicit, CharSet = CharSet.Unicode)] + private struct InputRecord + { + [FieldOffset(0)] internal short EventType; + [FieldOffset(4)] internal KeyEventRecord KeyEvent; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct KeyEventRecord + { + [MarshalAs(UnmanagedType.Bool)] internal bool KeyDown; + internal ushort RepeatCount; + internal ushort VirtualKeyCode; + internal ushort VirtualScanCode; + internal char UnicodeChar; + internal uint ControlKeyState; + } + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool AllocConsole(); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool FreeConsole(); + + [DllImport("kernel32.dll")] + private static extern IntPtr GetConsoleWindow(); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool ShowWindow(IntPtr window, int command); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool IsWindowVisible(IntPtr window); + + [DllImport( + "kernel32.dll", + EntryPoint = "CreateFileW", + CharSet = CharSet.Unicode, + SetLastError = true + )] + private static extern IntPtr CreateFile( + string fileName, + uint desiredAccess, + uint shareMode, + IntPtr securityAttributes, + uint creationDisposition, + uint flagsAndAttributes, + IntPtr templateFile + ); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetConsoleMode(IntPtr consoleInput, uint mode); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool FlushConsoleInputBuffer(IntPtr consoleInput); + + [DllImport("kernel32.dll", EntryPoint = "WriteConsoleInputW", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool WriteConsoleInput( + IntPtr consoleInput, + [In] InputRecord[] records, + uint recordCount, + out uint recordsWritten + ); + + [DllImport("user32.dll")] + private static extern uint MapVirtualKey(uint code, uint mapType); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetHandleInformation(IntPtr handle, uint mask, uint flags); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseHandle(IntPtr handle); +} diff --git a/native/windows-ssh-no-input-launcher/WindowsAnonymousPipeSet.cs b/native/windows-ssh-no-input-launcher/WindowsSshChildIo.cs similarity index 86% rename from native/windows-ssh-no-input-launcher/WindowsAnonymousPipeSet.cs rename to native/windows-ssh-no-input-launcher/WindowsSshChildIo.cs index 0a6efc8bd1f..a7d59c19254 100644 --- a/native/windows-ssh-no-input-launcher/WindowsAnonymousPipeSet.cs +++ b/native/windows-ssh-no-input-launcher/WindowsSshChildIo.cs @@ -6,12 +6,14 @@ using System.Threading; using Microsoft.Win32.SafeHandles; -internal sealed class WindowsAnonymousPipeSet : IDisposable +internal sealed class WindowsSshChildIo : IDisposable { private const uint HandleFlagInherit = 0x00000001; private const int PumpCompletionTimeoutMilliseconds = 5000; - private IntPtr stdinRead; + private readonly IntPtr stdinRead; + private readonly Stream stdoutDestination; + private readonly Stream stderrDestination; private IntPtr stdoutWrite; private IntPtr stderrWrite; private SafeFileHandle stdoutRead; @@ -21,8 +23,15 @@ internal sealed class WindowsAnonymousPipeSet : IDisposable private Exception pumpFailure; private readonly ManualResetEvent pumpFailureSignal = new ManualResetEvent(false); - private WindowsAnonymousPipeSet() + private WindowsSshChildIo( + IntPtr stdinRead, + Stream stdoutDestination, + Stream stderrDestination + ) { + this.stdinRead = stdinRead; + this.stdoutDestination = stdoutDestination; + this.stderrDestination = stderrDestination; } internal IntPtr StdinRead { get { return stdinRead; } } @@ -33,21 +42,23 @@ internal IntPtr PumpFailureHandle get { return pumpFailureSignal.SafeWaitHandle.DangerousGetHandle(); } } - internal static WindowsAnonymousPipeSet Create() + internal static WindowsSshChildIo Create( + IntPtr stdinRead, + Stream stdoutDestination, + Stream stderrDestination + ) { - WindowsAnonymousPipeSet pipes = new WindowsAnonymousPipeSet(); + WindowsSshChildIo pipes = new WindowsSshChildIo( + stdinRead, + stdoutDestination, + stderrDestination + ); try { SecurityAttributes security = new SecurityAttributes(); security.Length = Marshal.SizeOf(typeof(SecurityAttributes)); security.InheritHandle = true; - IntPtr stdinWrite; - CreateCheckedPipe(out pipes.stdinRead, out stdinWrite, security); - SetNonInheritable(stdinWrite); - // Why: Win32-OpenSSH must see EOF before CreateProcessW, not after startup. - CloseHandleChecked(stdinWrite); - IntPtr stdoutReadHandle; CreateCheckedPipe(out stdoutReadHandle, out pipes.stdoutWrite, security); SetNonInheritable(stdoutReadHandle); @@ -68,7 +79,6 @@ internal static WindowsAnonymousPipeSet Create() internal void CloseChildEndsInParent() { - CloseOwnedHandle(ref stdinRead); CloseOwnedHandle(ref stdoutWrite); CloseOwnedHandle(ref stderrWrite); } @@ -77,11 +87,11 @@ internal void StartPumps() { SafeFileHandle stdoutHandle = stdoutRead; stdoutRead = null; - stdoutPump = StartPump(stdoutHandle, Console.OpenStandardOutput(), "stdout"); + stdoutPump = StartPump(stdoutHandle, stdoutDestination, "stdout"); SafeFileHandle stderrHandle = stderrRead; stderrRead = null; - stderrPump = StartPump(stderrHandle, Console.OpenStandardError(), "stderr"); + stderrPump = StartPump(stderrHandle, stderrDestination, "stderr"); } internal void CompletePumps() @@ -208,14 +218,6 @@ private static void SetNonInheritable(IntPtr handle) } } - private static void CloseHandleChecked(IntPtr handle) - { - if (!CloseHandle(handle)) - { - throw new Win32Exception(Marshal.GetLastWin32Error(), "CloseHandle failed."); - } - } - private static void CloseOwnedHandle(ref IntPtr handle) { if (handle != IntPtr.Zero) diff --git a/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs b/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs index c7327e593b1..e24c83a0b2b 100644 --- a/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs +++ b/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs @@ -8,7 +8,6 @@ internal static class WindowsSshChildProcess { private const uint CreateSuspended = 0x00000004; private const uint ExtendedStartupInfoPresent = 0x00080000; - private const uint CreateNoWindow = 0x08000000; private const uint StartfUseStdHandles = 0x00000100; private const uint JobObjectLimitKillOnJobClose = 0x00002000; private const int JobObjectExtendedLimitInformationClass = 9; @@ -19,7 +18,14 @@ internal static class WindowsSshChildProcess internal static int Run(string executablePath, string[] args) { - using (WindowsAnonymousPipeSet pipes = WindowsAnonymousPipeSet.Create()) + Stream launcherStdout = Console.OpenStandardOutput(); + Stream launcherStderr = Console.OpenStandardError(); + using (WindowsPrivateConsoleInput console = WindowsPrivateConsoleInput.Create()) + using (WindowsSshChildIo pipes = WindowsSshChildIo.Create( + console.InputHandle, + launcherStdout, + launcherStderr + )) { IntPtr job = IntPtr.Zero; IntPtr attributeList = IntPtr.Zero; @@ -32,7 +38,9 @@ internal static int Run(string executablePath, string[] args) job = CreateKillOnCloseJob(); StartupInfoEx startup = CreateStartupInfo(pipes, out attributeList, out inheritedHandles); StringBuilder commandLine = WindowsCommandLine.Build(executablePath, args); - uint flags = CreateSuspended | ExtendedStartupInfoPresent | CreateNoWindow; + // Why: the SSH child must share the hidden private console for CONIN$ to remain a + // real console handle; CREATE_NO_WINDOW would detach it from that console. + uint flags = CreateSuspended | ExtendedStartupInfoPresent; if (!CreateProcess( executablePath, commandLine, @@ -125,7 +133,7 @@ out process } private static StartupInfoEx CreateStartupInfo( - WindowsAnonymousPipeSet pipes, + WindowsSshChildIo pipes, out IntPtr attributeList, out IntPtr inheritedHandles ) diff --git a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts index b11cb29a9f6..f7f8730ef7e 100644 --- a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +++ b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts @@ -7,7 +7,7 @@ import type { SshTarget } from '../../shared/ssh-types' import { buildSshArgs } from './system-ssh-args' import { findSystemSsh } from './system-ssh-binary' -type DiagnosticMode = 'private-pipe-launcher-no-n' +type DiagnosticMode = 'private-console-launcher-no-n' type DiagnosticResult = { mode: DiagnosticMode @@ -48,10 +48,10 @@ async function runDiagnostic(): Promise { if (!sshPath) { throw new Error('Native Windows OpenSSH client is unavailable') } - const mode: DiagnosticMode = 'private-pipe-launcher-no-n' + const mode: DiagnosticMode = 'private-console-launcher-no-n' const startedAt = performance.now() - // Why: the disconnected launcher creates private child pipes so Win32-OpenSSH receives EOF - // before CreateProcessW without depending on Node's Windows named-pipe behavior. + // Why: redirected stdio hangs in Win32-OpenSSH even with private pipes; this diagnostic gives + // only stdin a hidden real-console boundary with EOF queued before CreateProcessW. const child = spawn( launcherPath as string, [sshPath, ...buildSshArgs(createTarget()), 'echo ORCA-SYSTEM-SSH-OK'], @@ -138,16 +138,16 @@ async function runDiagnostic(): Promise { describe.skipIf(!hasLiveInput)('Windows OpenSSH no-input child-handle diagnostic', () => { it( - 'qualifies private-pipe stdin EOF without the Windows null-input option', + 'qualifies private-console stdin EOF without the Windows null-input option', { timeout: 15_000 }, async () => { expect(process.platform).toBe('win32') - const privatePipeLauncherNoN = await runDiagnostic() + const privateConsoleLauncherNoN = await runDiagnostic() console.log( - `ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ privatePipeLauncherNoN })}` + `ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ privateConsoleLauncherNoN })}` ) - expect(privatePipeLauncherNoN.success).toBe(true) + expect(privateConsoleLauncherNoN.success).toBe(true) } ) }) From 149a122b735659b47d7718f409d14e1ed2f947c2 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Thu, 16 Jul 2026 23:49:11 -0400 Subject: [PATCH 256/267] test(ssh): isolate Windows regular-file output Co-authored-by: Orca --- .../build-windows-ssh-no-input-launcher.mjs | 2 +- ...runtime-windows-no-input-launcher.test.mjs | 98 +++++-- ...elease-implementation-checklist-summary.md | 24 +- ...github-release-implementation-checklist.md | 97 ++++++- ...6-07-14-ssh-relay-github-release-plan.html | 14 +- .../WindowsBoundedOutputFiles.cs | 196 ++++++++++++++ .../WindowsSshChildIo.cs | 254 ------------------ .../WindowsSshChildProcess.cs | 128 +++++---- ...sh-system-no-input-windows-openssh.test.ts | 16 +- 9 files changed, 484 insertions(+), 345 deletions(-) create mode 100644 native/windows-ssh-no-input-launcher/WindowsBoundedOutputFiles.cs delete mode 100644 native/windows-ssh-no-input-launcher/WindowsSshChildIo.cs diff --git a/config/scripts/build-windows-ssh-no-input-launcher.mjs b/config/scripts/build-windows-ssh-no-input-launcher.mjs index 2e8b40bd1f6..3b0b0ff5ff7 100644 --- a/config/scripts/build-windows-ssh-no-input-launcher.mjs +++ b/config/scripts/build-windows-ssh-no-input-launcher.mjs @@ -14,7 +14,7 @@ const sourcePaths = [ 'OrcaSshNoInputLauncher.cs', 'WindowsCommandLine.cs', 'WindowsPrivateConsoleInput.cs', - 'WindowsSshChildIo.cs', + 'WindowsBoundedOutputFiles.cs', 'WindowsSshChildProcess.cs' ].map((name) => join(sourceRoot, name)) const outputPath = readArg('--output') ?? join(sourceRoot, '.build', 'orca-ssh-no-input.exe') diff --git a/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs b/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs index d4d26fa5319..7eabf2500f4 100644 --- a/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs +++ b/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs @@ -1,5 +1,5 @@ import { spawn, spawnSync } from 'node:child_process' -import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { performance } from 'node:perf_hooks' @@ -92,14 +92,20 @@ describe('Windows SSH no-input launcher', () => { it('keeps the artifact source scoped to the reviewed Win32 boundary', () => { const processSource = readFileSync(join(sourceRoot, 'WindowsSshChildProcess.cs'), 'utf8') const consoleSource = readFileSync(join(sourceRoot, 'WindowsPrivateConsoleInput.cs'), 'utf8') - const pipeSource = readFileSync(join(sourceRoot, 'WindowsSshChildIo.cs'), 'utf8') + const outputSource = readFileSync(join(sourceRoot, 'WindowsBoundedOutputFiles.cs'), 'utf8') expect(processSource).toContain('ProcThreadAttributeHandleList') expect(processSource).toContain('IntPtr.Size * 3') expect(processSource).toContain('CreateSuspended') expect(processSource).not.toContain('CreateNoWindow') expect(processSource).toContain('JobObjectLimitKillOnJobClose') - expect(processSource).toContain('WaitForMultipleObjects') + expect(processSource).toContain('WaitForSingleObject') + expect(processSource).toContain('OutputLimitPollMilliseconds = 50') + expect(processSource).toContain('JobSettlementTimeoutMilliseconds = 5000') + expect(processSource).toContain('TerminateJobObject') + expect(processSource).toContain('outputs.EnsureWithinLimits()') + expect(processSource).toContain('outputs.ReplayOutputs()') + expect(processSource).not.toContain('WaitForMultipleObjects') expect(processSource).toContain('if (processStarted && !assignedToJob)') expect(consoleSource).toContain('AllocConsole()') expect(consoleSource).toContain('"CONIN$"') @@ -113,8 +119,12 @@ describe('Windows SSH no-input launcher', () => { expect(processSource.indexOf('WindowsPrivateConsoleInput.Create()')).toBeLessThan( processSource.indexOf('CreateProcess(') ) - expect(pipeSource).toContain('PumpCompletionTimeoutMilliseconds') - expect(pipeSource).toContain('pumpFailureSignal.Set()') + expect(outputSource).toContain('MaxCapturedBytes = 16 * 1024 * 1024') + expect(outputSource).toContain('FileOptions.DeleteOnClose') + expect(outputSource).toContain('FileMode.CreateNew') + expect(outputSource).toContain('ReplayBufferBytes = 64 * 1024') + expect(outputSource).toContain('SetHandleInformation') + expect(processSource).toContain('outputs.CloseChildEndsInParent()') }) it.skipIf(process.platform === 'win32')( @@ -165,6 +175,26 @@ describe('Windows SSH no-input launcher', () => { expect(exited.status).toBe(37) }) + itWindows('fails closed above the output ceiling and removes capture files', () => { + const capturesBefore = listCaptureFiles() + const result = runLauncher('overflow-output') + + expect(result.status).toBe(1) + expect(result.stdout).toEqual(Buffer.alloc(0)) + expect(result.stderr.toString('utf8')).toContain( + 'SSH stdout exceeded the 16 MiB diagnostic capture limit.' + ) + expect(listCaptureFiles()).toEqual(capturesBefore) + }) + + itWindows('removes successful capture files after exact binary replay', () => { + const capturesBefore = listCaptureFiles() + const result = runLauncher('binary-output') + + expect(result.status).toBe(0) + expect(listCaptureFiles()).toEqual(capturesBefore) + }) + itWindows('does not leak an unrelated inheritable parent handle into the child', () => { const result = spawnSync(handleProbePath, [launcherPath], { cwd: fixtureRoot, @@ -182,17 +212,20 @@ describe('Windows SSH no-input launcher', () => { { timeout: 15_000 }, async () => { const startedAt = performance.now() - const child = spawn(launcherPath, [process.execPath, childFixturePath, 'hold'], { + const capturesBefore = listCaptureFiles() + const pidPath = join(fixtureRoot, 'held-child.pid') + const child = spawn(launcherPath, [process.execPath, childFixturePath, 'hold', pidPath], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true }) - const childPid = await readFirstLine(child.stdout, 5_000) + const childPid = await readPidFile(pidPath, 5_000) expect(Number.isInteger(childPid)).toBe(true) const closePromise = waitForClose(child, 5_000) expect(child.kill('SIGKILL')).toBe(true) await closePromise await waitForProcessExit(childPid, 5_000) + await waitForCaptureFiles(capturesBefore, 5_000) expect(performance.now() - startedAt).toBeLessThan(10_000) } ) @@ -204,10 +237,17 @@ function runLauncher(mode, args = [], options = {}) { encoding: null, timeout: 10_000, windowsHide: true, + maxBuffer: 32 * 1024 * 1024, ...options }) } +function listCaptureFiles() { + return readdirSync(tmpdir()) + .filter((name) => name.startsWith('orca-ssh-no-input-') && name.endsWith('.capture')) + .sort() +} + function findFrameworkCompiler(env) { const windowsDirectory = env.WINDIR ?? env.SystemRoot if (!windowsDirectory) { @@ -221,23 +261,26 @@ function findFrameworkCompiler(env) { ) } -function readFirstLine(stream, timeoutMs) { - return new Promise((resolveLine, reject) => { - let output = '' - const timeout = setTimeout( - () => reject(new Error('Timed out waiting for child PID.')), - timeoutMs - ) - stream.on('data', (chunk) => { - output += chunk.toString('utf8') - const newline = output.indexOf('\n') - if (newline >= 0) { - clearTimeout(timeout) - resolveLine(Number.parseInt(output.slice(0, newline), 10)) - } - }) - stream.on('error', reject) - }) +async function readPidFile(path, timeoutMs) { + const deadline = performance.now() + timeoutMs + while (performance.now() < deadline) { + if (existsSync(path)) { + return Number.parseInt(readFileSync(path, 'utf8'), 10) + } + await new Promise((resolveWait) => setTimeout(resolveWait, 25)) + } + throw new Error('Timed out waiting for child PID file.') +} + +async function waitForCaptureFiles(expected, timeoutMs) { + const deadline = performance.now() + timeoutMs + while (performance.now() < deadline) { + if (JSON.stringify(listCaptureFiles()) === JSON.stringify(expected)) { + return + } + await new Promise((resolveWait) => setTimeout(resolveWait, 25)) + } + throw new Error('Launcher capture files survived bounded cancellation cleanup.') } function waitForClose(child, timeoutMs) { @@ -278,8 +321,13 @@ if (mode === 'arguments-and-stdin') { process.stderr.write(Buffer.from([0xfe, 0x00, 0x7f, 0x0d, 0x0a])) } else if (mode === 'exit-code') { process.exit(Number.parseInt(process.argv[3], 10)) +} else if (mode === 'overflow-output') { + const chunk = Buffer.alloc(1024 * 1024, 0xa5) + for (let index = 0; index < 17; index += 1) { + process.stdout.write(chunk) + } } else if (mode === 'hold') { - process.stdout.write(String(process.pid) + '\n') + require('node:fs').writeFileSync(process.argv[3], String(process.pid)) setInterval(() => {}, 1000) } else { process.exit(99) 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 80118bfb76b..d50e5d6d771 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 @@ -10,7 +10,8 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected private- -console Windows diagnostic, 2026-07-16, Codex implementation owner.** +console plus bounded regular-output Windows diagnostic locally green; exact-head native proof +required, 2026-07-16, Codex implementation owner.** `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain @@ -188,6 +189,27 @@ full lint/reliability/max-lines, formatting, diff, and protected-resolver isolat this artifact-only package and require fresh x64/ARM64 compiler, console/EOF, launcher, live SSH, and exact teardown proof; no product, Beta, fallback, tuple, publication, default, or SignPath work is included. +Exact head `5841b12bc`, run `29552101635`, and native x64/ARM64 jobs +`87796673326`/`87796673353` prove the console prerequisites on both architectures: each passes 101 +files/860 runnable cases with 19 skips, legacy compilation, character-device `CONIN$`, zero-byte +cooked `ReadFile`, argv/binary/exit/handle-exclusion/cancellation, unchanged runtime reproducibility, +and full-size cache gates. The live client still times out after 8025.1553/8034.6943 milliseconds +with the same no-sentinel/no-exit lifecycle, and both exact teardowns catch locked `UsrClass.dat`. +`E-M6-WINDOWS-PRIVATE-CONSOLE-CI-RED-001` therefore falsifies stdin alone and leaves private output +pipes as the next isolated handle variable. Keep console EOF, replace only child stdout/stderr with +per-stream size-bounded delete-on-close regular files, replay bytes incrementally after exit, and +prove overflow/cleanup/cancellation plus live x64/ARM64 behavior. ConPTY and alternate SSH clients +remain later decisions; all production/Beta/fallback/tuple/publication/default/signing paths remain +untouched. +`E-M6-WINDOWS-PRIVATE-CONSOLE-REGULAR-OUTPUT-LOCAL-001` records that replacement locally green: +distinct parent-read and inheritable child-write regular-file handles avoid shared seek state; each +stream has a 16 MiB ceiling with 50 ms polling, delete-on-close plus explicit cleanup, and 64 KiB +post-exit binary replay after bounded job settlement. Syntax, focused 119/119, all 285 release +contracts, typecheck, full lint/reliability/max-lines, formatting, diff, and protected-resolver +isolation pass. Seven native +launcher cases plus live/full-size behavior remain honest local skips. Commit/push only this +runner-temp diagnostic and require fresh exact-head Windows x64/ARM64 compilation, overflow, +cleanup, cancellation, live lifecycle, and fixture-teardown proof. SignPath remains deferred. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown 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 f7376f6f840..051eb324023 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-16
    -Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — 2026-07-16, Codex implementation owner: disconnected private-console Windows diagnostic native proof**. `E-M6-WINDOWS-PRIVATE-CONSOLE-LOCAL-001` records the unpackaged, unsigned, runner-temp diagnostic locally green. Commit/push only this artifact-only package and require fresh Windows x64/ARM64 legacy-compiler, real-console qualification, pre-execution cooked EOF, argv/binary/exit/handle-exclusion/cancellation, live lifecycle, and exact teardown proof. Keep every capability disconnected from every production/default SSH path. Real Apple/SignPath rehearsals remain deferred.
    -Session checkpoint: **PRIVATE CONSOLE CONTRACTS LOCALLY GREEN / X64 AND ARM64 NATIVE PROOF REQUIRED — 2026-07-16, Codex implementation owner** — the launcher captures its caller output streams, detaches from any inherited console, allocates and hides a private console, queues Ctrl+Z plus Enter into `CONIN$` before child creation, and shares that console with the suspended/job-owned child while retaining private stdout/stderr pipes and the exact three-handle list. A dedicated native probe requires character-device stdin, successful `GetConsoleMode`, and zero-byte cooked `ReadFile`. Local focused 119/119 with seven native skips, release contracts 285/285 with five native skips, typecheck, full lint/reliability/max-lines, formatting, diff, and resolver isolation pass. No product caller, full-size transfer claim, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **Locally green / exact-head native proof required — 2026-07-16, Codex implementation owner: disconnected private-console plus bounded regular-output diagnostic**. `E-M6-WINDOWS-PRIVATE-CONSOLE-REGULAR-OUTPUT-LOCAL-001` records the one-variable runner-temp candidate locally green: proven private-console EOF, distinct bounded regular-file output handles, delete-on-close cleanup, bounded job settlement, and post-exit replay. Commit/push only this package and require x64/ARM64 legacy compilation, native contracts, live lifecycle, and exact teardown evidence. Every production/default SSH path remains disconnected. Real Apple/SignPath rehearsals remain deferred.
    +Session checkpoint: **PRIVATE CONSOLE EOF PROVEN / PRIVATE OUTPUT PIPES FALSIFIED / BOUNDED REGULAR OUTPUT ACTIVE — 2026-07-16, Codex implementation owner** — exact head `5841b12bc94ecd1027e507e4b741e09fcb0a093d`, run `29552101635`, and x64/ARM64 jobs `87796673326`/`87796673353` both pass 101 files/860 cases with 19 skips, legacy compilation, real-console qualification/EOF, argv/binary/exit/handle-exclusion/cancellation, unchanged full-runtime reproducibility, and full-size cache gates. Live diagnostics still time out after 8025.1553/8034.6943 milliseconds with ended stdout, closed channel, no sentinel/process exit, and zero stderr; both exact teardowns catch locked `UsrClass.dat`. All four non-Windows primary jobs and both Linux supplements pass. No product caller, full-size Windows transfer claim, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
    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) @@ -19807,6 +19807,90 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases wi Beta, tuple enablement, publication, default behavior, or signing. A RED native probe or live result must be recorded before selecting another mechanism such as ConPTY. +### E-M6-WINDOWS-PRIVATE-CONSOLE-CI-RED-001 — real-console EOF does not settle piped output lifecycle + +- Date/owner/head/run: 2026-07-16, Codex implementation owner; exact pushed head + `5841b12bc94ecd1027e507e4b741e09fcb0a093d`, artifact run `29552101635`, Windows x64 job + `87796673326` on `windows-2022`, and Windows ARM64 job `87796673353` on `windows-11-arm`. + GitHub records both jobs starting 2026-07-17 03:19Z and completing at 03:26Z/03:30Z. +- Native prerequisite proof: both architectures compile the launcher and direct console-EOF probe + with the legacy .NET Framework compiler. Each artifact contract step passes 101 files, 860 + runnable cases, and 19 declared skips, including character-device stdin, successful + `GetConsoleMode`, zero-byte cooked `ReadFile`, exact argv, binary stdout/stderr, exit code, + unrelated-handle exclusion, job cancellation, and bounded settlement. Both unchanged runtimes + then pass build-twice identity, closure, smoke, and full-size cache gates. +- X64 live result: `elapsedMs=8025.1553`, `success=false`, `timedOut=true`, `sentinel=false`, + `stdoutEnded=true`, `processExit=null`, `channelClosed=true`, `closeCode=null`, and + `stderrBytes=0`. +- ARM64 live result: `elapsedMs=8034.694300000001` with the same remaining fields. Both official + server logs prove authentication and remote command-session closure; both clients require + bounded forced termination. Neither result authorizes the unchanged product full-size test. +- Teardown result: x64 fails to unload fixture SID + `S-1-5-21-4137868374-391603265-4009536220-1003_Classes`; ARM64 independently fails SID + `S-1-5-21-1882319117-3219095328-2125279949-1001_Classes`. Both catch locked + `C:\Users\orca_ssh_fixture\AppData\Local\Microsoft\Windows\UsrClass.dat` and a retained profile + directory. The workflow stops before upload. +- Preserved evidence: Linux arm64/x64 jobs `87796673305`/`87796673330`, Darwin arm64/x64 jobs + `87796673368`/`87796673381`, and Linux oldest-userland supplements `87797349637`/`87797349630` + all pass on the same exact head. +- Interpretation: stdin is now independently proven to be a real console handle whose cooked read + returns EOF before the SSH child begins, so another stdin-only mechanism is not justified. + Private stdout/stderr pipes remain the unisolated redirected-handle variable. Upstream issue #1330 + reports a redirected-stdio hang and separately records a working regular-file stdout/stderr form; + qualify that narrower output-handle boundary before ConPTY or another SSH client. +- Next bounded diagnostic: retain the private console and EOF sequence, but give the child two + unique regular output files opened with delete-on-close semantics and exact inheritance. While + waiting, enforce an explicit per-stream byte ceiling and bounded polling/cancellation; only after + process exit replay each file incrementally to the launcher's original binary stdout/stderr. + Native tests must prove exact binary bytes, overflow failure, file-count/cleanup, cancellation, + exit propagation, and no unrelated handle inheritance on x64/ARM64. Keep the artifact runner- + temp-only and disconnected from product, Beta, fallback, tuple, publication, default, and signing. + +### E-M6-WINDOWS-PRIVATE-CONSOLE-REGULAR-OUTPUT-LOCAL-001 — bounded regular-output candidate is locally contract-green + +- Date/owner/base/runner: 2026-07-16, Codex implementation owner; uncommitted artifact-only package + atop exact pushed private-console RED head `5841b12bc94ecd1027e507e4b741e09fcb0a093d` on Apple arm64, + macOS 26.2 build 25C56, Node 26.5.0, and pnpm 10.24.0. +- Artifact boundary: the proven private-console stdin/EOF path is unchanged. Each child output now + uses a unique regular file with a distinct parent read handle and child write handle so inherited + writes cannot move the reader's seek pointer. Only the two child writers are inheritable; the + parent closes its copies immediately after `CreateProcessW`. Delete-on-close plus explicit + disposal cleanup owns both paths. The launcher polls the child every 50 ms, fails closed if + either file exceeds 16 MiB, terminates and waits up to five seconds for the owned job to release + descendant writers after child exit, then replays exact binary bytes in 64 KiB chunks. The + launcher remains runner-temp-only and unpackaged. +- Native contract design: exact argv/console EOF/binary output/exit/handle exclusion remain. New + Windows cases require an output above 16 MiB to return failure without replay, successful and + overflow paths to leave the pre-run capture-file set unchanged, and cancellation to learn the + held child PID out of band before proving job termination and capture cleanup. Seven launcher + native cases plus the live and full-size suites are honestly skipped on the local Apple host. +- Syntax/focused evidence: `node --check config/scripts/build-windows-ssh-no-input-launcher.mjs` + and `node --check config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs` pass. + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 + src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts + src/main/ssh/ssh-system-no-input-windows-openssh.test.ts + src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts + config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs + config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases with nine declared + native/live skips in 2.59 seconds real; maximum RSS is 171,491,328 bytes and peak footprint is + 97,064,696 bytes. +- Release contracts: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 + --reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 51 files and 285 runnable cases + with seven declared Windows skips in 13.29 seconds. +- Static gates: `/usr/bin/time -l pnpm typecheck` passes in 2.86 seconds real with + 1,247,576,064-byte maximum RSS and 97,343,320-byte peak footprint. `/usr/bin/time -l pnpm lint` + passes in 16.69 seconds real with the 26 existing warnings, all 41 reliability gates, and the + 355-entry max-lines ratchet; maximum RSS is 2,044,805,120 bytes and peak footprint is 97,163,000 + bytes. Purpose-file formatting and `git diff --check` pass; + `git diff --exit-code HEAD -- src/main/ssh/ssh-remote-node-resolution.ts + src/main/ssh/ssh-remote-node-resolution.test.ts` is empty. +- Residual boundary: local source contracts do not prove legacy .NET Framework compilation, + delete-on-close behavior, overflow settlement, binary replay, job cancellation, live OpenSSH, or + exact fixture teardown on Windows. Commit and push only this disconnected package, then require + fresh exact-head x64 and ARM64 evidence before interpreting the live result. No product caller, + Beta option, fallback, full-size Windows transfer claim, tuple, publication, default, signing, or + SignPath behavior is authorized. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -19865,10 +19949,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the private real-console stdin diagnostic recorded by -`E-M6-WINDOWS-PRIVATE-CONSOLE-LOCAL-001`, then require exact-head Windows x64 and ARM64 legacy- -compiler, console qualification/EOF, argv/binary/exit/handle-exclusion/cancellation, live -diagnostic, and exact profile/directory teardown evidence. Do not +Commit and push only the private-console plus bounded regular-output diagnostic recorded by +`E-M6-WINDOWS-PRIVATE-CONSOLE-REGULAR-OUTPUT-LOCAL-001`, then require exact-head Windows x64 and +ARM64 legacy-compiler, console qualification/EOF, bounded binary output/overflow/cleanup, +argv/exit/handle-exclusion/cancellation, live diagnostic, and exact profile/directory teardown +evidence. Do not change the production Windows handle or check the broad Milestone 6 Windows system-SSH item until both architectures select the same console-independent strategy. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index ec3a4995eed..6f1c2e39203 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1665,11 +1665,17 @@

    Prove production behavior, failure behavior, and rollback behavior.

    evidence proves that launcher compiles and passes all of those contracts, but Win32-OpenSSH still remains alive after authenticated remote command-session closure. This falsifies Node's pipe type and broad inherited-handle leakage. The - next artifact-only diagnostic uses a hidden launcher-owned private real console, - queues console EOF before child creation, and shares that console with the SSH + next artifact-only diagnostic used a hidden launcher-owned private real console, + queued console EOF before child creation, and shared that console with the SSH child while retaining private output pipes, exact handle inheritance, job - ownership, and bounded cancellation. Build and live-test it on native x64/arm64 - before packaging, signing, or adding a product caller. POSIX retains ignored + ownership, and bounded cancellation. Native x64/arm64 prove the console handle + and zero-byte cooked EOF but the live client still hangs, falsifying stdin alone. + The next one-variable diagnostic retains that console stdin and replaces only + child stdout/stderr pipes with unique delete-on-close regular files under an + explicit per-stream byte ceiling, bounded polling/cancellation, incremental + post-exit replay, and exact cleanup. Build and live-test it on native x64/arm64 + before considering ConPTY, another SSH client, packaging, signing, or a product + caller. POSIX retains ignored stdin plus -n; fixture-owned Windows session processes settle before profile hives are boundedly unloaded and native profile deletion begins; MaxSessions=1; interrupted upload; stale lock diff --git a/native/windows-ssh-no-input-launcher/WindowsBoundedOutputFiles.cs b/native/windows-ssh-no-input-launcher/WindowsBoundedOutputFiles.cs new file mode 100644 index 00000000000..e87d8c5fa75 --- /dev/null +++ b/native/windows-ssh-no-input-launcher/WindowsBoundedOutputFiles.cs @@ -0,0 +1,196 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +internal sealed class WindowsBoundedOutputFiles : IDisposable +{ + private const uint HandleFlagInherit = 0x00000001; + internal const long MaxCapturedBytes = 16 * 1024 * 1024; + private const int ReplayBufferBytes = 64 * 1024; + + private readonly IntPtr stdinRead; + private readonly Stream stdoutDestination; + private readonly Stream stderrDestination; + private FileStream stdoutCapture; + private FileStream stderrCapture; + private FileStream stdoutWriter; + private FileStream stderrWriter; + private string stdoutPath; + private string stderrPath; + + private WindowsBoundedOutputFiles( + IntPtr stdinRead, + Stream stdoutDestination, + Stream stderrDestination + ) + { + this.stdinRead = stdinRead; + this.stdoutDestination = stdoutDestination; + this.stderrDestination = stderrDestination; + } + + internal IntPtr StdinRead { get { return stdinRead; } } + internal IntPtr StdoutWrite + { + get { return stdoutWriter.SafeFileHandle.DangerousGetHandle(); } + } + internal IntPtr StderrWrite + { + get { return stderrWriter.SafeFileHandle.DangerousGetHandle(); } + } + + internal static WindowsBoundedOutputFiles Create( + IntPtr stdinRead, + Stream stdoutDestination, + Stream stderrDestination + ) + { + WindowsBoundedOutputFiles outputs = new WindowsBoundedOutputFiles( + stdinRead, + stdoutDestination, + stderrDestination + ); + try + { + outputs.stdoutCapture = CreateCaptureFile("stdout", out outputs.stdoutPath); + outputs.stdoutWriter = CreateChildWriter(outputs.stdoutPath, "stdout"); + outputs.stderrCapture = CreateCaptureFile("stderr", out outputs.stderrPath); + outputs.stderrWriter = CreateChildWriter(outputs.stderrPath, "stderr"); + return outputs; + } + catch + { + outputs.Dispose(); + throw; + } + } + + internal void CloseChildEndsInParent() + { + CloseFile(ref stdoutWriter); + CloseFile(ref stderrWriter); + } + + internal void EnsureWithinLimits() + { + EnsureWithinLimit(stdoutCapture, "stdout"); + EnsureWithinLimit(stderrCapture, "stderr"); + } + + internal void ReplayOutputs() + { + EnsureWithinLimits(); + Replay(stdoutCapture, stdoutDestination, "stdout"); + Replay(stderrCapture, stderrDestination, "stderr"); + } + + public void Dispose() + { + CloseChildEndsInParent(); + CloseFile(ref stdoutCapture); + CloseFile(ref stderrCapture); + DeleteCaptureFile(stdoutPath); + DeleteCaptureFile(stderrPath); + } + + private static FileStream CreateCaptureFile(string streamName, out string path) + { + path = Path.Combine( + Path.GetTempPath(), + "orca-ssh-no-input-" + Guid.NewGuid().ToString("N") + "." + streamName + ".capture" + ); + return new FileStream( + path, + FileMode.CreateNew, + FileAccess.ReadWrite, + FileShare.Read | FileShare.Write | FileShare.Delete, + ReplayBufferBytes, + FileOptions.SequentialScan + ); + } + + private static FileStream CreateChildWriter(string path, string streamName) + { + FileStream writer = new FileStream( + path, + FileMode.Open, + FileAccess.Write, + FileShare.Read | FileShare.Write | FileShare.Delete, + ReplayBufferBytes, + FileOptions.DeleteOnClose | FileOptions.SequentialScan + ); + try + { + if (!SetHandleInformation( + writer.SafeFileHandle.DangerousGetHandle(), + HandleFlagInherit, + HandleFlagInherit + )) + { + throw LastWin32("Unable to make " + streamName + " capture inheritable."); + } + return writer; + } + catch + { + writer.Dispose(); + throw; + } + } + + private static void EnsureWithinLimit(FileStream capture, string streamName) + { + if (capture.Length > MaxCapturedBytes) + { + throw new IOException( + "SSH " + streamName + " exceeded the 16 MiB diagnostic capture limit." + ); + } + } + + private static void Replay(FileStream capture, Stream destination, string streamName) + { + capture.Position = 0; + byte[] buffer = new byte[ReplayBufferBytes]; + long replayed = 0; + int read; + while ((read = capture.Read(buffer, 0, buffer.Length)) > 0) + { + replayed += read; + if (replayed > MaxCapturedBytes) + { + throw new IOException( + "SSH " + streamName + " changed beyond its 16 MiB capture limit." + ); + } + destination.Write(buffer, 0, read); + } + destination.Flush(); + } + + private static void CloseFile(ref FileStream file) + { + if (file != null) + { + file.Dispose(); + file = null; + } + } + + private static void DeleteCaptureFile(string path) + { + if (!String.IsNullOrEmpty(path) && File.Exists(path)) + { + File.Delete(path); + } + } + + private static Exception LastWin32(string message) + { + return new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), message); + } + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetHandleInformation(IntPtr handle, uint mask, uint flags); +} diff --git a/native/windows-ssh-no-input-launcher/WindowsSshChildIo.cs b/native/windows-ssh-no-input-launcher/WindowsSshChildIo.cs deleted file mode 100644 index a7d59c19254..00000000000 --- a/native/windows-ssh-no-input-launcher/WindowsSshChildIo.cs +++ /dev/null @@ -1,254 +0,0 @@ -using System; -using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using System.Threading; -using Microsoft.Win32.SafeHandles; - -internal sealed class WindowsSshChildIo : IDisposable -{ - private const uint HandleFlagInherit = 0x00000001; - private const int PumpCompletionTimeoutMilliseconds = 5000; - - private readonly IntPtr stdinRead; - private readonly Stream stdoutDestination; - private readonly Stream stderrDestination; - private IntPtr stdoutWrite; - private IntPtr stderrWrite; - private SafeFileHandle stdoutRead; - private SafeFileHandle stderrRead; - private Thread stdoutPump; - private Thread stderrPump; - private Exception pumpFailure; - private readonly ManualResetEvent pumpFailureSignal = new ManualResetEvent(false); - - private WindowsSshChildIo( - IntPtr stdinRead, - Stream stdoutDestination, - Stream stderrDestination - ) - { - this.stdinRead = stdinRead; - this.stdoutDestination = stdoutDestination; - this.stderrDestination = stderrDestination; - } - - internal IntPtr StdinRead { get { return stdinRead; } } - internal IntPtr StdoutWrite { get { return stdoutWrite; } } - internal IntPtr StderrWrite { get { return stderrWrite; } } - internal IntPtr PumpFailureHandle - { - get { return pumpFailureSignal.SafeWaitHandle.DangerousGetHandle(); } - } - - internal static WindowsSshChildIo Create( - IntPtr stdinRead, - Stream stdoutDestination, - Stream stderrDestination - ) - { - WindowsSshChildIo pipes = new WindowsSshChildIo( - stdinRead, - stdoutDestination, - stderrDestination - ); - try - { - SecurityAttributes security = new SecurityAttributes(); - security.Length = Marshal.SizeOf(typeof(SecurityAttributes)); - security.InheritHandle = true; - - IntPtr stdoutReadHandle; - CreateCheckedPipe(out stdoutReadHandle, out pipes.stdoutWrite, security); - SetNonInheritable(stdoutReadHandle); - pipes.stdoutRead = new SafeFileHandle(stdoutReadHandle, true); - - IntPtr stderrReadHandle; - CreateCheckedPipe(out stderrReadHandle, out pipes.stderrWrite, security); - SetNonInheritable(stderrReadHandle); - pipes.stderrRead = new SafeFileHandle(stderrReadHandle, true); - return pipes; - } - catch - { - pipes.Dispose(); - throw; - } - } - - internal void CloseChildEndsInParent() - { - CloseOwnedHandle(ref stdoutWrite); - CloseOwnedHandle(ref stderrWrite); - } - - internal void StartPumps() - { - SafeFileHandle stdoutHandle = stdoutRead; - stdoutRead = null; - stdoutPump = StartPump(stdoutHandle, stdoutDestination, "stdout"); - - SafeFileHandle stderrHandle = stderrRead; - stderrRead = null; - stderrPump = StartPump(stderrHandle, stderrDestination, "stderr"); - } - - internal void CompletePumps() - { - Stopwatch deadline = Stopwatch.StartNew(); - if (!JoinPump(stdoutPump, deadline) || !JoinPump(stderrPump, deadline)) - { - throw new TimeoutException("SSH output pumps did not settle within 5 seconds."); - } - ThrowIfPumpFailed(); - } - - internal void ThrowIfPumpFailed() - { - Exception failure; - lock (this) - { - failure = pumpFailure; - } - if (failure != null) - { - throw new IOException("Unable to relay SSH output.", failure); - } - } - - public void Dispose() - { - CloseChildEndsInParent(); - if (stdoutRead != null) - { - stdoutRead.Dispose(); - stdoutRead = null; - } - if (stderrRead != null) - { - stderrRead.Dispose(); - stderrRead = null; - } - Stopwatch deadline = Stopwatch.StartNew(); - JoinPump(stdoutPump, deadline); - JoinPump(stderrPump, deadline); - pumpFailureSignal.Dispose(); - } - - private Thread StartPump(SafeFileHandle sourceHandle, Stream destination, string name) - { - Thread thread = new Thread(delegate() - { - try - { - using (FileStream source = new FileStream(sourceHandle, FileAccess.Read, 4096, false)) - { - source.CopyTo(destination); - destination.Flush(); - } - } - catch (Exception error) - { - RecordPumpFailure(error); - } - }); - thread.Name = "Orca SSH " + name + " pump"; - thread.IsBackground = true; - try - { - thread.Start(); - return thread; - } - catch - { - sourceHandle.Dispose(); - throw; - } - } - - private void RecordPumpFailure(Exception error) - { - lock (this) - { - if (pumpFailure == null) - { - pumpFailure = error; - } - } - try - { - pumpFailureSignal.Set(); - } - catch (ObjectDisposedException) - { - // The launcher is already leaving after its bounded disposal wait. - } - } - - private static bool JoinPump(Thread pump, Stopwatch deadline) - { - if (pump == null) - { - return true; - } - int remaining = PumpCompletionTimeoutMilliseconds - (int)deadline.ElapsedMilliseconds; - return remaining > 0 && pump.Join(remaining); - } - - private static void CreateCheckedPipe( - out IntPtr readHandle, - out IntPtr writeHandle, - SecurityAttributes security - ) - { - if (!CreatePipe(out readHandle, out writeHandle, ref security, 0)) - { - throw new Win32Exception(Marshal.GetLastWin32Error(), "CreatePipe failed."); - } - } - - private static void SetNonInheritable(IntPtr handle) - { - if (!SetHandleInformation(handle, HandleFlagInherit, 0)) - { - int error = Marshal.GetLastWin32Error(); - CloseHandle(handle); - throw new Win32Exception(error, "SetHandleInformation failed."); - } - } - - private static void CloseOwnedHandle(ref IntPtr handle) - { - if (handle != IntPtr.Zero) - { - CloseHandle(handle); - handle = IntPtr.Zero; - } - } - - [StructLayout(LayoutKind.Sequential)] - private struct SecurityAttributes - { - internal int Length; - internal IntPtr SecurityDescriptor; - [MarshalAs(UnmanagedType.Bool)] internal bool InheritHandle; - } - - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool CreatePipe( - out IntPtr readPipe, - out IntPtr writePipe, - ref SecurityAttributes pipeAttributes, - uint size - ); - - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool SetHandleInformation(IntPtr handle, uint mask, uint flags); - - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool CloseHandle(IntPtr handle); -} diff --git a/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs b/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs index e24c83a0b2b..493a1c4f94a 100644 --- a/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs +++ b/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs @@ -12,16 +12,18 @@ internal static class WindowsSshChildProcess private const uint JobObjectLimitKillOnJobClose = 0x00002000; private const int JobObjectExtendedLimitInformationClass = 9; private const int ProcThreadAttributeHandleList = 0x00020002; - private const uint Infinite = 0xffffffff; private const uint WaitObject0 = 0x00000000; private const uint WaitFailed = 0xffffffff; + private const uint WaitTimeout = 0x00000102; + private const uint OutputLimitPollMilliseconds = 50; + private const uint JobSettlementTimeoutMilliseconds = 5000; internal static int Run(string executablePath, string[] args) { Stream launcherStdout = Console.OpenStandardOutput(); Stream launcherStderr = Console.OpenStandardError(); using (WindowsPrivateConsoleInput console = WindowsPrivateConsoleInput.Create()) - using (WindowsSshChildIo pipes = WindowsSshChildIo.Create( + using (WindowsBoundedOutputFiles outputs = WindowsBoundedOutputFiles.Create( console.InputHandle, launcherStdout, launcherStderr @@ -36,7 +38,7 @@ internal static int Run(string executablePath, string[] args) try { job = CreateKillOnCloseJob(); - StartupInfoEx startup = CreateStartupInfo(pipes, out attributeList, out inheritedHandles); + StartupInfoEx startup = CreateStartupInfo(outputs, out attributeList, out inheritedHandles); StringBuilder commandLine = WindowsCommandLine.Build(executablePath, args); // Why: the SSH child must share the hidden private console for CONIN$ to remain a // real console handle; CREATE_NO_WINDOW would detach it from that console. @@ -57,53 +59,31 @@ out process throw LastWin32("CreateProcessW failed."); } processStarted = true; - pipes.CloseChildEndsInParent(); - + outputs.CloseChildEndsInParent(); if (!AssignProcessToJobObject(job, process.Process)) { throw LastWin32("AssignProcessToJobObject failed."); } assignedToJob = true; - pipes.StartPumps(); if (ResumeThread(process.Thread) == UInt32.MaxValue) { throw LastWin32("ResumeThread failed."); } - IntPtr[] waitHandles = new IntPtr[] { process.Process, pipes.PumpFailureHandle }; - uint waitResult = WaitForMultipleObjects( - (uint)waitHandles.Length, - waitHandles, - false, - Infinite - ); - if (waitResult == WaitObject0 + 1) - { - pipes.ThrowIfPumpFailed(); - throw new IOException("SSH output pump failed without an error detail."); - } - if (waitResult == WaitFailed) - { - throw LastWin32("WaitForMultipleObjects failed."); - } - if (waitResult != WaitObject0) - { - throw new Win32Exception("Unexpected child-process wait result."); - } + WaitForChildWithinOutputLimits(process.Process, outputs); uint exitCode; if (!GetExitCodeProcess(process.Process, out exitCode)) { throw LastWin32("GetExitCodeProcess failed."); } - // Why: descendants must not retain output writers and turn normal completion into - // an unbounded pump wait after the SSH process itself exits. - CloseOwnedHandle(ref job); - pipes.CompletePumps(); + // Why: descendants must release inherited writers before replay can trust the bytes. + TerminateAndSettleJob(ref job); + outputs.ReplayOutputs(); return unchecked((int)exitCode); } finally { - pipes.CloseChildEndsInParent(); + outputs.CloseChildEndsInParent(); if (process.Thread != IntPtr.Zero) { CloseHandle(process.Thread); @@ -117,8 +97,8 @@ out process } CloseHandle(process.Process); } - // Why: closing the owned job also kills a still-running SSH child on cancellation. - CloseOwnedHandle(ref job); + // Why: managed failures must not outlive their capture files or SSH child job. + TerminateAndSettleJob(ref job); if (attributeList != IntPtr.Zero) { DeleteProcThreadAttributeList(attributeList); @@ -133,7 +113,7 @@ out process } private static StartupInfoEx CreateStartupInfo( - WindowsSshChildIo pipes, + WindowsBoundedOutputFiles outputs, out IntPtr attributeList, out IntPtr inheritedHandles ) @@ -155,9 +135,9 @@ out IntPtr inheritedHandles } inheritedHandles = Marshal.AllocHGlobal(IntPtr.Size * 3); - Marshal.WriteIntPtr(inheritedHandles, 0, pipes.StdinRead); - Marshal.WriteIntPtr(inheritedHandles, IntPtr.Size, pipes.StdoutWrite); - Marshal.WriteIntPtr(inheritedHandles, IntPtr.Size * 2, pipes.StderrWrite); + Marshal.WriteIntPtr(inheritedHandles, 0, outputs.StdinRead); + Marshal.WriteIntPtr(inheritedHandles, IntPtr.Size, outputs.StdoutWrite); + Marshal.WriteIntPtr(inheritedHandles, IntPtr.Size * 2, outputs.StderrWrite); if (!UpdateProcThreadAttribute( attributeList, 0, @@ -174,9 +154,9 @@ out IntPtr inheritedHandles StartupInfoEx startup = new StartupInfoEx(); startup.StartupInfo.Size = Marshal.SizeOf(typeof(StartupInfoEx)); startup.StartupInfo.Flags = StartfUseStdHandles; - startup.StartupInfo.StandardInput = pipes.StdinRead; - startup.StartupInfo.StandardOutput = pipes.StdoutWrite; - startup.StartupInfo.StandardError = pipes.StderrWrite; + startup.StartupInfo.StandardInput = outputs.StdinRead; + startup.StartupInfo.StandardOutput = outputs.StdoutWrite; + startup.StartupInfo.StandardError = outputs.StderrWrite; startup.AttributeList = attributeList; return startup; } @@ -197,6 +177,31 @@ out IntPtr inheritedHandles } } + private static void WaitForChildWithinOutputLimits( + IntPtr process, + WindowsBoundedOutputFiles outputs + ) + { + while (true) + { + uint waitResult = WaitForSingleObject(process, OutputLimitPollMilliseconds); + if (waitResult == WaitObject0) + { + outputs.EnsureWithinLimits(); + return; + } + if (waitResult == WaitFailed) + { + throw LastWin32("WaitForSingleObject failed."); + } + if (waitResult != WaitTimeout) + { + throw new Win32Exception("Unexpected child-process wait result."); + } + outputs.EnsureWithinLimits(); + } + } + private static IntPtr CreateKillOnCloseJob() { IntPtr job = CreateJobObject(IntPtr.Zero, null); @@ -244,6 +249,38 @@ private static void CloseOwnedHandle(ref IntPtr handle) } } + private static void TerminateAndSettleJob(ref IntPtr job) + { + if (job == IntPtr.Zero) + { + return; + } + try + { + if (!TerminateJobObject(job, 1)) + { + throw LastWin32("TerminateJobObject failed."); + } + uint waitResult = WaitForSingleObject(job, JobSettlementTimeoutMilliseconds); + if (waitResult == WaitTimeout) + { + throw new TimeoutException("SSH child job did not settle within 5 seconds."); + } + if (waitResult == WaitFailed) + { + throw LastWin32("Waiting for the SSH child job failed."); + } + if (waitResult != WaitObject0) + { + throw new Win32Exception("Unexpected SSH child job wait result."); + } + } + finally + { + CloseOwnedHandle(ref job); + } + } + [StructLayout(LayoutKind.Sequential)] private struct StartupInfo { @@ -384,16 +421,15 @@ uint informationLength [return: MarshalAs(UnmanagedType.Bool)] private static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process); + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool TerminateJobObject(IntPtr job, uint exitCode); + [DllImport("kernel32.dll", SetLastError = true)] private static extern uint ResumeThread(IntPtr thread); [DllImport("kernel32.dll", SetLastError = true)] - private static extern uint WaitForMultipleObjects( - uint count, - [In] IntPtr[] handles, - [MarshalAs(UnmanagedType.Bool)] bool waitAll, - uint milliseconds - ); + private static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] diff --git a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts index f7f8730ef7e..8fe45e3a699 100644 --- a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +++ b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts @@ -7,7 +7,7 @@ import type { SshTarget } from '../../shared/ssh-types' import { buildSshArgs } from './system-ssh-args' import { findSystemSsh } from './system-ssh-binary' -type DiagnosticMode = 'private-console-launcher-no-n' +type DiagnosticMode = 'private-console-regular-output-launcher-no-n' type DiagnosticResult = { mode: DiagnosticMode @@ -48,10 +48,10 @@ async function runDiagnostic(): Promise { if (!sshPath) { throw new Error('Native Windows OpenSSH client is unavailable') } - const mode: DiagnosticMode = 'private-console-launcher-no-n' + const mode: DiagnosticMode = 'private-console-regular-output-launcher-no-n' const startedAt = performance.now() - // Why: redirected stdio hangs in Win32-OpenSSH even with private pipes; this diagnostic gives - // only stdin a hidden real-console boundary with EOF queued before CreateProcessW. + // Why: Win32-OpenSSH still hangs with proven console EOF plus piped output, so this diagnostic + // isolates stdout/stderr as bounded regular files without changing production transport. const child = spawn( launcherPath as string, [sshPath, ...buildSshArgs(createTarget()), 'echo ORCA-SYSTEM-SSH-OK'], @@ -138,16 +138,16 @@ async function runDiagnostic(): Promise { describe.skipIf(!hasLiveInput)('Windows OpenSSH no-input child-handle diagnostic', () => { it( - 'qualifies private-console stdin EOF without the Windows null-input option', + 'qualifies private-console EOF plus regular-file output without the Windows null-input option', { timeout: 15_000 }, async () => { expect(process.platform).toBe('win32') - const privateConsoleLauncherNoN = await runDiagnostic() + const privateConsoleRegularOutputLauncherNoN = await runDiagnostic() console.log( - `ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ privateConsoleLauncherNoN })}` + `ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ privateConsoleRegularOutputLauncherNoN })}` ) - expect(privateConsoleLauncherNoN.success).toBe(true) + expect(privateConsoleRegularOutputLauncherNoN.success).toBe(true) } ) }) From b24dda744892c2ee43a3fb345d889ba97adb4239 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 17 Jul 2026 00:07:14 -0400 Subject: [PATCH 257/267] test(ssh): capture hung Windows client phase Co-authored-by: Orca --- ...runtime-windows-no-input-launcher.test.mjs | 50 +++++++++--- ...elease-implementation-checklist-summary.md | 21 ++++- ...github-release-implementation-checklist.md | 80 +++++++++++++++++-- ...6-07-14-ssh-relay-github-release-plan.html | 11 ++- .../OrcaSshNoInputLauncher.cs | 31 +++++-- .../WindowsSshChildProcess.cs | 47 +++++++++-- ...sh-system-no-input-windows-openssh.test.ts | 25 ++++-- 7 files changed, 224 insertions(+), 41 deletions(-) diff --git a/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs b/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs index 7eabf2500f4..521d7bb7696 100644 --- a/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs +++ b/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs @@ -91,6 +91,7 @@ afterAll(() => { describe('Windows SSH no-input launcher', () => { it('keeps the artifact source scoped to the reviewed Win32 boundary', () => { const processSource = readFileSync(join(sourceRoot, 'WindowsSshChildProcess.cs'), 'utf8') + const programSource = readFileSync(join(sourceRoot, 'OrcaSshNoInputLauncher.cs'), 'utf8') const consoleSource = readFileSync(join(sourceRoot, 'WindowsPrivateConsoleInput.cs'), 'utf8') const outputSource = readFileSync(join(sourceRoot, 'WindowsBoundedOutputFiles.cs'), 'utf8') @@ -103,6 +104,9 @@ describe('Windows SSH no-input launcher', () => { expect(processSource).toContain('OutputLimitPollMilliseconds = 50') expect(processSource).toContain('JobSettlementTimeoutMilliseconds = 5000') expect(processSource).toContain('TerminateJobObject') + expect(processSource).toContain('diagnosticTimeoutMilliseconds') + expect(programSource).toContain('--diagnostic-timeout-ms') + expect(programSource).toContain('diagnosticTimeoutMilliseconds > 60000') expect(processSource).toContain('outputs.EnsureWithinLimits()') expect(processSource).toContain('outputs.ReplayOutputs()') expect(processSource).not.toContain('WaitForMultipleObjects') @@ -195,6 +199,26 @@ describe('Windows SSH no-input launcher', () => { expect(listCaptureFiles()).toEqual(capturesBefore) }) + itWindows( + 'terminates the child job and replays bounded output at the diagnostic deadline', + { timeout: 10_000 }, + async () => { + const capturesBefore = listCaptureFiles() + const pidPath = join(fixtureRoot, 'timed-out-child.pid') + const result = runLauncher('hold', [pidPath], {}, 250) + const childPid = Number.parseInt(readFileSync(pidPath, 'utf8'), 10) + + expect(result.status).toBe(1) + expect(result.stdout.toString('utf8')).toBe('ORCA-HOLD-STDOUT') + expect(result.stderr.toString('utf8')).toContain('ORCA-HOLD-STDERR') + expect(result.stderr.toString('utf8')).toContain( + 'SSH child reached the 250 ms diagnostic timeout.' + ) + await waitForProcessExit(childPid, 5_000) + expect(listCaptureFiles()).toEqual(capturesBefore) + } + ) + itWindows('does not leak an unrelated inheritable parent handle into the child', () => { const result = spawnSync(handleProbePath, [launcherPath], { cwd: fixtureRoot, @@ -231,15 +255,21 @@ describe('Windows SSH no-input launcher', () => { ) }) -function runLauncher(mode, args = [], options = {}) { - return spawnSync(launcherPath, [process.execPath, childFixturePath, mode, ...args], { - cwd: fixtureRoot, - encoding: null, - timeout: 10_000, - windowsHide: true, - maxBuffer: 32 * 1024 * 1024, - ...options - }) +function runLauncher(mode, args = [], options = {}, diagnosticTimeoutMs = null) { + const diagnosticArgs = + diagnosticTimeoutMs == null ? [] : ['--diagnostic-timeout-ms', String(diagnosticTimeoutMs)] + return spawnSync( + launcherPath, + [...diagnosticArgs, process.execPath, childFixturePath, mode, ...args], + { + cwd: fixtureRoot, + encoding: null, + timeout: 10_000, + windowsHide: true, + maxBuffer: 32 * 1024 * 1024, + ...options + } + ) } function listCaptureFiles() { @@ -328,6 +358,8 @@ if (mode === 'arguments-and-stdin') { } } else if (mode === 'hold') { require('node:fs').writeFileSync(process.argv[3], String(process.pid)) + process.stdout.write('ORCA-HOLD-STDOUT') + process.stderr.write('ORCA-HOLD-STDERR') setInterval(() => {}, 1000) } else { process.exit(99) 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 d50e5d6d771..18d7efcc0f4 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 @@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected private- -console plus bounded regular-output Windows diagnostic locally green; exact-head native proof -required, 2026-07-16, Codex implementation owner.** +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — bounded verbose +Win32-OpenSSH lifecycle capture locally green; exact-head native proof required, 2026-07-16, Codex +implementation owner.** `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain @@ -210,6 +210,21 @@ isolation pass. Seven native launcher cases plus live/full-size behavior remain honest local skips. Commit/push only this runner-temp diagnostic and require fresh exact-head Windows x64/ARM64 compilation, overflow, cleanup, cancellation, live lifecycle, and fixture-teardown proof. SignPath remains deferred. +Exact head `149a122b735659b47d7718f409d14e1ed2f947c2`, run `29553325782`, and x64/ARM64 jobs +`87800290122`/`87800290144` prove 101 files/862 cases with 19 skips, including both new overflow and +capture-cleanup cases, plus unchanged reproducibility and full-size cache gates. The live probes +still time out after 8017.5611/8042.1902 ms with ended stdout, closed channel, no sentinel/process +exit, and zero stderr; both exact teardowns catch locked `UsrClass.dat`. All four non-Windows jobs +and both Linux supplements pass. `E-M6-WINDOWS-PRIVATE-CONSOLE-REGULAR-OUTPUT-CI-RED-001` therefore +falsifies output pipes as the remaining cause. Next add only an internal diagnostic deadline and +bounded replay of verbose OpenSSH output before choosing ConPTY or another client. Production, +Beta, fallback, tuple, publication, default, and signing paths remain untouched. +`E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-LOCAL-001` records that diagnostic locally green: an +optional 100-60,000 ms runner-only deadline terminates and settles the owned job before bounded +output replay; the live suite uses 6 seconds, `-vvv`, outer 8/10-second safety ceilings, and a 16 KiB +trace tail. Focused 119/119, all 285 release contracts, typecheck, full lint/reliability/max-lines, +formatting, diff, and protected-resolver isolation pass with honest native skips. Commit/push only +this artifact and require x64/ARM64 phase classification before selecting ConPTY or another client. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown 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 051eb324023..3c2a337c901 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps Date created: 2026-07-14
    Last updated: 2026-07-16
    -Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **Locally green / exact-head native proof required — 2026-07-16, Codex implementation owner: disconnected private-console plus bounded regular-output diagnostic**. `E-M6-WINDOWS-PRIVATE-CONSOLE-REGULAR-OUTPUT-LOCAL-001` records the one-variable runner-temp candidate locally green: proven private-console EOF, distinct bounded regular-file output handles, delete-on-close cleanup, bounded job settlement, and post-exit replay. Commit/push only this package and require x64/ARM64 legacy compilation, native contracts, live lifecycle, and exact teardown evidence. Every production/default SSH path remains disconnected. Real Apple/SignPath rehearsals remain deferred.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **Locally green / bounded verbose client-lifecycle native proof required — 2026-07-16, Codex implementation owner**. `E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-LOCAL-001` records the runner-temp internal deadline, job settlement, bounded replay, and verbose trace-tail contracts locally green. Commit/push only this diagnostic and require both native architectures to identify the last completed client phase before selecting ConPTY or another client. Every production/default SSH path remains disconnected. Real Apple/SignPath rehearsals remain deferred.
    Session checkpoint: **PRIVATE CONSOLE EOF PROVEN / PRIVATE OUTPUT PIPES FALSIFIED / BOUNDED REGULAR OUTPUT ACTIVE — 2026-07-16, Codex implementation owner** — exact head `5841b12bc94ecd1027e507e4b741e09fcb0a093d`, run `29552101635`, and x64/ARM64 jobs `87796673326`/`87796673353` both pass 101 files/860 cases with 19 skips, legacy compilation, real-console qualification/EOF, argv/binary/exit/handle-exclusion/cancellation, unchanged full-runtime reproducibility, and full-size cache gates. Live diagnostics still time out after 8025.1553/8034.6943 milliseconds with ended stdout, closed channel, no sentinel/process exit, and zero stderr; both exact teardowns catch locked `UsrClass.dat`. All four non-Windows primary jobs and both Linux supplements pass. No product caller, full-size Windows transfer claim, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
    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) @@ -19891,6 +19891,75 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases wi Beta option, fallback, full-size Windows transfer claim, tuple, publication, default, signing, or SignPath behavior is authorized. +### E-M6-WINDOWS-PRIVATE-CONSOLE-REGULAR-OUTPUT-CI-RED-001 — regular output files do not settle Win32-OpenSSH + +- Date/owner/head/run: 2026-07-16, Codex implementation owner; exact pushed head + `149a122b735659b47d7718f409d14e1ed2f947c2`, pull-request artifact run `29553325782`, Windows x64 + job `87800290122` on `windows-2022`, and Windows ARM64 job `87800290144` on `windows-11-arm`. +- Native contract proof: both architectures pass legacy .NET Framework compilation, the dedicated + console EOF probe, and 101 artifact-contract files with 862 runnable cases and 19 declared skips. + The two new cases raise the prior count from 860 to 862 and prove >16 MiB fail-closed output plus + successful/overflow delete-on-close cleanup; exact binary replay, argv, exit propagation, + unrelated-handle exclusion, job cancellation, and bounded settlement also pass. Both unchanged + runtimes pass build-twice identity, closure, smoke, and both full-size cache gates. +- X64 live result: `mode=private-console-regular-output-launcher-no-n`, + `elapsedMs=8017.5611`, `success=false`, `timedOut=true`, `sentinel=false`, `stdoutEnded=true`, + `processExit=null`, `channelClosed=true`, `closeCode=null`, and `stderrBytes=0`. +- ARM64 live result: the same lifecycle fields with `elapsedMs=8042.190199999999`. Both official + server logs prove accepted public-key authentication, remote command-session start, and remote + session close before the client requires bounded external termination. The full-size Windows + transfer remains correctly unexecuted after the failed diagnostic. +- Teardown result: x64 cannot unload fixture class hive + `S-1-5-21-4137868374-391603265-4009536220-1003_Classes`; ARM64 independently cannot unload + `S-1-5-21-1882319117-3219095328-2125279949-1001_Classes`. Both catch the locked + `C:\Users\orca_ssh_fixture\AppData\Local\Microsoft\Windows\UsrClass.dat` and retained profile + directory instead of silently leaking fixture state. +- Preserved evidence: Linux x64/ARM64 jobs `87800290119`/`87800290139`, Darwin x64/ARM64 jobs + `87800290123`/`87800290135`, and Linux oldest-userland supplements `87800855219`/`87800855211` + all pass at the same exact head. +- Interpretation/next bound: private pipes, overlapped pipes, zero-length regular stdin, proven + console EOF, and now distinct bounded regular-file stdout/stderr all reproduce the live hang. + Do not select a production mechanism from speculation. Add only a runner-temp internal deadline + that terminates and settles the owned job before replaying bounded output, invoke the exact client + with verbose diagnostics, and record the last completed OpenSSH phase on both architectures. + ConPTY and alternate-client qualification remain subsequent evidence decisions. No product, + Beta, fallback, tuple, publication, default, signing, or SignPath behavior is authorized. + +### E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-LOCAL-001 — internal timeout and bounded trace capture are locally green + +- Date/owner/base/runner: 2026-07-16, Codex implementation owner; uncommitted artifact-only package + atop exact regular-output RED head `149a122b735659b47d7718f409d14e1ed2f947c2` on Apple arm64, + macOS 26.2 build 25C56, Node 26.5.0, and pnpm 10.24.0. +- Diagnostic boundary: an optional runner-only `--diagnostic-timeout-ms` accepts 100 through 60,000 + ms and leaves ordinary launcher calls unbounded as before. At deadline the launcher terminates + and waits for its owned job, replays the same bounded regular-file stdout/stderr, then returns a + diagnostic failure. The live purpose suite uses 6,000 ms, retains the existing 8,000/10,000 ms + external safety ceilings, inserts `-vvv` before the exact OpenSSH destination separator, and logs + only a 16 KiB stderr tail plus lifecycle fields. Production argument construction is unchanged. +- Native contract design: the new Windows case writes stdout/stderr plus its PID, holds forever, + and requires internal timeout failure, exact replay, child-job termination, and unchanged capture- + file set. Existing console EOF, argv, binary output, overflow, cleanup, handle exclusion, exit, + and external-cancellation cases remain. The new native case is honestly skipped locally. +- Syntax/focused evidence: both launcher scripts pass `node --check`. + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 + src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts + src/main/ssh/ssh-system-no-input-windows-openssh.test.ts + src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts + config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs + config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases with ten declared + native/live skips in 2.69 seconds real; maximum RSS is 171,212,800 bytes and peak footprint is + 97,179,456 bytes. +- Release/static evidence: the unquoted release-contract command passes 51 files and 285 runnable + cases with eight declared Windows skips in 12.19 seconds. `/usr/bin/time -l pnpm typecheck` + passes in 2.57 seconds real with 1,258,979,328-byte maximum RSS and 97,163,024-byte peak + footprint. `/usr/bin/time -l pnpm lint` passes in 21.90 seconds real with the 26 existing + warnings, all 41 reliability gates, and the 355-entry max-lines ratchet; maximum RSS is + 2,039,169,024 bytes and peak footprint is 97,195,840 bytes. +- Residual boundary: local source contracts do not prove legacy compilation, internal timeout/job + settlement, output replay, or useful OpenSSH verbose content on Windows. Commit/push this isolated + package and require exact-head x64/ARM64 evidence. Do not select ConPTY, another client, a product + caller, Beta, fallback, tuple, publication, default, signing, or SignPath behavior from local proof. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -19949,11 +20018,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the private-console plus bounded regular-output diagnostic recorded by -`E-M6-WINDOWS-PRIVATE-CONSOLE-REGULAR-OUTPUT-LOCAL-001`, then require exact-head Windows x64 and -ARM64 legacy-compiler, console qualification/EOF, bounded binary output/overflow/cleanup, -argv/exit/handle-exclusion/cancellation, live diagnostic, and exact profile/directory teardown -evidence. Do not +Commit and push only the bounded internal-timeout and verbose-output capture recorded by +`E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-LOCAL-001`, then require exact-head Windows x64 and +ARM64 legacy-compiler, launcher-timeout settlement, verbose-client phase classification, and exact +profile/directory teardown evidence. Do not change the production Windows handle or check the broad Milestone 6 Windows system-SSH item until both architectures select the same console-independent strategy. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 6f1c2e39203..44b59a7bcec 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1670,12 +1670,15 @@

    Prove production behavior, failure behavior, and rollback behavior.

    child while retaining private output pipes, exact handle inheritance, job ownership, and bounded cancellation. Native x64/arm64 prove the console handle and zero-byte cooked EOF but the live client still hangs, falsifying stdin alone. - The next one-variable diagnostic retains that console stdin and replaces only + The next one-variable diagnostic retained that console stdin and replaced only child stdout/stderr pipes with unique delete-on-close regular files under an explicit per-stream byte ceiling, bounded polling/cancellation, incremental - post-exit replay, and exact cleanup. Build and live-test it on native x64/arm64 - before considering ConPTY, another SSH client, packaging, signing, or a product - caller. POSIX retains ignored + post-exit replay, and exact cleanup. Native x64/arm64 prove those contracts, but + the live client still hangs with the same lifecycle tuple, falsifying redirected + output pipes as the remaining cause. Before selecting ConPTY or another SSH + client, the runner-only launcher must boundedly terminate its own diagnostic + child and replay an exact verbose OpenSSH trace to classify the stuck client + phase. Do not package, sign, or add a product caller. POSIX retains ignored stdin plus -n; fixture-owned Windows session processes settle before profile hives are boundedly unloaded and native profile deletion begins; MaxSessions=1; interrupted upload; stale lock diff --git a/native/windows-ssh-no-input-launcher/OrcaSshNoInputLauncher.cs b/native/windows-ssh-no-input-launcher/OrcaSshNoInputLauncher.cs index 29a4cab4117..ee250c91fb3 100644 --- a/native/windows-ssh-no-input-launcher/OrcaSshNoInputLauncher.cs +++ b/native/windows-ssh-no-input-launcher/OrcaSshNoInputLauncher.cs @@ -7,21 +7,40 @@ private static int Main(string[] args) { try { - if (args.Length < 1) + int executableIndex = 0; + int diagnosticTimeoutMilliseconds = 0; + if (args.Length >= 2 && args[0] == "--diagnostic-timeout-ms") { - Console.Error.WriteLine("Usage: orca-ssh-no-input.exe [arguments...]"); + if (!Int32.TryParse(args[1], out diagnosticTimeoutMilliseconds) || + diagnosticTimeoutMilliseconds < 100 || + diagnosticTimeoutMilliseconds > 60000) + { + Console.Error.WriteLine("--diagnostic-timeout-ms requires 100 through 60000."); + return 2; + } + executableIndex = 2; + } + if (args.Length <= executableIndex) + { + Console.Error.WriteLine( + "Usage: orca-ssh-no-input.exe [--diagnostic-timeout-ms ] [arguments...]" + ); return 2; } - string executablePath = Path.GetFullPath(args[0]); + string executablePath = Path.GetFullPath(args[executableIndex]); if (!File.Exists(executablePath)) { Console.Error.WriteLine("Unable to locate the SSH executable at \"{0}\".", executablePath); return 2; } - string[] childArgs = new string[args.Length - 1]; - Array.Copy(args, 1, childArgs, 0, childArgs.Length); - return WindowsSshChildProcess.Run(executablePath, childArgs); + string[] childArgs = new string[args.Length - executableIndex - 1]; + Array.Copy(args, executableIndex + 1, childArgs, 0, childArgs.Length); + return WindowsSshChildProcess.Run( + executablePath, + childArgs, + diagnosticTimeoutMilliseconds + ); } catch (Exception error) { diff --git a/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs b/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs index 493a1c4f94a..4b1b1acd27c 100644 --- a/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs +++ b/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Text; @@ -18,7 +19,11 @@ internal static class WindowsSshChildProcess private const uint OutputLimitPollMilliseconds = 50; private const uint JobSettlementTimeoutMilliseconds = 5000; - internal static int Run(string executablePath, string[] args) + internal static int Run( + string executablePath, + string[] args, + int diagnosticTimeoutMilliseconds + ) { Stream launcherStdout = Console.OpenStandardOutput(); Stream launcherStderr = Console.OpenStandardError(); @@ -70,7 +75,22 @@ out process throw LastWin32("ResumeThread failed."); } - WaitForChildWithinOutputLimits(process.Process, outputs); + bool exited = WaitForChildWithinOutputLimits( + process.Process, + outputs, + diagnosticTimeoutMilliseconds + ); + if (!exited) + { + // Why: the runner must recover verbose bytes without relying on an external + // kill that also destroys the delete-on-close diagnostic captures. + TerminateAndSettleJob(ref job); + outputs.ReplayOutputs(); + throw new TimeoutException( + "SSH child reached the " + diagnosticTimeoutMilliseconds + + " ms diagnostic timeout." + ); + } uint exitCode; if (!GetExitCodeProcess(process.Process, out exitCode)) { @@ -177,18 +197,33 @@ out IntPtr inheritedHandles } } - private static void WaitForChildWithinOutputLimits( + private static bool WaitForChildWithinOutputLimits( IntPtr process, - WindowsBoundedOutputFiles outputs + WindowsBoundedOutputFiles outputs, + int diagnosticTimeoutMilliseconds ) { + Stopwatch elapsed = Stopwatch.StartNew(); while (true) { - uint waitResult = WaitForSingleObject(process, OutputLimitPollMilliseconds); + uint waitMilliseconds = OutputLimitPollMilliseconds; + if (diagnosticTimeoutMilliseconds > 0) + { + long remaining = diagnosticTimeoutMilliseconds - elapsed.ElapsedMilliseconds; + if (remaining <= 0) + { + return false; + } + if (remaining < waitMilliseconds) + { + waitMilliseconds = (uint)remaining; + } + } + uint waitResult = WaitForSingleObject(process, waitMilliseconds); if (waitResult == WaitObject0) { outputs.EnsureWithinLimits(); - return; + return true; } if (waitResult == WaitFailed) { diff --git a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts index 8fe45e3a699..f90acfe38f5 100644 --- a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +++ b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts @@ -7,7 +7,7 @@ import type { SshTarget } from '../../shared/ssh-types' import { buildSshArgs } from './system-ssh-args' import { findSystemSsh } from './system-ssh-binary' -type DiagnosticMode = 'private-console-regular-output-launcher-no-n' +type DiagnosticMode = 'private-console-regular-output-verbose-timeout' type DiagnosticResult = { mode: DiagnosticMode @@ -19,6 +19,7 @@ type DiagnosticResult = { channelClosed: boolean closeCode: number | null | 'not-observed' stderrBytes: number + stderrTail: string elapsedMs: number } @@ -48,19 +49,26 @@ async function runDiagnostic(): Promise { if (!sshPath) { throw new Error('Native Windows OpenSSH client is unavailable') } - const mode: DiagnosticMode = 'private-console-regular-output-launcher-no-n' + const mode: DiagnosticMode = 'private-console-regular-output-verbose-timeout' const startedAt = performance.now() // Why: Win32-OpenSSH still hangs with proven console EOF plus piped output, so this diagnostic // isolates stdout/stderr as bounded regular files without changing production transport. + const sshArgs = buildSshArgs(createTarget()) + const destinationSeparator = sshArgs.indexOf('--') + if (destinationSeparator < 0) { + throw new Error('Windows OpenSSH diagnostic arguments lost the destination separator') + } + sshArgs.splice(destinationSeparator, 0, '-vvv') const child = spawn( launcherPath as string, - [sshPath, ...buildSshArgs(createTarget()), 'echo ORCA-SYSTEM-SSH-OK'], + ['--diagnostic-timeout-ms', '6000', sshPath, ...sshArgs, 'echo ORCA-SYSTEM-SSH-OK'], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true } ) return new Promise((resolve, reject) => { let stdout = '' let stderrBytes = 0 + let stderrTail = '' let stdoutEnded = false let processExit: number | null | 'not-observed' = 'not-observed' let channelClosed = false @@ -91,6 +99,7 @@ async function runDiagnostic(): Promise { channelClosed, closeCode, stderrBytes, + stderrTail, elapsedMs: performance.now() - startedAt }) } @@ -115,6 +124,8 @@ async function runDiagnostic(): Promise { }) child.stderr?.on('data', (chunk: Buffer) => { stderrBytes += chunk.length + // Why: retain only the diagnostic tail needed to classify the stuck client phase. + stderrTail = (stderrTail + chunk.toString('utf8')).slice(-16 * 1024) }) child.on('exit', (code) => { processExit = code @@ -138,16 +149,16 @@ async function runDiagnostic(): Promise { describe.skipIf(!hasLiveInput)('Windows OpenSSH no-input child-handle diagnostic', () => { it( - 'qualifies private-console EOF plus regular-file output without the Windows null-input option', + 'captures the verbose client phase after a bounded private-console diagnostic timeout', { timeout: 15_000 }, async () => { expect(process.platform).toBe('win32') - const privateConsoleRegularOutputLauncherNoN = await runDiagnostic() + const privateConsoleRegularOutputVerboseTimeout = await runDiagnostic() console.log( - `ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ privateConsoleRegularOutputLauncherNoN })}` + `ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ privateConsoleRegularOutputVerboseTimeout })}` ) - expect(privateConsoleRegularOutputLauncherNoN.success).toBe(true) + expect(privateConsoleRegularOutputVerboseTimeout.success).toBe(true) } ) }) From a6feb8c3be0e15ae45fc6b431c1665e5fd84d2c9 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 17 Jul 2026 00:25:12 -0400 Subject: [PATCH 258/267] test(ssh): pin Windows fixture host trust Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 26 ++++- ...github-release-implementation-checklist.md | 108 ++++++++++++++---- ...6-07-14-ssh-relay-github-release-plan.html | 26 +++-- ...sh-system-no-input-windows-openssh.test.ts | 73 ++++++++++-- 4 files changed, 190 insertions(+), 43 deletions(-) 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 18d7efcc0f4..763f4f3c4d8 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 @@ -9,9 +9,13 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — bounded verbose -Win32-OpenSSH lifecycle capture locally green; exact-head native proof required, 2026-07-16, Codex -implementation owner.** +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — explicit fixture +host-key trust correction in progress, 2026-07-16, Codex implementation owner.** Exact head +`b24dda744892c2ee43a3fb345d889ba97adb4239`, run `29554073553`, and Windows x64/ARM64 jobs +`87802525440`/`87802525442` prove the bounded verbose diagnostic and show both clients stop before +authentication while looking for the runner account's missing default `known_hosts`. The next +single-variable correction passes the fixture's exact pinned file with strict checking; production +SSH arguments and every product/default path remain unchanged. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain @@ -225,6 +229,22 @@ output replay; the live suite uses 6 seconds, `-vvv`, outer 8/10-second safety c trace tail. Focused 119/119, all 285 release contracts, typecheck, full lint/reliability/max-lines, formatting, diff, and protected-resolver isolation pass with honest native skips. Commit/push only this artifact and require x64/ARM64 phase classification before selecting ConPTY or another client. +Exact head `b24dda744892c2ee43a3fb345d889ba97adb4239`, run `29554073553`, and Windows x64/ARM64 +jobs `87802525440`/`87802525442` prove 101 files/863 cases with 19 skips plus the internal timeout, +replay, and job-termination contracts. Both 6111.6731/6395.98-millisecond traces stop before +authentication at `hostkeys_find_by_key_hostfile`: Win32-OpenSSH looks under the runner account +instead of the isolated fixture client home. `E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-CI-RED-001` +therefore replaces the prior session-close hypothesis with one narrower correction: pass only the +fixture's exact pinned `known_hosts` with strict checking. Both profile teardowns still catch locked +`UsrClass.dat`; all four non-Windows jobs and both Linux supplements pass. +`E-M6-WINDOWS-DIAGNOSTIC-HOST-TRUST-CORRECTION-LOCAL-001` records that correction locally green: +the purpose contract proves `-vvv`, `StrictHostKeyChecking=yes`, and the exact fixture +`UserKnownHostsFile` occur before `--`, rejects `StrictHostKeyChecking=no`, and compares against an +unchanged production argument vector. Focused 120/120, all 285 release contracts, typecheck, full +lint/reliability/max-lines, formatting, diff, and protected-resolver isolation pass with honest +native/live skips. Commit/push only this runner diagnostic and require fresh x64/ARM64 strict-trust, +authentication, natural settlement, full-size transfer, and exact teardown proof. Production, +Beta, fallback, tuple, publication, default, and signing paths remain untouched. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown 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 3c2a337c901..a9066c68d8f 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-16
    -Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **Locally green / bounded verbose client-lifecycle native proof required — 2026-07-16, Codex implementation owner**. `E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-LOCAL-001` records the runner-temp internal deadline, job settlement, bounded replay, and verbose trace-tail contracts locally green. Commit/push only this diagnostic and require both native architectures to identify the last completed client phase before selecting ConPTY or another client. Every production/default SSH path remains disconnected. Real Apple/SignPath rehearsals remain deferred.
    -Session checkpoint: **PRIVATE CONSOLE EOF PROVEN / PRIVATE OUTPUT PIPES FALSIFIED / BOUNDED REGULAR OUTPUT ACTIVE — 2026-07-16, Codex implementation owner** — exact head `5841b12bc94ecd1027e507e4b741e09fcb0a093d`, run `29552101635`, and x64/ARM64 jobs `87796673326`/`87796673353` both pass 101 files/860 cases with 19 skips, legacy compilation, real-console qualification/EOF, argv/binary/exit/handle-exclusion/cancellation, unchanged full-runtime reproducibility, and full-size cache gates. Live diagnostics still time out after 8025.1553/8034.6943 milliseconds with ended stdout, closed channel, no sentinel/process exit, and zero stderr; both exact teardowns catch locked `UsrClass.dat`. All four non-Windows primary jobs and both Linux supplements pass. No product caller, full-size Windows transfer claim, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — explicit fixture host-key trust correction, 2026-07-16, Codex implementation owner**. Exact head `b24dda744892c2ee43a3fb345d889ba97adb4239`, run `29554073553`, and x64/ARM64 jobs `87802525440`/`87802525442` prove the internal deadline, job settlement, bounded replay, and verbose trace capture. Both clients stop before authentication while resolving the runner account's missing default `known_hosts`; Win32-OpenSSH ignored the workflow's overridden profile variables. Pass only the fixture's exact pinned `known_hosts` path with strict checking, keep production arguments unchanged, and require fresh native proof before selecting ConPTY or another client. Every production/default SSH path remains disconnected. Real Apple/SignPath rehearsals remain deferred.
    +Session checkpoint: **VERBOSE TRACE CLASSIFIED / FIXTURE TRUST PATH ACTIVE — 2026-07-16, Codex implementation owner** — x64/ARM64 pass 101 files/863 cases with 19 skips, legacy compilation, launcher internal-timeout/replay/job-termination, unchanged runtime reproducibility, and full-size cache gates. Their 6111.6731/6395.98-millisecond traces both show `C:\\Users\\runneradmin\\.ssh\\known_hosts` lookup and stop at `hostkeys_find_by_key_hostfile` before authentication; lifecycle is `sentinel=false`, `stdoutEnded=true`, `processExit=1`, `channelClosed=true`, `closeCode=1`, and `stderrBytes=7923`. Both exact teardowns still catch locked `UsrClass.dat`. All four non-Windows primary jobs and both Linux supplements pass. No product caller, full-size Windows transfer claim, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
    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) @@ -19867,15 +19867,15 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases wi - Syntax/focused evidence: `node --check config/scripts/build-windows-ssh-no-input-launcher.mjs` and `node --check config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs` pass. `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 - src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts - src/main/ssh/ssh-system-no-input-windows-openssh.test.ts - src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts - config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs - config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases with nine declared +src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases with nine declared native/live skips in 2.59 seconds real; maximum RSS is 171,491,328 bytes and peak footprint is 97,064,696 bytes. - Release contracts: `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 - --reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 51 files and 285 runnable cases +--reporter=dot config/scripts/ssh-relay-runtime-*.test.mjs` passes 51 files and 285 runnable cases with seven declared Windows skips in 13.29 seconds. - Static gates: `/usr/bin/time -l pnpm typecheck` passes in 2.86 seconds real with 1,247,576,064-byte maximum RSS and 97,343,320-byte peak footprint. `/usr/bin/time -l pnpm lint` @@ -19883,7 +19883,7 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases wi 355-entry max-lines ratchet; maximum RSS is 2,044,805,120 bytes and peak footprint is 97,163,000 bytes. Purpose-file formatting and `git diff --check` pass; `git diff --exit-code HEAD -- src/main/ssh/ssh-remote-node-resolution.ts - src/main/ssh/ssh-remote-node-resolution.test.ts` is empty. +src/main/ssh/ssh-remote-node-resolution.test.ts` is empty. - Residual boundary: local source contracts do not prove legacy .NET Framework compilation, delete-on-close behavior, overflow settlement, binary replay, job cancellation, live OpenSSH, or exact fixture teardown on Windows. Commit and push only this disconnected package, then require @@ -19942,11 +19942,11 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases wi and external-cancellation cases remain. The new native case is honestly skipped locally. - Syntax/focused evidence: both launcher scripts pass `node --check`. `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 - src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts - src/main/ssh/ssh-system-no-input-windows-openssh.test.ts - src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts - config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs - config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases with ten declared +src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases with ten declared native/live skips in 2.69 seconds real; maximum RSS is 171,212,800 bytes and peak footprint is 97,179,456 bytes. - Release/static evidence: the unquoted release-contract command passes 51 files and 285 runnable @@ -19960,6 +19960,74 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases wi package and require exact-head x64/ARM64 evidence. Do not select ConPTY, another client, a product caller, Beta, fallback, tuple, publication, default, signing, or SignPath behavior from local proof. +### E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-CI-RED-001 — native trace exposes missing fixture host-key path + +- Date/owner/source/run: 2026-07-16, Codex implementation owner; exact head + `b24dda744892c2ee43a3fb345d889ba97adb4239`, artifact run + [29554073553](https://github.com/stablyai/orca/actions/runs/29554073553), Windows x64 job + [87802525440](https://github.com/stablyai/orca/actions/runs/29554073553/job/87802525440) on + `windows-2022`, and native ARM64 job + [87802525442](https://github.com/stablyai/orca/actions/runs/29554073553/job/87802525442) on + `windows-11-arm64`; both use Node 24.18.0. +- Preserved gates: both Windows jobs pass legacy .NET launcher compilation and 101 files/863 + runnable cases with 19 declared skips, including internal diagnostic timeout, exact replay, + job-owned termination, console EOF, argv/binary/exit, output overflow/cleanup, unrelated-handle + exclusion, external cancellation, clean-build identity, runtime smoke, and both full-size cache + gates. Linux x64/ARM64 jobs `87802525448`/`87802525441`, Darwin x64/ARM64 jobs + `87802525450`/`87802525446`, and Linux oldest-userland supplements + `87803014758`/`87803014770` pass at the same exact head. +- Native lifecycle evidence: x64 exits after 6111.6731 ms and ARM64 after 6395.98 ms. Both report + `timedOut=false`, `sentinel=false`, `stdoutEnded=true`, `processExit=1`, `channelClosed=true`, + `closeCode=1`, and `stderrBytes=7923`. The false outer-timeout field is expected: the new internal + 6,000 ms launcher deadline terminates and settles the owned job, replays the bounded trace, and + returns exit 1 before the 8,000/10,000 ms external safety ceilings. +- Trace classification: both stock Win32-OpenSSH 9.5 clients establish TCP, complete key exchange, + receive the fixture's Ed25519 host key, and then try + `C:\Users\runneradmin\.ssh\known_hosts`. They stop at `hostkeys_find_by_key_hostfile` because that + default file is absent. The workflow-created pinned file is instead under the isolated + `ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_CLIENT_HOME`. Win32-OpenSSH therefore ignored the step's + overridden `HOME`/`USERPROFILE` for host-key discovery. Earlier server authentication/session + log lines came from the fixture readiness probe and do not prove this diagnostic authenticated. +- Teardown/residual: both jobs still fail visibly on the owned fixture profile's locked + `UsrClass.dat`; the full-size Windows transfer correctly remains unexecuted after the diagnostic. + This result does not justify ConPTY or an alternate client. The next single-variable correction + must pass only the fixture's exact pinned `known_hosts` path with `StrictHostKeyChecking=yes`, + never weaken trust, and keep production SSH argument construction unchanged. + +### E-M6-WINDOWS-DIAGNOSTIC-HOST-TRUST-CORRECTION-LOCAL-001 — exact pinned trust path is locally green + +- Date/owner/base/runner: 2026-07-16, Codex implementation owner; uncommitted runner-only test + correction atop exact verbose-trace head `b24dda744892c2ee43a3fb345d889ba97adb4239` on Apple arm64, + macOS 26.2 build 25C56, Node 26.5.0, and pnpm 10.24.0. +- Boundary: the live diagnostic now requires + `ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_CLIENT_HOME`, builds its exact fixture file with + `path.join(clientHome, '.ssh', 'known_hosts')`, and inserts `-vvv`, + `StrictHostKeyChecking=yes`, and `UserKnownHostsFile=` before OpenSSH's `--` + separator. The purpose contract compares the complete diagnostic vector with an independently + built production vector, rejects `StrictHostKeyChecking=no`, and proves connection reuse and all + production arguments are unchanged. +- Syntax/focused evidence: both launcher scripts pass `node --check`. + `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 120 runnable cases with ten declared + native/live skips in 3.13 seconds real; maximum RSS is 174,571,520 bytes and peak footprint is + 97,113,848 bytes. +- Release/static evidence: the unquoted release-contract command passes 51 files and 285 runnable + cases with eight declared Windows skips in 16.23 seconds; maximum RSS is 196,100,096 bytes and + peak footprint is 97,179,360 bytes. `/usr/bin/time -l pnpm typecheck` passes in 3.38 seconds real + with 1,208,680,448-byte maximum RSS and 97,031,952-byte peak footprint. `/usr/bin/time -l pnpm +lint` passes in 18.59 seconds real with the 26 existing warnings, all 41 reliability gates, and + the 355-entry max-lines ratchet; maximum RSS is 2,027,831,296 bytes and peak footprint is + 97,392,520 bytes. +- Residual boundary: local execution does not prove the fixture path is accepted by Win32-OpenSSH, + authentication completes, the launcher exits naturally, full-size transfer runs, or the fixture + profile tears down. Commit/push only this correction and require exact-head x64/ARM64 evidence. + No production caller, Beta, fallback, tuple, publication, default, signing, or SignPath behavior + is authorized by this local proof. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -20018,12 +20086,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the bounded internal-timeout and verbose-output capture recorded by -`E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-LOCAL-001`, then require exact-head Windows x64 and -ARM64 legacy-compiler, launcher-timeout settlement, verbose-client phase classification, and exact -profile/directory teardown evidence. Do not -change the production Windows handle or check the broad Milestone 6 Windows system-SSH item until -both architectures select the same console-independent strategy. Do not add an +Commit and push only the explicit diagnostic fixture-trust correction recorded by +`E-M6-WINDOWS-DIAGNOSTIC-HOST-TRUST-CORRECTION-LOCAL-001`, then require exact-head Windows x64 and +ARM64 legacy-compiler, launcher, strict host-key authentication, natural client settlement, +full-size transfer, and exact profile/directory teardown evidence. Do not change the production +Windows handle or check the broad Milestone 6 Windows system-SSH item until both architectures pass +the same console-independent strategy. Do not add an Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 44b59a7bcec..a0d81e387e1 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1669,19 +1669,23 @@

    Prove production behavior, failure behavior, and rollback behavior.

    queued console EOF before child creation, and shared that console with the SSH child while retaining private output pipes, exact handle inheritance, job ownership, and bounded cancellation. Native x64/arm64 prove the console handle - and zero-byte cooked EOF but the live client still hangs, falsifying stdin alone. - The next one-variable diagnostic retained that console stdin and replaced only - child stdout/stderr pipes with unique delete-on-close regular files under an - explicit per-stream byte ceiling, bounded polling/cancellation, incremental + and zero-byte cooked EOF but the live client still hangs, falsifying stdin + alone. The next one-variable diagnostic retained that console stdin and replaced + only child stdout/stderr pipes with unique delete-on-close regular files under + an explicit per-stream byte ceiling, bounded polling/cancellation, incremental post-exit replay, and exact cleanup. Native x64/arm64 prove those contracts, but the live client still hangs with the same lifecycle tuple, falsifying redirected - output pipes as the remaining cause. Before selecting ConPTY or another SSH - client, the runner-only launcher must boundedly terminate its own diagnostic - child and replay an exact verbose OpenSSH trace to classify the stuck client - phase. Do not package, sign, or add a product caller. POSIX retains ignored - stdin plus -n; fixture-owned Windows session processes settle - before profile hives are boundedly unloaded and native profile deletion begins; - MaxSessions=1; interrupted upload; stale lock + output pipes as the remaining cause. The bounded verbose trace on native + x64/arm64 then shows that Win32-OpenSSH ignores the workflow's overridden + profile variables for host-key discovery and stops before authentication while + looking for the runner account's missing default known_hosts. + Before selecting ConPTY or another SSH client, pass only the fixture's exact + pinned known_hosts path with strict host-key checking and require + both architectures to pass. Do not weaken trust, package, sign, or add a product + caller. POSIX retains ignored stdin plus -n; fixture-owned Windows + session processes settle before profile hives are boundedly unloaded and native + profile deletion begins; MaxSessions=1; interrupted upload; stale + lock Bundled Node hashes the full staged tree before sentinel write; diff --git a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts index f90acfe38f5..2b3f476ccb8 100644 --- a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +++ b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts @@ -1,4 +1,5 @@ import { spawn } from 'node:child_process' +import { join } from 'node:path' import { performance } from 'node:perf_hooks' import { describe, expect, it } from 'vitest' @@ -26,9 +27,12 @@ type DiagnosticResult = { const host = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_HOST const user = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_USER const identityFile = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_IDENTITY +const clientHome = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_CLIENT_HOME const launcherPath = process.env.ORCA_SSH_WINDOWS_NO_INPUT_LAUNCHER const port = Number.parseInt(process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_PORT ?? '', 10) -const hasLiveInput = Boolean(host && user && identityFile && launcherPath && Number.isInteger(port)) +const hasLiveInput = Boolean( + host && user && identityFile && clientHome && launcherPath && Number.isInteger(port) +) function createTarget(): SshTarget { return { @@ -44,6 +48,25 @@ function createTarget(): SshTarget { } } +function buildDiagnosticSshArgs(target: SshTarget, knownHostsPath: string): string[] { + const sshArgs = buildSshArgs(target) + const destinationSeparator = sshArgs.indexOf('--') + if (destinationSeparator < 0) { + throw new Error('Windows OpenSSH diagnostic arguments lost the destination separator') + } + // Why: Win32-OpenSSH ignores overridden profile variables; use only the fixture's pinned trust. + sshArgs.splice( + destinationSeparator, + 0, + '-vvv', + '-o', + 'StrictHostKeyChecking=yes', + '-o', + `UserKnownHostsFile=${knownHostsPath}` + ) + return sshArgs +} + async function runDiagnostic(): Promise { const sshPath = findSystemSsh() if (!sshPath) { @@ -51,14 +74,10 @@ async function runDiagnostic(): Promise { } const mode: DiagnosticMode = 'private-console-regular-output-verbose-timeout' const startedAt = performance.now() - // Why: Win32-OpenSSH still hangs with proven console EOF plus piped output, so this diagnostic - // isolates stdout/stderr as bounded regular files without changing production transport. - const sshArgs = buildSshArgs(createTarget()) - const destinationSeparator = sshArgs.indexOf('--') - if (destinationSeparator < 0) { - throw new Error('Windows OpenSSH diagnostic arguments lost the destination separator') - } - sshArgs.splice(destinationSeparator, 0, '-vvv') + const sshArgs = buildDiagnosticSshArgs( + createTarget(), + join(clientHome as string, '.ssh', 'known_hosts') + ) const child = spawn( launcherPath as string, ['--diagnostic-timeout-ms', '6000', sshPath, ...sshArgs, 'echo ORCA-SYSTEM-SSH-OK'], @@ -147,6 +166,42 @@ async function runDiagnostic(): Promise { }) } +describe('Windows OpenSSH no-input diagnostic trust arguments', () => { + it('adds only strict fixture trust and verbosity before the destination separator', () => { + const target: SshTarget = { + id: 'windows-no-input-contract', + label: 'windows-no-input-contract', + host: '127.0.0.1', + port: 22224, + username: 'fixture-user', + identityFile: 'C:\\fixture\\client-key', + identitiesOnly: true, + systemSshConnectionReuse: true, + source: 'manual' + } + const knownHostsPath = 'C:\\fixture\\client-home\\.ssh\\known_hosts' + const productionArgs = buildSshArgs(target) + const diagnosticArgs = buildDiagnosticSshArgs(target, knownHostsPath) + const destinationSeparator = productionArgs.indexOf('--') + const diagnosticOnlyArgs = [ + '-vvv', + '-o', + 'StrictHostKeyChecking=yes', + '-o', + `UserKnownHostsFile=${knownHostsPath}` + ] + + expect(diagnosticArgs).toEqual([ + ...productionArgs.slice(0, destinationSeparator), + ...diagnosticOnlyArgs, + ...productionArgs.slice(destinationSeparator) + ]) + expect(diagnosticArgs.indexOf('-vvv')).toBeLessThan(diagnosticArgs.indexOf('--')) + expect(diagnosticArgs).not.toContain('StrictHostKeyChecking=no') + expect(buildSshArgs(target)).toEqual(productionArgs) + }) +}) + describe.skipIf(!hasLiveInput)('Windows OpenSSH no-input child-handle diagnostic', () => { it( 'captures the verbose client phase after a bounded private-console diagnostic timeout', From 42098d1b1393a97c03fec6e7b685859f8296b90d Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 17 Jul 2026 00:53:03 -0400 Subject: [PATCH 259/267] test(ssh): qualify Windows no-input launcher adapter Co-authored-by: Orca --- .../ssh-relay-runtime-workflow.test.mjs | 18 ++- ...elease-implementation-checklist-summary.md | 27 +++-- ...github-release-implementation-checklist.md | 105 +++++++++++++++-- ...6-07-14-ssh-relay-github-release-plan.html | 10 +- src/main/ssh/ssh-connection.test.ts | 49 ++++++++ src/main/ssh/ssh-connection.ts | 28 ++++- ...ndows-system-ssh-openssh-full-size.test.ts | 9 +- src/main/ssh/ssh-system-fallback.ts | 7 +- .../ssh/system-ssh-command-launcher.test.ts | 111 ++++++++++++++++++ src/main/ssh/system-ssh-command.ts | 24 +++- 10 files changed, 356 insertions(+), 32 deletions(-) create mode 100644 src/main/ssh/system-ssh-command-launcher.test.ts diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index e31cd933ff1..6be7aa7e12a 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -142,8 +142,24 @@ describe('SSH relay runtime artifact workflow', () => { }) it('runs full-size Windows system SSH against loopback native OpenSSH', async () => { - const workflow = parse(await readFile(workflowUrl, 'utf8')) + const [workflowSource, fullSizeSource, connectionSource, managerSource] = await Promise.all([ + readFile(workflowUrl, 'utf8'), + readFile( + new URL( + '../../src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts', + import.meta.url + ), + 'utf8' + ), + readFile(new URL('../../src/main/ssh/ssh-connection.ts', import.meta.url), 'utf8'), + readFile(new URL('../../src/main/ssh/ssh-connection-manager.ts', import.meta.url), 'utf8') + ]) + const workflow = parse(workflowSource) assertWindowsOpenSshWorkflow(workflow, expect) + expect(fullSizeSource).toContain('process.env.ORCA_SSH_WINDOWS_NO_INPUT_LAUNCHER') + expect(fullSizeSource).toContain('{ windowsNoInputLauncherPath: launcherPath as string }') + expect(connectionSource).not.toContain('ORCA_SSH_WINDOWS_NO_INPUT_LAUNCHER') + expect(managerSource).not.toContain('windowsNoInputLauncherPath') }) it('uploads only the first output after two clean builds verify and compare', async () => { 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 763f4f3c4d8..7cf1f1509fa 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-16 +Last updated: 2026-07-17 Use this file to track the project. The [detailed evidence ledger](./2026-07-14-ssh-relay-github-release-implementation-checklist.md) @@ -9,13 +9,15 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — explicit fixture -host-key trust correction in progress, 2026-07-16, Codex implementation owner.** Exact head -`b24dda744892c2ee43a3fb345d889ba97adb4239`, run `29554073553`, and Windows x64/ARM64 jobs -`87802525440`/`87802525442` prove the bounded verbose diagnostic and show both clients stop before -authentication while looking for the runner account's missing default `known_hosts`. The next -single-variable correction passes the fixture's exact pinned file with strict checking; production -SSH arguments and every product/default path remain unchanged. +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — explicit Windows +no-input launcher-path adapter locally green, exact-head native rerun next, 2026-07-17, Codex implementation owner.** Exact head +`a6feb8c3be0e15ae45fc6b431c1665e5fd84d2c9`, run `29554814775`, and Windows x64/ARM64 jobs +`87804630533`/`87804630504` prove the strict-trust diagnostic naturally completes through the +private-console launcher while the unchanged direct-spawn product probe still hangs after remote +session close. The candidate is constructor-injected only into the native full-size purpose test; +`E-M6-WINDOWS-NOINPUT-LAUNCHER-ADAPTER-LOCAL-001` records syntax, 124 focused cases, 285 release +contracts, typecheck, lint/reliability/max-lines, formatting, diff, and protected-resolver isolation +green. Exact-head native x64/ARM64 full-size and teardown gates remain before any product/default caller. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain @@ -245,6 +247,15 @@ lint/reliability/max-lines, formatting, diff, and protected-resolver isolation p native/live skips. Commit/push only this runner diagnostic and require fresh x64/ARM64 strict-trust, authentication, natural settlement, full-size transfer, and exact teardown proof. Production, Beta, fallback, tuple, publication, default, and signing paths remain untouched. +Exact head `a6feb8c3be0e15ae45fc6b431c1665e5fd84d2c9`, run `29554814775`, and Windows x64/ARM64 jobs +`87804630533`/`87804630504` prove the strict diagnostic succeeds in 884.6485/707.1382 ms with the +sentinel, ended stdout, natural exit/close 0, and exact pinned trust. The unchanged direct-spawn +product probe then times out in 31113/30963 ms with all lifecycle fields unobserved after server-side +authentication and command-session close. `E-M6-WINDOWS-DIAGNOSTIC-HOST-TRUST-CI-RED-001` selects +the proven private-console/bounded-regular-output launcher on both architectures and authorizes only +an explicitly injected launcher-path adapter. Core code must not read the workflow environment; +when no path is supplied, existing behavior remains exact. Full-size, cancellation, concurrency, +reuse, and teardown proof remain required before packaging or any product/Beta/default caller. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1, serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown 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 a9066c68d8f..9cd2d87cdaa 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-16
    -Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — explicit fixture host-key trust correction, 2026-07-16, Codex implementation owner**. Exact head `b24dda744892c2ee43a3fb345d889ba97adb4239`, run `29554073553`, and x64/ARM64 jobs `87802525440`/`87802525442` prove the internal deadline, job settlement, bounded replay, and verbose trace capture. Both clients stop before authentication while resolving the runner account's missing default `known_hosts`; Win32-OpenSSH ignored the workflow's overridden profile variables. Pass only the fixture's exact pinned `known_hosts` path with strict checking, keep production arguments unchanged, and require fresh native proof before selecting ConPTY or another client. Every production/default SSH path remains disconnected. Real Apple/SignPath rehearsals remain deferred.
    -Session checkpoint: **VERBOSE TRACE CLASSIFIED / FIXTURE TRUST PATH ACTIVE — 2026-07-16, Codex implementation owner** — x64/ARM64 pass 101 files/863 cases with 19 skips, legacy compilation, launcher internal-timeout/replay/job-termination, unchanged runtime reproducibility, and full-size cache gates. Their 6111.6731/6395.98-millisecond traces both show `C:\\Users\\runneradmin\\.ssh\\known_hosts` lookup and stop at `hostkeys_find_by_key_hostfile` before authentication; lifecycle is `sentinel=false`, `stdoutEnded=true`, `processExit=1`, `channelClosed=true`, `closeCode=1`, and `stderrBytes=7923`. Both exact teardowns still catch locked `UsrClass.dat`. All four non-Windows primary jobs and both Linux supplements pass. No product caller, full-size Windows transfer claim, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
    +Last updated: 2026-07-17
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — explicit Windows no-input launcher-path adapter exact-head native qualification, 2026-07-17, Codex implementation owner**. Exact head `a6feb8c3be0e15ae45fc6b431c1665e5fd84d2c9`, run `29554814775`, and x64/ARM64 jobs `87804630533`/`87804630504` prove strict pinned trust, public-key authentication, command execution, and natural launcher settlement on both architectures. `E-M6-WINDOWS-NOINPUT-LAUNCHER-ADAPTER-LOCAL-001` records the adapter package locally green behind constructor injection for the no-input probe only. Commit/push and exact-head x64/ARM64 full-size transfer and teardown proof remain required. Keep the path absent from every product/default caller and keep legacy as the sole production path. Real Apple/SignPath rehearsals remain deferred.
    +Session checkpoint: **ADAPTER LOCAL GREEN / NATIVE RERUN NEXT — 2026-07-17, Codex implementation owner** — the candidate preserves direct spawn when no path is supplied, ignores the Windows-only path on POSIX, does not infer it from the runner environment, and injects it only from the native full-size purpose test. Syntax, 124 focused cases with ten declared native/live skips, 285 release contracts with eight declared Windows skips, typecheck, full lint/reliability/max-lines, formatting, diff, and protected-resolver isolation pass. No product caller, full-size Windows transfer claim, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
    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) @@ -20028,6 +20028,94 @@ lint` passes in 18.59 seconds real with the 26 existing warnings, all 41 reliabi No production caller, Beta, fallback, tuple, publication, default, signing, or SignPath behavior is authorized by this local proof. +### E-M6-WINDOWS-DIAGNOSTIC-HOST-TRUST-CI-RED-001 — strict trust proves the launcher and exposes the unchanged direct spawn + +- Date/owner/source/run: 2026-07-16, Codex implementation owner; exact head + `a6feb8c3be0e15ae45fc6b431c1665e5fd84d2c9`, artifact run + [29554814775](https://github.com/stablyai/orca/actions/runs/29554814775), Windows x64 job + [87804630533](https://github.com/stablyai/orca/actions/runs/29554814775/job/87804630533) on + `windows-2022`, and native ARM64 job + [87804630504](https://github.com/stablyai/orca/actions/runs/29554814775/job/87804630504) on + `windows-11-arm64`; both use Node 24.18.0. +- Preserved gates: both Windows jobs pass legacy .NET launcher compilation and 101 files/863 + runnable cases with 19 declared skips, including the complete launcher contract set, clean-build + identity, runtime smoke, and both full-size cache gates. Darwin x64/ARM64 jobs + `87804630514`/`87804630513`, Linux x64/ARM64 jobs `87804630528`/`87804630515`, and Linux + oldest-userland supplements `87805041222`/`87805041216` pass at the same exact head. +- Selected launcher evidence: the x64 diagnostic completes in 884.6485 ms and ARM64 in 707.1382 + ms. Both report `success=true`, `timedOut=false`, `sentinel=true`, `stdoutEnded=true`, + `processExit=0`, `channelClosed=true`, `closeCode=0`, and `stderrBytes=11090`. Both traces load the + exact isolated fixture `known_hosts`, match its Ed25519 host key, authenticate with the pinned + client key, execute the sentinel command, and exit naturally. This closes the prior host-trust + confounder and selects the private-console/bounded-regular-output launcher on both architectures. +- Unchanged product RED: the following full-size suite intentionally still enters the existing + direct-spawn `SshConnection` probe. X64 times out after 31,113 ms and ARM64 after 30,963 ms with + `sentinel=false`, `stdoutEnded=false`, `processExit=not-observed`, and `channelClosed=false`. + Server logs independently prove public-key authentication, command-session start, and remote + session close. The failure is therefore the unchanged Win32-OpenSSH direct child-handle path, not + fixture trust or remote execution. +- Teardown/residual: both jobs still fail visibly on the owned fixture profile's locked + `UsrClass.dat`; no full-size transfer metrics run after the failed connection probe. The next + package may add only an explicit trusted launcher-path option from `SshConnection` to the + Windows no-input command adapter, omit Win32-OpenSSH `-n` only when that launcher is selected, + and keep the existing direct spawn when no path is supplied. Core code must not read the runner + environment variable; the native purpose test injects the runner-built path explicitly. No + product/default caller, packaged asset, Beta, fallback, tuple, publication, signing, or SignPath + behavior is authorized by this evidence. + +### E-M6-WINDOWS-NOINPUT-LAUNCHER-ADAPTER-LOCAL-001 — explicit adapter is locally green and default-disconnected + +- Date/owner/base/runner: 2026-07-17, Codex implementation owner; uncommitted adapter package atop + exact head `a6feb8c3be0e15ae45fc6b431c1665e5fd84d2c9` on Apple arm64, macOS 26.2 build 25C56, Node + 26.5.0, and pnpm 10.24.0. The repository requests Node 24; the exact-head native workflow remains + the authoritative Node 24 gate. +- Boundary: `SshConnection` accepts an immutable copied `windowsNoInputLauncherPath` only through + explicit constructor injection and passes it only to its no-input connection probe. On native + Windows that option launches the already-qualified private-console/bounded-regular-output + executable, passes the resolved `ssh.exe` path as argv zero, omits OpenSSH `-n`, and gives the + launcher ignored stdin with piped stdout/stderr. When absent, the existing Win32 direct-spawn, + piped-then-closed stdin, and `-n` path remain exact. POSIX ignores the Windows-only option and + retains ignored stdin plus `-n`; ordinary payload commands never receive the launcher path. +- Activation guardrails: core connection code does not read + `ORCA_SSH_WINDOWS_NO_INPUT_LAUNCHER`; an environment value alone cannot activate the adapter. The + native full-size purpose test is the sole caller that reads the runner-built path and injects it. + The production `SshConnectionManager` has no launcher option or third constructor argument. + No packaged asset, Electron/startup caller, target setting, fallback, Beta, tuple, publication, + default, signing, or SignPath path is connected. +- Syntax/focused evidence: `node --check + config/scripts/build-windows-ssh-no-input-launcher.mjs` and `node --check + config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs` pass. `/usr/bin/time -l pnpm + exec vitest run --config config/vitest.config.ts --maxWorkers=1 + src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts + src/main/ssh/system-ssh-command-launcher.test.ts + src/main/ssh/ssh-system-no-input-windows-openssh.test.ts + src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts + config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs + config/scripts/ssh-relay-runtime-workflow.test.mjs` passes six files with one native file skipped, + 124 runnable cases, and ten declared native/live skips in 4.35 seconds real; maximum RSS is + 150,519,808 bytes and peak footprint is 97,687,504 bytes. +- Release/static evidence: `/usr/bin/time -l pnpm exec vitest run --config + config/vitest.config.ts --maxWorkers=1 --reporter=dot + config/scripts/ssh-relay-runtime-*.test.mjs` passes 51 files and 285 runnable cases with eight + declared Windows skips in 17.95 seconds real; maximum RSS is 195,674,112 bytes and peak footprint + is 97,228,728 bytes. `/usr/bin/time -l pnpm typecheck` passes in 2.77 seconds real with + 1,267,253,248-byte maximum RSS and 97,277,808-byte peak footprint. `/usr/bin/time -l pnpm lint` + passes in 16.38 seconds real with the same 26 existing warnings, all 41 reliability gates, and the + 355-entry max-lines ratchet; maximum RSS is 2,054,815,744 bytes and peak footprint is 97,654,616 + bytes. The first lint attempt exposed that the fallback test file crossed 800 code lines; the two + launcher-selection contracts were moved into purpose-named + `system-ssh-command-launcher.test.ts`, after which lint passes without a disable or ratchet change. +- Formatting/isolation: `pnpm exec oxfmt --write` passes for all modified TypeScript and MJS files; + `git diff --check` and exact `git diff --exit-code HEAD -- + src/main/ssh/ssh-remote-node-resolution.ts + src/main/ssh/ssh-remote-node-resolution.test.ts` pass. The protected Milestone 0 resolver files + remain byte-for-byte unchanged. +- Residual gate: local mocks do not prove the adapter settles native Win32-OpenSSH or that the owned + Windows fixture profile is released. Commit/push this isolated package, then require exact-head + Windows x64 and ARM64 launcher-backed probe, full-size serial/concurrent transfer, collision, + cancellation settlement, connection reuse, and exact profile/directory teardown. Do not check + the broad Milestone 6 Windows cell or add any product/default caller until both jobs pass. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -20086,12 +20174,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the explicit diagnostic fixture-trust correction recorded by -`E-M6-WINDOWS-DIAGNOSTIC-HOST-TRUST-CORRECTION-LOCAL-001`, then require exact-head Windows x64 and -ARM64 legacy-compiler, launcher, strict host-key authentication, natural client settlement, -full-size transfer, and exact profile/directory teardown evidence. Do not change the production -Windows handle or check the broad Milestone 6 Windows system-SSH item until both architectures pass -the same console-independent strategy. Do not add an +Commit and push only the explicit Windows no-input launcher-path adapter recorded by +`E-M6-WINDOWS-NOINPUT-LAUNCHER-ADAPTER-LOCAL-001`, then require exact-head Windows x64/ARM64 +launcher-backed probe, full-size transfer, cancellation, concurrency, connection +reuse, and exact profile/directory teardown evidence. Do not check the broad Milestone 6 Windows +system-SSH item or add an Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index a0d81e387e1..40481f3de6f 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1679,9 +1679,13 @@

    Prove production behavior, failure behavior, and rollback behavior.

    x64/arm64 then shows that Win32-OpenSSH ignores the workflow's overridden profile variables for host-key discovery and stops before authentication while looking for the runner account's missing default known_hosts. - Before selecting ConPTY or another SSH client, pass only the fixture's exact - pinned known_hosts path with strict host-key checking and require - both architectures to pass. Do not weaken trust, package, sign, or add a product + Passing only the fixture's exact pinned known_hosts path then + proves strict trust, authentication, command execution, and natural launcher + exit on both architectures. The following unchanged direct-spawn product probe + still hangs after authenticated remote session close, selecting the + private-console/ bounded-regular-output launcher rather than ConPTY or another + SSH client. Add only an explicitly injected launcher-path adapter first; when + absent, preserve existing behavior, and do not package, sign, or add a product caller. POSIX retains ignored stdin plus -n; fixture-owned Windows session processes settle before profile hives are boundedly unloaded and native profile deletion begins; MaxSessions=1; interrupted upload; stale diff --git a/src/main/ssh/ssh-connection.test.ts b/src/main/ssh/ssh-connection.test.ts index 5e2488ddc74..3b31f55b081 100644 --- a/src/main/ssh/ssh-connection.test.ts +++ b/src/main/ssh/ssh-connection.test.ts @@ -956,6 +956,55 @@ describe('SshConnection', () => { expect(probeChannel?.stdin.end).toHaveBeenCalledOnce() }) + it('passes an explicit Windows no-input launcher only to the system SSH probe', async () => { + vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig()) + const launcherPath = 'C:\\fixture\\orca-ssh-no-input-launcher.exe' + const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks(), { + windowsNoInputLauncherPath: launcherPath + }) + + await conn.connect() + await conn.exec('echo after-connect') + + expect(spawnSystemSshCommandMock).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ configHost: 'fdpass-host' }), + 'echo ORCA-SYSTEM-SSH-OK', + expect.objectContaining({ + noInput: true, + windowsNoInputLauncherPath: launcherPath + }) + ) + expect(spawnSystemSshCommandMock).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ configHost: 'fdpass-host' }), + 'echo after-connect', + expect.not.objectContaining({ windowsNoInputLauncherPath: expect.anything() }) + ) + }) + + it('does not infer the Windows no-input launcher from the runner environment', async () => { + vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig()) + const previousLauncherPath = process.env.ORCA_SSH_WINDOWS_NO_INPUT_LAUNCHER + process.env.ORCA_SSH_WINDOWS_NO_INPUT_LAUNCHER = 'C:\\fixture\\orca-ssh-no-input-launcher.exe' + try { + const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks()) + await conn.connect() + } finally { + if (previousLauncherPath === undefined) { + delete process.env.ORCA_SSH_WINDOWS_NO_INPUT_LAUNCHER + } else { + process.env.ORCA_SSH_WINDOWS_NO_INPUT_LAUNCHER = previousLauncherPath + } + } + + expect(spawnSystemSshCommandMock).toHaveBeenCalledWith( + expect.objectContaining({ configHost: 'fdpass-host' }), + 'echo ORCA-SYSTEM-SSH-OK', + expect.not.objectContaining({ windowsNoInputLauncherPath: expect.anything() }) + ) + }) + it('allows concurrent exec commands for system SSH with an Orca ControlMaster socket', async () => { getOrcaControlSocketPathMock.mockReturnValue('/tmp/orca-ssh-501/live-socket') vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig()) diff --git a/src/main/ssh/ssh-connection.ts b/src/main/ssh/ssh-connection.ts index 1da01e2f497..adb3aa3762f 100644 --- a/src/main/ssh/ssh-connection.ts +++ b/src/main/ssh/ssh-connection.ts @@ -14,6 +14,7 @@ import { writeBufferViaSystemSsh, writeFileViaSystemSsh, type SystemSshBuildArgsOptions, + type SystemSshCommandOptions, type SystemSshProcess } from './ssh-system-fallback' import { resolveWithSshG, type SshResolvedConfig } from './ssh-config-parser' @@ -46,6 +47,11 @@ type SshRemoteFileOptions = { hostPlatform?: RemoteHostPlatform } +export type SshConnectionSystemSshOptions = { + // Why: native qualification must not connect the launcher to a product/default caller yet. + windowsNoInputLauncherPath?: string +} + // Upper bound on waiting, after an abort, for the in-flight open callback or // for an aborted late-opened channel to finish closing before rejecting // anyway. Normal opens and closes complete in one network round-trip. @@ -77,15 +83,21 @@ export class SshConnection { private state: SshConnectionState private callbacks: SshConnectionCallbacks private target: SshTarget + private readonly systemSshOptions: SshConnectionSystemSshOptions private reconnectTimer: ReturnType | null = null private disposed = false private cachedPassphrase: string | null = null private cachedPassword: string | null = null private connectGeneration = 0 - constructor(target: SshTarget, callbacks: SshConnectionCallbacks) { + constructor( + target: SshTarget, + callbacks: SshConnectionCallbacks, + systemSshOptions: SshConnectionSystemSshOptions = {} + ) { this.target = target this.callbacks = callbacks + this.systemSshOptions = { ...systemSshOptions } this.state = { targetId: target.id, status: 'disconnected', @@ -931,16 +943,24 @@ export class SshConnection { private spawnTrackedSystemSshCommand( command: string, - options?: SshExecOptions & Pick + options?: SshExecOptions & Pick ): ClientChannel { if (options?.signal?.aborted) { throw createSshOperationAbortError() } const buildArgsOptions = this.getSystemSshBuildArgsOptions() + const windowsNoInputLauncherPath = + options?.noInput === true ? this.systemSshOptions.windowsNoInputLauncherPath : undefined const commandOptions = - options === undefined && Object.keys(buildArgsOptions).length === 0 + options === undefined && + Object.keys(buildArgsOptions).length === 0 && + windowsNoInputLauncherPath === undefined ? undefined - : { ...options, ...buildArgsOptions } + : { + ...options, + ...buildArgsOptions, + ...(windowsNoInputLauncherPath === undefined ? {} : { windowsNoInputLauncherPath }) + } const channel = commandOptions === undefined ? spawnSystemSshCommand(this.target, command) diff --git a/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts b/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts index 4c736b38635..78055d5b783 100644 --- a/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +++ b/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts @@ -51,6 +51,7 @@ type Measurement = { const host = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_HOST const user = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_USER const identityFile = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_IDENTITY +const launcherPath = process.env.ORCA_SSH_WINDOWS_NO_INPUT_LAUNCHER const remoteRoot = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_REMOTE_ROOT const serverVersion = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_SERVER_VERSION const fixtureArchiveSha256 = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_ARCHIVE_SHA256 @@ -62,6 +63,7 @@ const hasLiveInput = Boolean( host && user && identityFile && + launcherPath && remoteRoot && serverVersion && fixtureArchiveSha256 && @@ -262,7 +264,12 @@ describe.skipIf(!hasLiveInput)( systemSshConnectionReuse: true, source: 'manual' } - const connection = new SshConnection(target, { onStateChange: () => {} }) + // Why: native evidence must exercise the selected launcher adapter before packaging it. + const connection = new SshConnection( + target, + { onStateChange: () => {} }, + { windowsNoInputLauncherPath: launcherPath as string } + ) const serialStage = stagePath('system-serial', identity) const concurrentStage = stagePath('system-concurrent', identity) const cancelledStage = stagePath('system-cancelled', identity) diff --git a/src/main/ssh/ssh-system-fallback.ts b/src/main/ssh/ssh-system-fallback.ts index b34a7579a1f..e597d67b09b 100644 --- a/src/main/ssh/ssh-system-fallback.ts +++ b/src/main/ssh/ssh-system-fallback.ts @@ -4,7 +4,12 @@ export { getOrcaControlSocketPath, type SystemSshBuildArgsOptions } from './system-ssh-args' -export { spawnSystemSsh, spawnSystemSshCommand, type SystemSshProcess } from './system-ssh-command' +export { + spawnSystemSsh, + spawnSystemSshCommand, + type SystemSshCommandOptions, + type SystemSshProcess +} from './system-ssh-command' export { downloadFileViaSystemSsh, uploadFileViaSystemSsh, diff --git a/src/main/ssh/system-ssh-command-launcher.test.ts b/src/main/ssh/system-ssh-command-launcher.test.ts new file mode 100644 index 00000000000..a6b0606975e --- /dev/null +++ b/src/main/ssh/system-ssh-command-launcher.test.ts @@ -0,0 +1,111 @@ +import { EventEmitter } from 'node:events' +import { PassThrough } from 'node:stream' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { SshTarget } from '../../shared/ssh-types' + +const { existsSyncMock, spawnMock } = vi.hoisted(() => ({ + existsSyncMock: vi.fn(), + spawnMock: vi.fn() +})) + +vi.mock('fs', async (importOriginal) => { + const actual = (await importOriginal()) as Record + return { ...actual, existsSync: existsSyncMock } +}) + +vi.mock('child_process', () => ({ spawn: spawnMock })) + +import { spawnSystemSshCommand } from './system-ssh-command' + +function createTarget(): SshTarget { + return { + id: 'target-1', + label: 'Test Server', + host: 'example.com', + port: 22, + username: 'deploy' + } +} + +function createChildProcess(): EventEmitter & { + stdin: PassThrough + stdout: PassThrough + stderr: PassThrough + pid: number + kill: ReturnType +} { + const child = new EventEmitter() as EventEmitter & { + stdin: PassThrough + stdout: PassThrough + stderr: PassThrough + pid: number + kill: ReturnType + } + child.stdin = new PassThrough() + child.stdout = new PassThrough() + child.stderr = new PassThrough() + child.pid = 12345 + child.kill = vi.fn() + return child +} + +describe('system SSH Windows no-input launcher selection', () => { + beforeEach(() => { + existsSyncMock.mockReset() + existsSyncMock.mockReturnValue(true) + spawnMock.mockReset() + spawnMock.mockReturnValue(createChildProcess()) + }) + + it('uses an explicitly supplied Windows launcher without OpenSSH null input', () => { + const platform = vi.spyOn(process, 'platform', 'get').mockReturnValue('win32') + const launcherPath = 'C:\\fixture\\orca-ssh-no-input-launcher.exe' + const windowsSshPath = 'C:\\Windows\\System32\\OpenSSH\\ssh.exe' + const previousSystemSshPath = process.env.ORCA_SYSTEM_SSH_PATH + process.env.ORCA_SYSTEM_SSH_PATH = windowsSshPath + try { + spawnSystemSshCommand(createTarget(), 'echo ready', { + noInput: true, + windowsNoInputLauncherPath: launcherPath + }) + } finally { + platform.mockRestore() + if (previousSystemSshPath === undefined) { + delete process.env.ORCA_SYSTEM_SSH_PATH + } else { + process.env.ORCA_SYSTEM_SSH_PATH = previousSystemSshPath + } + } + + const args = spawnMock.mock.calls[0][1] as string[] + expect(spawnMock).toHaveBeenCalledWith( + launcherPath, + expect.arrayContaining([ + windowsSshPath, + '--', + 'deploy@example.com', + "exec /bin/sh -c 'echo ready'" + ]), + expect.objectContaining({ stdio: ['ignore', 'pipe', 'pipe'] }) + ) + expect(args).not.toContain('-n') + }) + + it('ignores a supplied launcher on POSIX and preserves OpenSSH no-input handling', () => { + const platform = vi.spyOn(process, 'platform', 'get').mockReturnValue('linux') + try { + spawnSystemSshCommand(createTarget(), 'echo ready', { + noInput: true, + windowsNoInputLauncherPath: '/tmp/not-a-posix-launcher' + }) + } finally { + platform.mockRestore() + } + + expect(spawnMock).toHaveBeenCalledWith( + '/usr/bin/ssh', + expect.arrayContaining(['-n', '--', 'deploy@example.com']), + expect.objectContaining({ stdio: ['ignore', 'pipe', 'pipe'] }) + ) + }) +}) diff --git a/src/main/ssh/system-ssh-command.ts b/src/main/ssh/system-ssh-command.ts index b2bf4f480c9..0f5a84e0b55 100644 --- a/src/main/ssh/system-ssh-command.ts +++ b/src/main/ssh/system-ssh-command.ts @@ -19,7 +19,10 @@ export type SystemSshCommandChannel = ClientChannel & { _process?: ChildProcess } -type SystemSshCommandOptions = SshExecOptions & SystemSshBuildArgsOptions +export type SystemSshCommandOptions = SshExecOptions & + SystemSshBuildArgsOptions & { + windowsNoInputLauncherPath?: string + } /** * Spawn a system ssh process connecting to the given target. @@ -63,14 +66,25 @@ export function spawnSystemSshCommand( const remoteCommand = options?.wrapCommand === false ? command : wrapRemoteCommandForPosixShell(command) const noInput = options?.noInput === true - const stdinMode = noInput && process.platform !== 'win32' ? 'ignore' : 'pipe' - const proc = spawn(sshPath, [...buildSshArgs(target, options), remoteCommand], { + const windowsNoInputLauncherPath = + noInput && process.platform === 'win32' ? options?.windowsNoInputLauncherPath : undefined + const sshArgs = buildSshArgs( + target, + windowsNoInputLauncherPath ? { ...options, noInput: false } : options + ) + const executable = windowsNoInputLauncherPath ?? sshPath + const args = windowsNoInputLauncherPath + ? [sshPath, ...sshArgs, remoteCommand] + : [...sshArgs, remoteCommand] + const stdinMode = + windowsNoInputLauncherPath || (noInput && process.platform !== 'win32') ? 'ignore' : 'pipe' + const proc = spawn(executable, args, { // Why: Win32-OpenSSH can hang when stdin is mapped to NUL; give it a - // pipe whose parent end is closed immediately, as required by #856/#1330. + // proven launcher only when its verified path is explicitly supplied. stdio: [stdinMode, 'pipe', 'pipe'], windowsHide: true }) - if (noInput && process.platform === 'win32') { + if (noInput && process.platform === 'win32' && !windowsNoInputLauncherPath) { proc.stdin?.destroy() } return wrapCommandProcess(proc, !noInput) From 167ec962dbb0269cfdf974ab8617a76a67545de7 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 17 Jul 2026 01:30:19 -0400 Subject: [PATCH 260/267] test(ssh): isolate Windows launcher argument effects Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 17 ++-- ...github-release-implementation-checklist.md | 93 +++++++++++++++++-- src/main/ssh/ssh-connection.test.ts | 2 +- src/main/ssh/ssh-connection.ts | 5 +- src/main/ssh/ssh-system-fallback.ts | 2 + ...sh-system-no-input-windows-openssh.test.ts | 67 ++++++++----- .../ssh/system-ssh-command-launcher.test.ts | 8 +- src/main/ssh/system-ssh-command.ts | 17 +++- 8 files changed, 168 insertions(+), 43 deletions(-) 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 7cf1f1509fa..7e1eb5c768b 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 @@ -10,14 +10,15 @@ A checked box means the work has evidence in the detailed ledger. Design approva complete a box. Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — explicit Windows -no-input launcher-path adapter locally green, exact-head native rerun next, 2026-07-17, Codex implementation owner.** Exact head -`a6feb8c3be0e15ae45fc6b431c1665e5fd84d2c9`, run `29554814775`, and Windows x64/ARM64 jobs -`87804630533`/`87804630504` prove the strict-trust diagnostic naturally completes through the -private-console launcher while the unchanged direct-spawn product probe still hangs after remote -session close. The candidate is constructor-injected only into the native full-size purpose test; -`E-M6-WINDOWS-NOINPUT-LAUNCHER-ADAPTER-LOCAL-001` records syntax, 124 focused cases, 285 release -contracts, typecheck, lint/reliability/max-lines, formatting, diff, and protected-resolver isolation -green. Exact-head native x64/ARM64 full-size and teardown gates remain before any product/default caller. +no-input launcher adapter native selection diagnostic next, 2026-07-17, Codex implementation owner.** +Exact head `42098d1b1393a97c03fec6e7b685859f8296b90d`, run `29555939964`, and Windows x64/ARM64 jobs +`87807875088`/`87807875112` pass every artifact/launcher/cache gate and the standalone strict-trust +launcher in 944.6233/852.6974 ms. The injected `SshConnection` probe still times out in +32211/32140 ms with no lifecycle events before transfer; both teardowns retain locked +`UsrClass.dat`. `E-M6-WINDOWS-NOINPUT-ARGUMENT-MATRIX-LOCAL-001` records non-secret launch-mode +telemetry plus a bounded production/strict/verbose argument matrix locally green with 124 focused +cases, 285 release contracts, typecheck, lint/reliability/max-lines, diff, and resolver isolation. +Commit/push this diagnostic next; no adapter change or product/default caller is authorized yet. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain 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 9cd2d87cdaa..a9191d5f529 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-17
    -Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — explicit Windows no-input launcher-path adapter exact-head native qualification, 2026-07-17, Codex implementation owner**. Exact head `a6feb8c3be0e15ae45fc6b431c1665e5fd84d2c9`, run `29554814775`, and x64/ARM64 jobs `87804630533`/`87804630504` prove strict pinned trust, public-key authentication, command execution, and natural launcher settlement on both architectures. `E-M6-WINDOWS-NOINPUT-LAUNCHER-ADAPTER-LOCAL-001` records the adapter package locally green behind constructor injection for the no-input probe only. Commit/push and exact-head x64/ARM64 full-size transfer and teardown proof remain required. Keep the path absent from every product/default caller and keep legacy as the sole production path. Real Apple/SignPath rehearsals remain deferred.
    -Session checkpoint: **ADAPTER LOCAL GREEN / NATIVE RERUN NEXT — 2026-07-17, Codex implementation owner** — the candidate preserves direct spawn when no path is supplied, ignores the Windows-only path on POSIX, does not infer it from the runner environment, and injects it only from the native full-size purpose test. Syntax, 124 focused cases with ten declared native/live skips, 285 release contracts with eight declared Windows skips, typecheck, full lint/reliability/max-lines, formatting, diff, and protected-resolver isolation pass. No product caller, full-size Windows transfer claim, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — Windows launcher adapter argument-matrix native qualification, 2026-07-17, Codex implementation owner**. Exact head `42098d1b1393a97c03fec6e7b685859f8296b90d`, run `29555939964`, and x64/ARM64 jobs `87807875088`/`87807875112` preserve every artifact/launcher/cache gate and independently prove the standalone strict-trust launcher, but the injected `SshConnection` probe still times out with no lifecycle events before any full-size transfer. `E-M6-WINDOWS-NOINPUT-ARGUMENT-MATRIX-LOCAL-001` adds non-secret launch-mode telemetry and a bounded four-vector native comparison without changing behavior. Commit/push only that diagnostic. Keep the path absent from every product/default caller and keep legacy as the sole production path. Real Apple/SignPath rehearsals remain deferred.
    +Session checkpoint: **ADAPTER NATIVE RED / ARGUMENT MATRIX LOCAL GREEN — 2026-07-17, Codex implementation owner** — the next diagnostic compares production args, strict trust only, verbosity only, and strict plus verbose under the same launcher deadline, while timeout errors report only `direct`, `windows-no-input-launcher`, or `unknown`. Syntax, 124 focused cases with ten declared skips, 285 release contracts with eight declared skips, typecheck, lint/reliability/max-lines, formatting, diff, and protected-resolver isolation pass. The prior x64/ARM64 full-size and teardown gates remain red; no launch behavior, product caller, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
    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) @@ -20116,6 +20116,85 @@ lint` passes in 18.59 seconds real with the 26 existing warnings, all 41 reliabi cancellation settlement, connection reuse, and exact profile/directory teardown. Do not check the broad Milestone 6 Windows cell or add any product/default caller until both jobs pass. +### E-M6-WINDOWS-NOINPUT-LAUNCHER-ADAPTER-CI-RED-001 — injected connection probe still does not settle + +- Date/owner/source/run: 2026-07-17, Codex implementation owner; exact head + `42098d1b1393a97c03fec6e7b685859f8296b90d`, artifact run + [29555939964](https://github.com/stablyai/orca/actions/runs/29555939964), Windows x64 job + [87807875088](https://github.com/stablyai/orca/actions/runs/29555939964/job/87807875088) on + `windows-2022`, and native ARM64 job + [87807875112](https://github.com/stablyai/orca/actions/runs/29555939964/job/87807875112) on + `windows-11-arm64`; both use Node 24.18.0. +- Preserved gates: both Windows jobs pass 101 artifact files, 863 runnable cases, and 19 declared + skips, including legacy .NET compilation, every launcher argv/EOF/binary/overflow/cleanup/ + cancellation contract, exact clean-build identity, runtime smoke, and both full-size cache gates. + Darwin x64/ARM64 jobs `87807875081`/`87807875114`, Linux x64/ARM64 jobs + `87807875099`/`87807875105`, and Linux oldest-userland supplements + `87808435117`/`87808435146` pass at the same exact head. +- Standalone launcher control: x64 completes in 944.6233 ms and ARM64 in 852.6974 ms. Both report + `success=true`, `timedOut=false`, `sentinel=true`, `stdoutEnded=true`, `processExit=0`, + `channelClosed=true`, `closeCode=0`, and `stderrBytes=11090`; their verbose traces prove exact + pinned host trust, public-key authentication, command completion, output drain, and natural + OpenSSH exit. +- Injected connection RED: the immediately following full-size purpose test supplies the same + runner-built launcher path through the new `SshConnection` constructor. X64 still times out in + 32,211 ms and ARM64 in 32,140 ms before transfer with `sentinel=false`, `stdoutEnded=false`, + `processExit=not-observed`, and `channelClosed=false`. Both server logs prove the separate attempt + authenticates, starts its command session, closes that remote session, and then loses the client + connection. No serial/concurrent/collision/cancellation/reuse metrics run. +- Teardown/residual: both fixture teardowns fail visibly after bounded retries because unloading the + owned SID's `_Classes` hive returns exit 1 and `UsrClass.dat` remains locked. This remains a + separate teardown gate and is not hidden by the probe failure. +- Classification/next boundary: unit and source contracts prove intended option propagation, but + this native result does not prove that the spawned full-size probe actually selected the launcher + or explain why its argv differs from the standalone control. Add only non-secret launch-mode + telemetry plus a bounded standalone comparison of the production and strict/verbose argument + vectors. Do not change handles, enable fallback, package the launcher, add a product/default + caller, or claim the broad Windows cell until the exact difference is selected on both + architectures. + +### E-M6-WINDOWS-NOINPUT-ARGUMENT-MATRIX-LOCAL-001 — bounded launch-mode and argv diagnostics are locally green + +- Date/owner/base/runner: 2026-07-17, Codex implementation owner; uncommitted diagnostic atop exact + red head `42098d1b1393a97c03fec6e7b685859f8296b90d` on Apple arm64, macOS 26.2 build 25C56, Node + 26.5.0, and pnpm 10.24.0. The repository requests Node 24; native jobs remain authoritative. +- Boundary: each wrapped system-SSH command now carries only a non-secret + `direct`/`windows-no-input-launcher` launch-mode label. Probe timeouts include that label beside + the existing sentinel/stdout/process/channel booleans; no path or argv content is logged. The + native purpose diagnostic runs four sequential launcher children with the same 6,000 ms internal + deadline: exact production args, strict fixture trust only, verbosity only, and strict trust plus + verbosity. Each child retains the same private-console/bounded-output handles, pinned target, + command, identity, and external settlement bounds. The matrix deliberately expects every vector + to settle so a native failure selects the exact differing vector without hiding RED. +- Behavior isolation: the launcher selection, handles, OpenSSH `-n` policy, connection timeout, + transfer code, fixture, production/default callers, Beta mode, fallback, tuple, publication, and + signing remain unchanged. Unit contracts prove explicit Windows injection reports + `windows-no-input-launcher`, while POSIX ignores the supplied Windows path and reports `direct`. +- Syntax/focused evidence: both launcher scripts pass `node --check`. `/usr/bin/time -l pnpm exec + vitest run --config config/vitest.config.ts --maxWorkers=1 + src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts + src/main/ssh/system-ssh-command-launcher.test.ts + src/main/ssh/ssh-system-no-input-windows-openssh.test.ts + src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts + config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs + config/scripts/ssh-relay-runtime-workflow.test.mjs` passes six files with one skipped, 124 + runnable cases, and ten declared native/live skips in 18.18 seconds real; maximum RSS is + 139,968,512 bytes and peak footprint is 97,785,640 bytes. +- Release/static evidence: the exact unquoted release-contract command passes 51 files and 285 + runnable cases with eight declared Windows skips in 383.15 seconds real; maximum RSS is + 189,906,944 bytes and peak footprint is 97,523,424 bytes. The machine was under sustained load, + so this timing is retained as observed evidence rather than a performance baseline. `/usr/bin/time + -l pnpm typecheck` passes in 110.83 seconds real with 1,795,964,928-byte maximum RSS and + 98,195,288-byte peak footprint. `/usr/bin/time -l pnpm lint` passes in 199.22 seconds real with + the same 26 existing warnings, all 41 reliability gates, and the 355-entry max-lines ratchet; + maximum RSS is 2,102,296,576 bytes and peak footprint is 97,589,080 bytes. +- Formatting/isolation: `pnpm exec oxfmt --write` passes for every modified TypeScript file; + `git diff --check` and exact protected-resolver comparison pass. No Milestone 0 resolver byte + changed. +- Residual gate: local Darwin skips do not classify the native argument matrix or prove launch-mode + selection in the failing connection probe. Commit/push this diagnostic and require exact-head + x64/ARM64 matrix plus timeout-label evidence before any behavior correction. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -20174,11 +20253,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the explicit Windows no-input launcher-path adapter recorded by -`E-M6-WINDOWS-NOINPUT-LAUNCHER-ADAPTER-LOCAL-001`, then require exact-head Windows x64/ARM64 -launcher-backed probe, full-size transfer, cancellation, concurrency, connection -reuse, and exact profile/directory teardown evidence. Do not check the broad Milestone 6 Windows -system-SSH item or add an +Commit and push only the launch-mode and bounded argument-matrix diagnostic recorded by +`E-M6-WINDOWS-NOINPUT-ARGUMENT-MATRIX-LOCAL-001`, then require exact-head Windows x64/ARM64 evidence +before changing the adapter. The full-size transfer, cancellation, concurrency, +connection reuse, and exact profile/directory teardown gates remain open. Do not check the broad +Milestone 6 Windows system-SSH item or add an Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets diff --git a/src/main/ssh/ssh-connection.test.ts b/src/main/ssh/ssh-connection.test.ts index 3b31f55b081..09836a9e1ec 100644 --- a/src/main/ssh/ssh-connection.test.ts +++ b/src/main/ssh/ssh-connection.test.ts @@ -1264,7 +1264,7 @@ describe('SshConnection', () => { try { const connect = expect(conn.connect()).rejects.toThrow( - 'System SSH connection timed out (sentinel=true, stdoutEnded=true, processExit=0, channelClosed=false)' + 'System SSH connection timed out (launchMode=unknown, sentinel=true, stdoutEnded=true, processExit=0, channelClosed=false)' ) await vi.advanceTimersByTimeAsync(0) channel.emit('data', Buffer.from('ORCA-SYSTEM-SSH-OK\r\n')) diff --git a/src/main/ssh/ssh-connection.ts b/src/main/ssh/ssh-connection.ts index adb3aa3762f..88b077b0381 100644 --- a/src/main/ssh/ssh-connection.ts +++ b/src/main/ssh/ssh-connection.ts @@ -14,6 +14,7 @@ import { writeBufferViaSystemSsh, writeFileViaSystemSsh, type SystemSshBuildArgsOptions, + type SystemSshCommandChannel, type SystemSshCommandOptions, type SystemSshProcess } from './ssh-system-fallback' @@ -774,9 +775,11 @@ export class SshConnection { channel.close() // Why: native Windows can separate process exit from inherited // stdio closure; these booleans diagnose that boundary safely. + const launchMode = + (channel as SystemSshCommandChannel)._systemSshLaunchMode ?? 'unknown' reject( new Error( - `System SSH connection timed out (sentinel=${sentinelObserved}, stdoutEnded=${stdoutEnded}, processExit=${processExit}, channelClosed=false)` + `System SSH connection timed out (launchMode=${launchMode}, sentinel=${sentinelObserved}, stdoutEnded=${stdoutEnded}, processExit=${processExit}, channelClosed=false)` ) ) }) diff --git a/src/main/ssh/ssh-system-fallback.ts b/src/main/ssh/ssh-system-fallback.ts index e597d67b09b..9b434a545b7 100644 --- a/src/main/ssh/ssh-system-fallback.ts +++ b/src/main/ssh/ssh-system-fallback.ts @@ -7,6 +7,8 @@ export { export { spawnSystemSsh, spawnSystemSshCommand, + type SystemSshCommandChannel, + type SystemSshCommandLaunchMode, type SystemSshCommandOptions, type SystemSshProcess } from './system-ssh-command' diff --git a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts index 2b3f476ccb8..4d2298e9ae0 100644 --- a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +++ b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts @@ -8,7 +8,11 @@ import type { SshTarget } from '../../shared/ssh-types' import { buildSshArgs } from './system-ssh-args' import { findSystemSsh } from './system-ssh-binary' -type DiagnosticMode = 'private-console-regular-output-verbose-timeout' +type DiagnosticMode = + | 'private-console-regular-output-production-args' + | 'private-console-regular-output-strict-trust' + | 'private-console-regular-output-verbose' + | 'private-console-regular-output-verbose-strict-trust' type DiagnosticResult = { mode: DiagnosticMode @@ -48,36 +52,33 @@ function createTarget(): SshTarget { } } -function buildDiagnosticSshArgs(target: SshTarget, knownHostsPath: string): string[] { +function buildDiagnosticSshArgs( + target: SshTarget, + knownHostsPath: string, + options: { strictTrust: boolean; verbose: boolean } = { strictTrust: true, verbose: true } +): string[] { const sshArgs = buildSshArgs(target) const destinationSeparator = sshArgs.indexOf('--') if (destinationSeparator < 0) { throw new Error('Windows OpenSSH diagnostic arguments lost the destination separator') } // Why: Win32-OpenSSH ignores overridden profile variables; use only the fixture's pinned trust. - sshArgs.splice( - destinationSeparator, - 0, - '-vvv', - '-o', - 'StrictHostKeyChecking=yes', - '-o', - `UserKnownHostsFile=${knownHostsPath}` - ) + const diagnosticArgs = [ + ...(options.verbose ? ['-vvv'] : []), + ...(options.strictTrust + ? ['-o', 'StrictHostKeyChecking=yes', '-o', `UserKnownHostsFile=${knownHostsPath}`] + : []) + ] + sshArgs.splice(destinationSeparator, 0, ...diagnosticArgs) return sshArgs } -async function runDiagnostic(): Promise { +async function runDiagnostic(mode: DiagnosticMode, sshArgs: string[]): Promise { const sshPath = findSystemSsh() if (!sshPath) { throw new Error('Native Windows OpenSSH client is unavailable') } - const mode: DiagnosticMode = 'private-console-regular-output-verbose-timeout' const startedAt = performance.now() - const sshArgs = buildDiagnosticSshArgs( - createTarget(), - join(clientHome as string, '.ssh', 'known_hosts') - ) const child = spawn( launcherPath as string, ['--diagnostic-timeout-ms', '6000', sshPath, ...sshArgs, 'echo ORCA-SYSTEM-SSH-OK'], @@ -204,16 +205,38 @@ describe('Windows OpenSSH no-input diagnostic trust arguments', () => { describe.skipIf(!hasLiveInput)('Windows OpenSSH no-input child-handle diagnostic', () => { it( - 'captures the verbose client phase after a bounded private-console diagnostic timeout', - { timeout: 15_000 }, + 'isolates production, strict-trust, and verbose launcher argument effects', + { timeout: 40_000 }, async () => { expect(process.platform).toBe('win32') - const privateConsoleRegularOutputVerboseTimeout = await runDiagnostic() + const target = createTarget() + const knownHostsPath = join(clientHome as string, '.ssh', 'known_hosts') + const production = await runDiagnostic( + 'private-console-regular-output-production-args', + buildSshArgs(target) + ) + const strictTrust = await runDiagnostic( + 'private-console-regular-output-strict-trust', + buildDiagnosticSshArgs(target, knownHostsPath, { strictTrust: true, verbose: false }) + ) + const verbose = await runDiagnostic( + 'private-console-regular-output-verbose', + buildDiagnosticSshArgs(target, knownHostsPath, { strictTrust: false, verbose: true }) + ) + const verboseStrictTrust = await runDiagnostic( + 'private-console-regular-output-verbose-strict-trust', + buildDiagnosticSshArgs(target, knownHostsPath) + ) console.log( - `ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ privateConsoleRegularOutputVerboseTimeout })}` + `ssh_windows_no_input_argument_matrix=${JSON.stringify({ production, strictTrust, verbose, verboseStrictTrust })}` ) - expect(privateConsoleRegularOutputVerboseTimeout.success).toBe(true) + expect({ + production: production.success, + strictTrust: strictTrust.success, + verbose: verbose.success, + verboseStrictTrust: verboseStrictTrust.success + }).toEqual({ production: true, strictTrust: true, verbose: true, verboseStrictTrust: true }) } ) }) diff --git a/src/main/ssh/system-ssh-command-launcher.test.ts b/src/main/ssh/system-ssh-command-launcher.test.ts index a6b0606975e..d3905fe3150 100644 --- a/src/main/ssh/system-ssh-command-launcher.test.ts +++ b/src/main/ssh/system-ssh-command-launcher.test.ts @@ -63,8 +63,9 @@ describe('system SSH Windows no-input launcher selection', () => { const windowsSshPath = 'C:\\Windows\\System32\\OpenSSH\\ssh.exe' const previousSystemSshPath = process.env.ORCA_SYSTEM_SSH_PATH process.env.ORCA_SYSTEM_SSH_PATH = windowsSshPath + let channel: ReturnType try { - spawnSystemSshCommand(createTarget(), 'echo ready', { + channel = spawnSystemSshCommand(createTarget(), 'echo ready', { noInput: true, windowsNoInputLauncherPath: launcherPath }) @@ -89,12 +90,14 @@ describe('system SSH Windows no-input launcher selection', () => { expect.objectContaining({ stdio: ['ignore', 'pipe', 'pipe'] }) ) expect(args).not.toContain('-n') + expect(channel._systemSshLaunchMode).toBe('windows-no-input-launcher') }) it('ignores a supplied launcher on POSIX and preserves OpenSSH no-input handling', () => { const platform = vi.spyOn(process, 'platform', 'get').mockReturnValue('linux') + let channel: ReturnType try { - spawnSystemSshCommand(createTarget(), 'echo ready', { + channel = spawnSystemSshCommand(createTarget(), 'echo ready', { noInput: true, windowsNoInputLauncherPath: '/tmp/not-a-posix-launcher' }) @@ -107,5 +110,6 @@ describe('system SSH Windows no-input launcher selection', () => { expect.arrayContaining(['-n', '--', 'deploy@example.com']), expect.objectContaining({ stdio: ['ignore', 'pipe', 'pipe'] }) ) + expect(channel._systemSshLaunchMode).toBe('direct') }) }) diff --git a/src/main/ssh/system-ssh-command.ts b/src/main/ssh/system-ssh-command.ts index 0f5a84e0b55..e3c712f6386 100644 --- a/src/main/ssh/system-ssh-command.ts +++ b/src/main/ssh/system-ssh-command.ts @@ -17,8 +17,11 @@ export type SystemSshProcess = { export type SystemSshCommandChannel = ClientChannel & { _process?: ChildProcess + _systemSshLaunchMode?: SystemSshCommandLaunchMode } +export type SystemSshCommandLaunchMode = 'direct' | 'windows-no-input-launcher' + export type SystemSshCommandOptions = SshExecOptions & SystemSshBuildArgsOptions & { windowsNoInputLauncherPath?: string @@ -87,7 +90,11 @@ export function spawnSystemSshCommand( if (noInput && process.platform === 'win32' && !windowsNoInputLauncherPath) { proc.stdin?.destroy() } - return wrapCommandProcess(proc, !noInput) + return wrapCommandProcess( + proc, + !noInput, + windowsNoInputLauncherPath ? 'windows-no-input-launcher' : 'direct' + ) } function wrapChildProcess(proc: ChildProcess): SystemSshProcess { @@ -109,7 +116,11 @@ function wrapChildProcess(proc: ChildProcess): SystemSshProcess { } } -function wrapCommandProcess(proc: ChildProcess, acceptsInput: boolean): SystemSshCommandChannel { +function wrapCommandProcess( + proc: ChildProcess, + acceptsInput: boolean, + launchMode: SystemSshCommandLaunchMode +): SystemSshCommandChannel { const duplex = new Duplex({ read() { proc.stdout?.resume() @@ -128,6 +139,7 @@ function wrapCommandProcess(proc: ChildProcess, acceptsInput: boolean): SystemSs stdin: NodeJS.WritableStream stderr: NodeJS.ReadableStream _process?: ChildProcess + _systemSshLaunchMode?: SystemSshCommandLaunchMode close: () => void } mutableChannel.stdin = @@ -141,6 +153,7 @@ function wrapCommandProcess(proc: ChildProcess, acceptsInput: boolean): SystemSs }) mutableChannel.stderr = proc.stderr! mutableChannel._process = proc + mutableChannel._systemSshLaunchMode = launchMode mutableChannel.close = () => { try { proc.kill('SIGTERM') From 34b51eb12640795d1cdd99a17c6fbadda79c2f60 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 17 Jul 2026 01:56:18 -0400 Subject: [PATCH 261/267] test(ssh): pin Windows qualification host trust Co-authored-by: Orca --- .../ssh-relay-runtime-workflow.test.mjs | 7 +- ...elease-implementation-checklist-summary.md | 24 +++-- ...github-release-implementation-checklist.md | 94 +++++++++++++++++-- src/main/ssh/ssh-connection.test.ts | 19 +++- src/main/ssh/ssh-connection.ts | 5 + ...ndows-system-ssh-openssh-full-size.test.ts | 7 +- src/main/ssh/ssh-system-fallback.test.ts | 34 +++++++ ...sh-system-no-input-windows-openssh.test.ts | 68 +++----------- src/main/ssh/system-ssh-args.ts | 13 +++ 9 files changed, 193 insertions(+), 78 deletions(-) diff --git a/config/scripts/ssh-relay-runtime-workflow.test.mjs b/config/scripts/ssh-relay-runtime-workflow.test.mjs index 6be7aa7e12a..0a41b4b1867 100644 --- a/config/scripts/ssh-relay-runtime-workflow.test.mjs +++ b/config/scripts/ssh-relay-runtime-workflow.test.mjs @@ -157,9 +157,14 @@ describe('SSH relay runtime artifact workflow', () => { const workflow = parse(workflowSource) assertWindowsOpenSshWorkflow(workflow, expect) expect(fullSizeSource).toContain('process.env.ORCA_SSH_WINDOWS_NO_INPUT_LAUNCHER') - expect(fullSizeSource).toContain('{ windowsNoInputLauncherPath: launcherPath as string }') + expect(fullSizeSource).toContain('windowsNoInputLauncherPath: launcherPath as string') + expect(fullSizeSource).toContain( + "strictKnownHostsFile: join(clientHome as string, '.ssh', 'known_hosts')" + ) expect(connectionSource).not.toContain('ORCA_SSH_WINDOWS_NO_INPUT_LAUNCHER') + expect(connectionSource).not.toContain('ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_CLIENT_HOME') expect(managerSource).not.toContain('windowsNoInputLauncherPath') + expect(managerSource).not.toContain('strictKnownHostsFile') }) it('uploads only the first output after two clean builds verify and compare', async () => { 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 7e1eb5c768b..6ad213f8dbd 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 @@ -9,16 +9,20 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — explicit Windows -no-input launcher adapter native selection diagnostic next, 2026-07-17, Codex implementation owner.** -Exact head `42098d1b1393a97c03fec6e7b685859f8296b90d`, run `29555939964`, and Windows x64/ARM64 jobs -`87807875088`/`87807875112` pass every artifact/launcher/cache gate and the standalone strict-trust -launcher in 944.6233/852.6974 ms. The injected `SshConnection` probe still times out in -32211/32140 ms with no lifecycle events before transfer; both teardowns retain locked -`UsrClass.dat`. `E-M6-WINDOWS-NOINPUT-ARGUMENT-MATRIX-LOCAL-001` records non-secret launch-mode -telemetry plus a bounded production/strict/verbose argument matrix locally green with 124 focused -cases, 285 release contracts, typecheck, lint/reliability/max-lines, diff, and resolver isolation. -Commit/push this diagnostic next; no adapter change or product/default caller is authorized yet. +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — explicit pinned-host-trust +qualification correction locally green, exact-head native rerun next, 2026-07-17, Codex implementation +owner.** Exact head +`167ec962dbb0269cfdf974ab8617a76a67545de7`, run `29557508199`, and Windows x64/ARM64 jobs +`87812780534`/`87812780579` independently select the same boundary: launcher-backed production and +verbose-only arguments fail after the 6-second internal deadline because Win32-OpenSSH resolves +`runneradmin` host files, while strict pinned trust succeeds with complete lifecycle settlement in +219.4604/447.5773 ms and strict-plus-verbose succeeds in 243.0841/469.5499 ms. All four primary +non-Windows jobs and both Linux oldest-userland supplements pass. +`E-M6-WINDOWS-PINNED-HOST-TRUST-CORRECTION-LOCAL-001` records the selected correction locally green: +126 focused cases, 285 release contracts, typecheck, full lint/reliability/max-lines, formatting, +diff, protected-resolver isolation, and no product caller pass. Commit/push this exact package and +require x64/ARM64 full-size, cancellation, concurrency, reuse, and exact teardown proof. No upload, +Beta, fallback, tuple, publication, default, or signing behavior is authorized. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain 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 a9191d5f529..21612a369d8 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-17
    -Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — Windows launcher adapter argument-matrix native qualification, 2026-07-17, Codex implementation owner**. Exact head `42098d1b1393a97c03fec6e7b685859f8296b90d`, run `29555939964`, and x64/ARM64 jobs `87807875088`/`87807875112` preserve every artifact/launcher/cache gate and independently prove the standalone strict-trust launcher, but the injected `SshConnection` probe still times out with no lifecycle events before any full-size transfer. `E-M6-WINDOWS-NOINPUT-ARGUMENT-MATRIX-LOCAL-001` adds non-secret launch-mode telemetry and a bounded four-vector native comparison without changing behavior. Commit/push only that diagnostic. Keep the path absent from every product/default caller and keep legacy as the sole production path. Real Apple/SignPath rehearsals remain deferred.
    -Session checkpoint: **ADAPTER NATIVE RED / ARGUMENT MATRIX LOCAL GREEN — 2026-07-17, Codex implementation owner** — the next diagnostic compares production args, strict trust only, verbosity only, and strict plus verbose under the same launcher deadline, while timeout errors report only `direct`, `windows-no-input-launcher`, or `unknown`. Syntax, 124 focused cases with ten declared skips, 285 release contracts with eight declared skips, typecheck, lint/reliability/max-lines, formatting, diff, and protected-resolver isolation pass. The prior x64/ARM64 full-size and teardown gates remain red; no launch behavior, product caller, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — explicit pinned-host-trust correction exact-head native qualification, 2026-07-17, Codex implementation owner**. Exact head `167ec962dbb0269cfdf974ab8617a76a67545de7`, run `29557508199`, and x64/ARM64 jobs `87812780534`/`87812780579` select explicit pinned trust. `E-M6-WINDOWS-PINNED-HOST-TRUST-CORRECTION-LOCAL-001` records the correction locally green: constructor input reaches every command in the live purpose connection, launcher selection remains probe-only, the failing matrix is replaced by one bounded strict-trust control, and no environment or product caller is added. Commit/push only this package and require fresh native x64/ARM64 proof. Legacy remains the sole production path; real Apple/SignPath rehearsals remain deferred.
    +Session checkpoint: **PINNED TRUST CORRECTION LOCAL GREEN / NATIVE RERUN NEXT — 2026-07-17, Codex implementation owner** — 126 focused cases with ten declared skips, 285 release contracts with eight declared skips, typecheck, lint/reliability/max-lines, formatting, diff, protected-resolver isolation, and consumer isolation pass. Full-size transfer, cancellation, concurrency, reuse, and exact profile teardown remain unproven. No product caller, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
    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) @@ -20195,6 +20195,86 @@ lint` passes in 18.59 seconds real with the 26 existing warnings, all 41 reliabi selection in the failing connection probe. Commit/push this diagnostic and require exact-head x64/ARM64 matrix plus timeout-label evidence before any behavior correction. +### E-M6-WINDOWS-NOINPUT-ARGUMENT-MATRIX-CI-RED-001 — native matrix selects explicit pinned host trust + +- Date/owner/source/run: 2026-07-17, Codex implementation owner; exact head + `167ec962dbb0269cfdf974ab8617a76a67545de7`, artifact run + [29557508199](https://github.com/stablyai/orca/actions/runs/29557508199), Windows x64 job + [87812780534](https://github.com/stablyai/orca/actions/runs/29557508199/job/87812780534) on + `windows-2022`, and native ARM64 job + [87812780579](https://github.com/stablyai/orca/actions/runs/29557508199/job/87812780579) on + `windows-11-arm64`; both use Node 24.18.0. +- Preserved gates: both Windows jobs pass 101 artifact files, 863 runnable cases, and 19 declared + skips before the live purpose step, including legacy launcher compilation, all launcher + argv/EOF/binary/overflow/cleanup/cancellation contracts, exact clean-build identity, runtime smoke, + and both full-size cache gates. Darwin x64/ARM64 jobs `87812780560`/`87812780548`, Linux + x64/ARM64 jobs `87812780522`/`87812780553`, and Linux oldest-userland supplements + `87813279471`/`87813279463` pass at the same exact head. +- X64 matrix: production arguments fail after 6110.6764 ms and verbose-only after 6090.3466 ms; + both settle through process/channel close 1 with no sentinel. Strict pinned trust succeeds in + 219.4604 ms and strict-plus-verbose in 243.0841 ms; both observe the sentinel, ended stdout, + process exit 0, channel close 0, and no internal timeout. +- ARM64 matrix: production arguments fail after 6333.9305 ms and verbose-only after 6295.0097 ms + with the same bounded close-1 lifecycle. Strict pinned trust succeeds in 447.5773 ms and + strict-plus-verbose in 469.5499 ms with the complete success lifecycle. +- Classification: both verbose traces show Win32-OpenSSH resolving host files under + `C:\Users\runneradmin` despite workflow HOME/USERPROFILE isolation. The exact fixture + `UserKnownHostsFile` plus `StrictHostKeyChecking=yes` is the only selected argument difference; + verbosity is irrelevant. This proves a qualification-fixture trust-path gap, not a launcher or + runtime-transfer failure. +- Teardown/residual: the matrix deliberately expects all four vectors to succeed, so it stops each + Windows step before the injected `SshConnection` full-size test and no launch-mode or transfer + metric is produced. Both teardowns again detect unload exit 1 for the owned SID's `_Classes` hive, + locked `UsrClass.dat`, and the retained fixture profile directory. Add only explicit + constructor-injected `strictKnownHostsFile` input, propagate it to every command in the live + purpose test, and replace the intentionally failing matrix with a bounded strict-trust control. + Do not add an environment lookup, product/default caller, fallback, Beta, tuple, publication, or + signing behavior. + +### E-M6-WINDOWS-PINNED-HOST-TRUST-CORRECTION-LOCAL-001 — explicit qualification trust is locally green + +- Date/owner/base/runner: 2026-07-17, Codex implementation owner; uncommitted correction atop exact + matrix head `167ec962dbb0269cfdf974ab8617a76a67545de7` on Apple arm64, macOS 26.2 build 25C56, + Node 26.5.0, and pnpm 10.24.0. The repository requests Node 24; native jobs remain authoritative. +- Implementation boundary: `SystemSshBuildArgsOptions.strictKnownHostsFile` emits only + `StrictHostKeyChecking=yes` and the exact `UserKnownHostsFile` before the destination when + explicitly supplied. `SshConnectionSystemSshOptions` carries that value to probe, exec, and file + operations for the native live purpose connection. The Windows no-input launcher remains + constructor-injected only into the probe. The full-size purpose test derives the exact fixture + file from its existing runner input; core code reads no workflow environment value, and the + manager/product/default path supplies neither option. +- Diagnostic retirement: the intentionally failing four-vector native matrix is replaced by one + 6,000 ms internal-deadline standalone launcher control using the same strict trust builder path. + This preserves independent launcher settlement evidence without spending two bounded failures or + preventing the following full-size test from running. +- Focused evidence: after an initial run exposed and corrected three assertion/source-oracle + mismatches, `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts + --maxWorkers=1 src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts + src/main/ssh/system-ssh-command-launcher.test.ts + src/main/ssh/ssh-system-no-input-windows-openssh.test.ts + src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts + config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs + config/scripts/ssh-relay-runtime-workflow.test.mjs` exits 0 with six files passed, one native file + skipped, 126 runnable cases passed, and ten declared native/live skips in 22.65 seconds real; + maximum RSS is 150,585,344 bytes and peak footprint is 97,769,280 bytes. +- Release/static evidence: `/usr/bin/time -l pnpm exec vitest run --config + config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs` exits 0 with 51 + files, 285 runnable cases passed, and eight declared skips in 58.62 seconds real; maximum RSS is + 195,690,496 bytes and peak footprint is 97,539,832 bytes. `/usr/bin/time -l pnpm typecheck` exits + 0 in 11.38 seconds real with 1,774,403,584-byte maximum RSS and 98,293,520-byte peak footprint. + `/usr/bin/time -l pnpm lint` exits 0 in 25.24 seconds real with the existing 26 warnings, all 41 + reliability gates, and the 355-entry max-lines ratchet; maximum RSS is 1,905,852,416 bytes and + peak footprint is 97,949,576 bytes. +- Formatting/isolation: targeted `oxfmt --check`, `git diff --check`, the exact protected-resolver + comparison, and a non-test consumer search pass. The only non-test references are the inert + argument builder/propagation path; no manager, Electron/startup importer, environment lookup, + runtime transfer behavior, upload, default/fallback, Beta, tuple, publication, or signing path is + connected. +- Residual gate: Darwin cannot execute the live Windows control or transfer. Commit/push this exact + correction and require exact-head x64/ARM64 launcher control, injected probe launch mode, + full-size serial/concurrent/collision/cancellation metrics, connection reuse, and exact fixture + profile/directory teardown before advancing. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -20253,11 +20333,11 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the launch-mode and bounded argument-matrix diagnostic recorded by -`E-M6-WINDOWS-NOINPUT-ARGUMENT-MATRIX-LOCAL-001`, then require exact-head Windows x64/ARM64 evidence -before changing the adapter. The full-size transfer, cancellation, concurrency, -connection reuse, and exact profile/directory teardown gates remain open. Do not check the broad -Milestone 6 Windows system-SSH item or add an +Commit and push only the explicit pinned-host-trust qualification correction recorded by +`E-M6-WINDOWS-PINNED-HOST-TRUST-CORRECTION-LOCAL-001`, then require exact-head Windows x64/ARM64 +launcher control, launcher-backed probe, full-size transfer, cancellation, concurrency, connection +reuse, and exact profile/directory teardown evidence. Do not check the broad Milestone 6 Windows +system-SSH item or add an Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets diff --git a/src/main/ssh/ssh-connection.test.ts b/src/main/ssh/ssh-connection.test.ts index 09836a9e1ec..2bf6a6034c9 100644 --- a/src/main/ssh/ssh-connection.test.ts +++ b/src/main/ssh/ssh-connection.test.ts @@ -956,11 +956,13 @@ describe('SshConnection', () => { expect(probeChannel?.stdin.end).toHaveBeenCalledOnce() }) - it('passes an explicit Windows no-input launcher only to the system SSH probe', async () => { + it('limits the explicit launcher to the probe and carries explicit pinned trust to all commands', async () => { vi.mocked(resolveWithSshG).mockResolvedValueOnce(createResolvedConfig()) const launcherPath = 'C:\\fixture\\orca-ssh-no-input-launcher.exe' + const knownHostsPath = 'C:\\fixture\\client-home\\.ssh\\known_hosts' const conn = new SshConnection(createTarget({ configHost: 'fdpass-host' }), createCallbacks(), { - windowsNoInputLauncherPath: launcherPath + windowsNoInputLauncherPath: launcherPath, + strictKnownHostsFile: knownHostsPath }) await conn.connect() @@ -972,14 +974,18 @@ describe('SshConnection', () => { 'echo ORCA-SYSTEM-SSH-OK', expect.objectContaining({ noInput: true, - windowsNoInputLauncherPath: launcherPath + windowsNoInputLauncherPath: launcherPath, + strictKnownHostsFile: knownHostsPath }) ) expect(spawnSystemSshCommandMock).toHaveBeenNthCalledWith( 2, expect.objectContaining({ configHost: 'fdpass-host' }), 'echo after-connect', - expect.not.objectContaining({ windowsNoInputLauncherPath: expect.anything() }) + expect.objectContaining({ strictKnownHostsFile: knownHostsPath }) + ) + expect(spawnSystemSshCommandMock.mock.calls[1][2]).not.toEqual( + expect.objectContaining({ windowsNoInputLauncherPath: expect.anything() }) ) }) @@ -1001,7 +1007,10 @@ describe('SshConnection', () => { expect(spawnSystemSshCommandMock).toHaveBeenCalledWith( expect.objectContaining({ configHost: 'fdpass-host' }), 'echo ORCA-SYSTEM-SSH-OK', - expect.not.objectContaining({ windowsNoInputLauncherPath: expect.anything() }) + expect.not.objectContaining({ + strictKnownHostsFile: expect.anything(), + windowsNoInputLauncherPath: expect.anything() + }) ) }) diff --git a/src/main/ssh/ssh-connection.ts b/src/main/ssh/ssh-connection.ts index 88b077b0381..31acb972fdc 100644 --- a/src/main/ssh/ssh-connection.ts +++ b/src/main/ssh/ssh-connection.ts @@ -51,6 +51,8 @@ type SshRemoteFileOptions = { export type SshConnectionSystemSshOptions = { // Why: native qualification must not connect the launcher to a product/default caller yet. windowsNoInputLauncherPath?: string + // Why: Win32-OpenSSH resolves host files from the token profile, not isolated test HOME values. + strictKnownHostsFile?: string } // Upper bound on waiting, after an abort, for the in-flight open callback or @@ -990,6 +992,9 @@ export class SshConnection { if (this.systemSshControlMasterDisabledForSession) { options.disableControlMaster = true } + if (this.systemSshOptions.strictKnownHostsFile) { + options.strictKnownHostsFile = this.systemSshOptions.strictKnownHostsFile + } return options } diff --git a/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts b/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts index 78055d5b783..b6bcbd00b06 100644 --- a/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +++ b/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts @@ -51,6 +51,7 @@ type Measurement = { const host = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_HOST const user = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_USER const identityFile = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_IDENTITY +const clientHome = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_CLIENT_HOME const launcherPath = process.env.ORCA_SSH_WINDOWS_NO_INPUT_LAUNCHER const remoteRoot = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_REMOTE_ROOT const serverVersion = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_SERVER_VERSION @@ -63,6 +64,7 @@ const hasLiveInput = Boolean( host && user && identityFile && + clientHome && launcherPath && remoteRoot && serverVersion && @@ -268,7 +270,10 @@ describe.skipIf(!hasLiveInput)( const connection = new SshConnection( target, { onStateChange: () => {} }, - { windowsNoInputLauncherPath: launcherPath as string } + { + windowsNoInputLauncherPath: launcherPath as string, + strictKnownHostsFile: join(clientHome as string, '.ssh', 'known_hosts') + } ) const serialStage = stagePath('system-serial', identity) const concurrentStage = stagePath('system-concurrent', identity) diff --git a/src/main/ssh/ssh-system-fallback.test.ts b/src/main/ssh/ssh-system-fallback.test.ts index f896ef847f6..34f2c820fe9 100644 --- a/src/main/ssh/ssh-system-fallback.test.ts +++ b/src/main/ssh/ssh-system-fallback.test.ts @@ -407,6 +407,24 @@ describe('spawnSystemSsh', () => { expect(args).not.toContain('-S') }) + it('places explicit pinned host trust before the destination', () => { + const knownHostsPath = 'C:\\fixture\\client-home\\.ssh\\known_hosts' + const args = buildSshArgs(createTarget(), { strictKnownHostsFile: knownHostsPath }) + const destinationIdx = args.indexOf('--') + + expect(args).toEqual( + expect.arrayContaining([ + '-o', + 'StrictHostKeyChecking=yes', + '-o', + `UserKnownHostsFile=${knownHostsPath}` + ]) + ) + expect(args.indexOf('StrictHostKeyChecking=yes')).toBeLessThan(destinationIdx) + expect(args.indexOf(`UserKnownHostsFile=${knownHostsPath}`)).toBeLessThan(destinationIdx) + expect(args).not.toContain('StrictHostKeyChecking=no') + }) + it('spawns a remote command through the system ssh target', () => { spawnSystemSshCommand(createTarget({ configHost: 'fdpass-host' }), 'echo hello') @@ -559,6 +577,22 @@ describe('spawnSystemSsh', () => { expect(proc.stderr.listenerCount('data')).toBe(0) }) + it('propagates explicit pinned host trust through file operations', async () => { + const proc = createEventedProcess() + const knownHostsPath = 'C:\\fixture\\client-home\\.ssh\\known_hosts' + spawnMock.mockReturnValue(proc) + + const promise = writeFileViaSystemSsh(createTarget(), '/tmp/file', 'contents', { + strictKnownHostsFile: knownHostsPath + }) + proc.emit('close', 0, null) + + await expect(promise).resolves.toBeUndefined() + const args = spawnMock.mock.calls[0][1] as string[] + expect(args).toContain('StrictHostKeyChecking=yes') + expect(args).toContain(`UserKnownHostsFile=${knownHostsPath}`) + }) + it('writes binary buffers to POSIX system SSH targets with exclusive create', async () => { const proc = createEventedProcess() spawnMock.mockReturnValue(proc) diff --git a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts index 4d2298e9ae0..342f7c00abf 100644 --- a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +++ b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts @@ -8,11 +8,7 @@ import type { SshTarget } from '../../shared/ssh-types' import { buildSshArgs } from './system-ssh-args' import { findSystemSsh } from './system-ssh-binary' -type DiagnosticMode = - | 'private-console-regular-output-production-args' - | 'private-console-regular-output-strict-trust' - | 'private-console-regular-output-verbose' - | 'private-console-regular-output-verbose-strict-trust' +type DiagnosticMode = 'private-console-regular-output-strict-trust' type DiagnosticResult = { mode: DiagnosticMode @@ -52,25 +48,9 @@ function createTarget(): SshTarget { } } -function buildDiagnosticSshArgs( - target: SshTarget, - knownHostsPath: string, - options: { strictTrust: boolean; verbose: boolean } = { strictTrust: true, verbose: true } -): string[] { - const sshArgs = buildSshArgs(target) - const destinationSeparator = sshArgs.indexOf('--') - if (destinationSeparator < 0) { - throw new Error('Windows OpenSSH diagnostic arguments lost the destination separator') - } +function buildDiagnosticSshArgs(target: SshTarget, knownHostsPath: string): string[] { // Why: Win32-OpenSSH ignores overridden profile variables; use only the fixture's pinned trust. - const diagnosticArgs = [ - ...(options.verbose ? ['-vvv'] : []), - ...(options.strictTrust - ? ['-o', 'StrictHostKeyChecking=yes', '-o', `UserKnownHostsFile=${knownHostsPath}`] - : []) - ] - sshArgs.splice(destinationSeparator, 0, ...diagnosticArgs) - return sshArgs + return buildSshArgs(target, { strictKnownHostsFile: knownHostsPath }) } async function runDiagnostic(mode: DiagnosticMode, sshArgs: string[]): Promise { @@ -167,8 +147,8 @@ async function runDiagnostic(mode: DiagnosticMode, sshArgs: string[]): Promise { - it('adds only strict fixture trust and verbosity before the destination separator', () => { +describe('Windows OpenSSH no-input pinned trust arguments', () => { + it('adds only strict fixture trust before the destination separator', () => { const target: SshTarget = { id: 'windows-no-input-contract', label: 'windows-no-input-contract', @@ -183,9 +163,8 @@ describe('Windows OpenSSH no-input diagnostic trust arguments', () => { const knownHostsPath = 'C:\\fixture\\client-home\\.ssh\\known_hosts' const productionArgs = buildSshArgs(target) const diagnosticArgs = buildDiagnosticSshArgs(target, knownHostsPath) - const destinationSeparator = productionArgs.indexOf('--') + const insertionIndex = productionArgs.indexOf('-T') + 1 const diagnosticOnlyArgs = [ - '-vvv', '-o', 'StrictHostKeyChecking=yes', '-o', @@ -193,50 +172,31 @@ describe('Windows OpenSSH no-input diagnostic trust arguments', () => { ] expect(diagnosticArgs).toEqual([ - ...productionArgs.slice(0, destinationSeparator), + ...productionArgs.slice(0, insertionIndex), ...diagnosticOnlyArgs, - ...productionArgs.slice(destinationSeparator) + ...productionArgs.slice(insertionIndex) ]) - expect(diagnosticArgs.indexOf('-vvv')).toBeLessThan(diagnosticArgs.indexOf('--')) + expect(diagnosticArgs).not.toContain('-vvv') expect(diagnosticArgs).not.toContain('StrictHostKeyChecking=no') expect(buildSshArgs(target)).toEqual(productionArgs) }) }) -describe.skipIf(!hasLiveInput)('Windows OpenSSH no-input child-handle diagnostic', () => { +describe.skipIf(!hasLiveInput)('Windows OpenSSH no-input pinned-trust control', () => { it( - 'isolates production, strict-trust, and verbose launcher argument effects', - { timeout: 40_000 }, + 'settles the launcher with the exact pinned fixture host file', + { timeout: 15_000 }, async () => { expect(process.platform).toBe('win32') const target = createTarget() const knownHostsPath = join(clientHome as string, '.ssh', 'known_hosts') - const production = await runDiagnostic( - 'private-console-regular-output-production-args', - buildSshArgs(target) - ) const strictTrust = await runDiagnostic( 'private-console-regular-output-strict-trust', - buildDiagnosticSshArgs(target, knownHostsPath, { strictTrust: true, verbose: false }) - ) - const verbose = await runDiagnostic( - 'private-console-regular-output-verbose', - buildDiagnosticSshArgs(target, knownHostsPath, { strictTrust: false, verbose: true }) - ) - const verboseStrictTrust = await runDiagnostic( - 'private-console-regular-output-verbose-strict-trust', buildDiagnosticSshArgs(target, knownHostsPath) ) - console.log( - `ssh_windows_no_input_argument_matrix=${JSON.stringify({ production, strictTrust, verbose, verboseStrictTrust })}` - ) + console.log(`ssh_windows_no_input_pinned_trust=${JSON.stringify({ strictTrust })}`) - expect({ - production: production.success, - strictTrust: strictTrust.success, - verbose: verbose.success, - verboseStrictTrust: verboseStrictTrust.success - }).toEqual({ production: true, strictTrust: true, verbose: true, verboseStrictTrust: true }) + expect(strictTrust.success).toBe(true) } ) }) diff --git a/src/main/ssh/system-ssh-args.ts b/src/main/ssh/system-ssh-args.ts index ab9391e3629..48c71b4bfc8 100644 --- a/src/main/ssh/system-ssh-args.ts +++ b/src/main/ssh/system-ssh-args.ts @@ -6,6 +6,7 @@ export type SystemSshBuildArgsOptions = { disableControlMaster?: boolean suppressOrcaControlMaster?: boolean noInput?: boolean + strictKnownHostsFile?: string } export function buildSshArgs(target: SshTarget, options?: SystemSshBuildArgsOptions): string[] { @@ -19,6 +20,15 @@ export function buildSshArgs(target: SshTarget, options?: SystemSshBuildArgsOpti // OpenSSH's input worker for commands that can never consume stdin. args.push('-n') } + if (options?.strictKnownHostsFile) { + // Why: native fixtures need explicit pinned trust because Win32-OpenSSH ignores HOME overrides. + args.push( + '-o', + 'StrictHostKeyChecking=yes', + '-o', + `UserKnownHostsFile=${options.strictKnownHostsFile}` + ) + } // Why: ControlMaster multiplexes all SSH exec commands over a single connection, // eliminating the ~9s handshake overhead per command. Without this, each @@ -100,6 +110,9 @@ export function getSystemSshBuildArgsFromOperationOptions( if (options?.noInput === true) { buildArgsOptions.noInput = true } + if (options?.strictKnownHostsFile) { + buildArgsOptions.strictKnownHostsFile = options.strictKnownHostsFile + } return Object.keys(buildArgsOptions).length === 0 ? undefined : buildArgsOptions } From ad3c3975c099f0e197661091b1cec9b0a0d6d553 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 17 Jul 2026 02:15:54 -0400 Subject: [PATCH 262/267] fix(ssh): route runtime commands by remote dialect Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 27 +++--- ...github-release-implementation-checklist.md | 84 +++++++++++++++++-- ...posix-system-ssh-openssh-full-size.test.ts | 2 +- .../ssh-relay-runtime-posix-tree-transfer.ts | 2 +- ...ay-runtime-system-ssh-file-channel.test.ts | 40 ++++++++- ...h-relay-runtime-system-ssh-file-channel.ts | 20 +++-- ...ndows-system-ssh-openssh-full-size.test.ts | 2 +- ...ssh-relay-runtime-windows-tree-transfer.ts | 2 +- 8 files changed, 148 insertions(+), 31 deletions(-) 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 6ad213f8dbd..1f7a09df016 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 @@ -9,20 +9,21 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — explicit pinned-host-trust -qualification correction locally green, exact-head native rerun next, 2026-07-17, Codex implementation +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — explicit remote-command +dialect correction locally green, exact-head native rerun next, 2026-07-17, Codex implementation owner.** Exact head -`167ec962dbb0269cfdf974ab8617a76a67545de7`, run `29557508199`, and Windows x64/ARM64 jobs -`87812780534`/`87812780579` independently select the same boundary: launcher-backed production and -verbose-only arguments fail after the 6-second internal deadline because Win32-OpenSSH resolves -`runneradmin` host files, while strict pinned trust succeeds with complete lifecycle settlement in -219.4604/447.5773 ms and strict-plus-verbose succeeds in 243.0841/469.5499 ms. All four primary -non-Windows jobs and both Linux oldest-userland supplements pass. -`E-M6-WINDOWS-PINNED-HOST-TRUST-CORRECTION-LOCAL-001` records the selected correction locally green: -126 focused cases, 285 release contracts, typecheck, full lint/reliability/max-lines, formatting, -diff, protected-resolver isolation, and no product caller pass. Commit/push this exact package and -require x64/ARM64 full-size, cancellation, concurrency, reuse, and exact teardown proof. No upload, -Beta, fallback, tuple, publication, default, or signing behavior is authorized. +`34b51eb12640795d1cdd99a17c6fbadda79c2f60`, run `29558649829`, and Windows x64/ARM64 jobs +`87816177268`/`87816177284` prove pinned launcher control in 734.8928/681.1723 ms and advance the +injected connection beyond its probe. Both then fail the first Windows staging command because the +generic system-SSH file channel applies the POSIX `exec /bin/sh -c` wrapper to an already complete +PowerShell command; no payload byte or transfer metric runs. All four primary non-Windows jobs and +both Linux oldest-userland supplements pass. Add only an explicit `posix`/`powershell` command +dialect at the file-channel boundary, preserving the exact POSIX call and using `wrapCommand: false` +only for PowerShell. `E-M6-WINDOWS-COMMAND-DIALECT-CORRECTION-LOCAL-001` records 40 focused cases, +285 release contracts, typecheck, full lint/reliability/max-lines, formatting, exact call-site audit, +diff, and resolver isolation green. Commit/push this exact package and require native full-size, +cancellation, concurrency, reuse, and exact teardown proof. No product/default caller, Beta, +fallback, tuple, publication, or signing behavior is authorized. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain 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 21612a369d8..1d5486fd8ad 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-17
    -Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — explicit pinned-host-trust correction exact-head native qualification, 2026-07-17, Codex implementation owner**. Exact head `167ec962dbb0269cfdf974ab8617a76a67545de7`, run `29557508199`, and x64/ARM64 jobs `87812780534`/`87812780579` select explicit pinned trust. `E-M6-WINDOWS-PINNED-HOST-TRUST-CORRECTION-LOCAL-001` records the correction locally green: constructor input reaches every command in the live purpose connection, launcher selection remains probe-only, the failing matrix is replaced by one bounded strict-trust control, and no environment or product caller is added. Commit/push only this package and require fresh native x64/ARM64 proof. Legacy remains the sole production path; real Apple/SignPath rehearsals remain deferred.
    -Session checkpoint: **PINNED TRUST CORRECTION LOCAL GREEN / NATIVE RERUN NEXT — 2026-07-17, Codex implementation owner** — 126 focused cases with ten declared skips, 285 release contracts with eight declared skips, typecheck, lint/reliability/max-lines, formatting, diff, protected-resolver isolation, and consumer isolation pass. Full-size transfer, cancellation, concurrency, reuse, and exact profile teardown remain unproven. No product caller, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — explicit remote-command dialect correction exact-head native qualification, 2026-07-17, Codex implementation owner**. Exact head `34b51eb12640795d1cdd99a17c6fbadda79c2f60`, run `29558649829`, and x64/ARM64 jobs `87816177268`/`87816177284` select a required `posix`/`powershell` file-channel dialect. `E-M6-WINDOWS-COMMAND-DIALECT-CORRECTION-LOCAL-001` records the correction locally green: POSIX retains `connection.exec(command)` exactly and PowerShell alone uses `{ wrapCommand: false }`. Commit/push only this package and require fresh native x64/ARM64 proof. Legacy remains the sole production path; real Apple/SignPath rehearsals remain deferred.
    +Session checkpoint: **COMMAND DIALECT CORRECTION LOCAL GREEN / NATIVE RERUN NEXT — 2026-07-17, Codex implementation owner** — 40 focused cases with two declared live skips, 285 release contracts with eight declared skips, typecheck, lint/reliability/max-lines, formatting, exact call-site audit, diff, and protected-resolver isolation pass. Full-size transfer, cancellation, concurrency, reuse, and exact profile teardown remain unproven. No product caller, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
    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) @@ -20275,6 +20275,77 @@ lint` passes in 18.59 seconds real with the 26 existing warnings, all 41 reliabi full-size serial/concurrent/collision/cancellation metrics, connection reuse, and exact fixture profile/directory teardown before advancing. +### E-M6-WINDOWS-PINNED-HOST-TRUST-CI-RED-001 — probe passes and exposes PowerShell wrapping defect + +- Date/owner/source/run: 2026-07-17, Codex implementation owner; exact head + `34b51eb12640795d1cdd99a17c6fbadda79c2f60`, artifact run + [29558649829](https://github.com/stablyai/orca/actions/runs/29558649829), Windows x64 job + [87816177268](https://github.com/stablyai/orca/actions/runs/29558649829/job/87816177268) on + `windows-2022`, and native ARM64 job + [87816177284](https://github.com/stablyai/orca/actions/runs/29558649829/job/87816177284) on + `windows-11-arm64`; both use Node 24.18.0. +- Preserved gates: both Windows jobs pass 101 artifact files, 863 runnable cases, and 19 declared + skips, including launcher compilation and all launcher lifecycle/cancellation/overflow contracts, + exact clean builds, runtime smoke, and both full-size cache gates. Darwin x64/ARM64 jobs + `87816177263`/`87816177289`, Linux x64/ARM64 jobs `87816177277`/`87816177290`, and Linux + oldest-userland supplements `87817034753`/`87817034773` pass at the same exact head. +- Launcher/probe proof: the strict pinned launcher control reports the complete sentinel/stdout/ + process/channel success lifecycle in 734.8928 ms on x64 and 681.1723 ms on ARM64. Each following + full-size test advances through `connection.connect()` and reaches the first staging control + command, proving the constructor-injected launcher plus strict trust no longer blocks the probe. +- Exact transfer RED: x64 fails after 1.520 seconds and ARM64 after 1.667 seconds with + `SSH relay runtime system SSH file command failed (exit 1): 'exec' is not recognized as an + internal or external command, operable program or batch file.` The Windows staging layer already + produced a complete encoded PowerShell command, but the generic system-SSH file channel called + `connection.exec(command)` with its default POSIX wrapper. No staging root, payload byte, + serial/concurrent/collision/cancellation measurement, or connection-reuse metric runs. +- Teardown/residual: both bounded teardowns again detect unload exit 1 for the owned SID's + `_Classes` hive, locked `UsrClass.dat`, and the retained fixture profile directory. Add only an + explicit remote-command dialect to the file-channel boundary: POSIX must retain the exact existing + exec call and PowerShell must call `connection.exec(command, { wrapCommand: false })`. Require + focused POSIX/PowerShell routing contracts and fresh x64/ARM64 full-size/teardown evidence before + advancing. Do not connect product/default, fallback, Beta, tuple, publication, or signing paths. + +### E-M6-WINDOWS-COMMAND-DIALECT-CORRECTION-LOCAL-001 — explicit POSIX/PowerShell routing is locally green + +- Date/owner/base/runner: 2026-07-17, Codex implementation owner; uncommitted correction atop exact + native red head `34b51eb12640795d1cdd99a17c6fbadda79c2f60` on Apple arm64, macOS 26.2 build + 25C56, Node 26.5.0, and pnpm 10.24.0. The repository requests Node 24; native jobs remain + authoritative. +- Implementation boundary: `openSshRelayRuntimeSystemSshFileChannel` now requires the concrete + `RemoteCommandDialect`. POSIX tree/live callers pass `posix` and retain the exact prior + `connection.exec(command)` call. Windows tree/live callers pass `powershell` and call only + `connection.exec(command, { wrapCommand: false })` because the staging/file layers already emit + complete encoded PowerShell commands. Input validation rejects any other dialect. SSH auth, + connection reuse, channel adaptation, byte streaming, cancellation ownership, diagnostics, + launcher selection, strict trust, and every product/default caller remain unchanged. +- Focused evidence: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts + --maxWorkers=1 src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts + src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts + src/main/ssh/ssh-relay-runtime-windows-tree-transfer.test.ts + src/main/ssh/ssh-relay-runtime-posix-system-ssh-openssh-full-size.test.ts + src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts + config/scripts/ssh-relay-runtime-workflow.test.mjs` exits 0 with four files passed, two live files + skipped, 40 runnable cases passed, and two declared skips in 4.35 seconds real; maximum RSS is + 173,981,696 bytes and peak footprint is 97,801,928 bytes. The routing contract independently + asserts the exact POSIX one-argument call and PowerShell `{ wrapCommand: false }` call. +- Release/static evidence: `/usr/bin/time -l pnpm exec vitest run --config + config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs` exits 0 with 51 + files, 285 runnable cases passed, and eight declared skips in 20.51 seconds real; maximum RSS is + 195,592,192 bytes and peak footprint is 97,146,712 bytes. `/usr/bin/time -l pnpm typecheck` exits + 0 in 7.16 seconds real with 1,229,029,376-byte maximum RSS and 97,998,656-byte peak footprint. + `/usr/bin/time -l pnpm lint` exits 0 in 50.55 seconds real with the existing 26 warnings, all 41 + reliability gates, and the 355-entry max-lines ratchet; maximum RSS is 2,049,720,320 bytes and + peak footprint is 97,654,616 bytes. +- Formatting/isolation: targeted `oxfmt --check`, `git diff --check`, exact protected-resolver + comparison, and an exact non-unit call-site audit pass. The only four callers are the POSIX and + Windows tree-transfer modules plus their native full-size reuse controls, and every caller names + its dialect. No manager, Electron/startup importer, upload, fallback, Beta, tuple, publication, + default, or signing path is connected. +- Residual gate: local Darwin skips both live transfer files. Commit/push this exact correction and + require exact-head x64/ARM64 full-size serial/concurrent/collision/cancellation metrics, + connection reuse, and exact fixture profile/directory teardown before advancing. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -20333,11 +20404,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the explicit pinned-host-trust qualification correction recorded by -`E-M6-WINDOWS-PINNED-HOST-TRUST-CORRECTION-LOCAL-001`, then require exact-head Windows x64/ARM64 -launcher control, launcher-backed probe, full-size transfer, cancellation, concurrency, connection -reuse, and exact profile/directory teardown evidence. Do not check the broad Milestone 6 Windows -system-SSH item or add an +Commit and push only the explicit command-dialect correction recorded by +`E-M6-WINDOWS-COMMAND-DIALECT-CORRECTION-LOCAL-001`, then require exact-head Windows x64/ARM64 +full-size transfer, cancellation, concurrency, connection reuse, and exact profile/directory +teardown evidence. Do not check the broad Milestone 6 Windows system-SSH item or add an Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets diff --git a/src/main/ssh/ssh-relay-runtime-posix-system-ssh-openssh-full-size.test.ts b/src/main/ssh/ssh-relay-runtime-posix-system-ssh-openssh-full-size.test.ts index 586caede435..a98a0d5953d 100644 --- a/src/main/ssh/ssh-relay-runtime-posix-system-ssh-openssh-full-size.test.ts +++ b/src/main/ssh/ssh-relay-runtime-posix-system-ssh-openssh-full-size.test.ts @@ -331,7 +331,7 @@ describe.skipIf(!hasLiveInput)( command: 'true', signal: new AbortController().signal, openChannel: (command, signal) => - openSshRelayRuntimeSystemSshFileChannel(connection, command, signal) + openSshRelayRuntimeSystemSshFileChannel(connection, command, signal, 'posix') }) expect(connection.getState().status).toBe('connected') diff --git a/src/main/ssh/ssh-relay-runtime-posix-tree-transfer.ts b/src/main/ssh/ssh-relay-runtime-posix-tree-transfer.ts index 9e451c72e78..0ebf52d0020 100644 --- a/src/main/ssh/ssh-relay-runtime-posix-tree-transfer.ts +++ b/src/main/ssh/ssh-relay-runtime-posix-tree-transfer.ts @@ -137,7 +137,7 @@ export async function transferSshRelayRuntimeTreeViaPosixSystemSsh( const { tree, connection, signal, onProgress } = options const remoteStagingRoot = normalizeStagingRoot(options.remoteStagingRoot) const openChannel: OpenChannel = (command, exactSignal) => - openSshRelayRuntimeSystemSshFileChannel(connection, command, exactSignal) + openSshRelayRuntimeSystemSshFileChannel(connection, command, exactSignal, 'posix') const runControl = (command: string): Promise => runSshRelayRuntimePosixControlCommand({ command, signal, openChannel }) signal.throwIfAborted() diff --git a/src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts b/src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts index 567c40cd488..1e1788748e6 100644 --- a/src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts +++ b/src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts @@ -7,7 +7,7 @@ import { SSH_RELAY_RUNTIME_POSIX_FILE_DESTINATION_LIMITS } from './ssh-relay-runtime-posix-file-destination' import { - openSshRelayRuntimeSystemSshFileChannel, + openSshRelayRuntimeSystemSshFileChannel as openSystemSshFileChannelWithDialect, SSH_RELAY_RUNTIME_SYSTEM_SSH_FILE_CHANNEL_LIMITS, type SshRelayRuntimeSystemSshConnection } from './ssh-relay-runtime-system-ssh-file-channel' @@ -62,6 +62,14 @@ function createConnection( } } +function openSshRelayRuntimeSystemSshFileChannel( + connection: SshRelayRuntimeSystemSshConnection, + command: string, + signal: AbortSignal +) { + return openSystemSshFileChannelWithDialect(connection, command, signal, 'posix') +} + afterEach(() => { vi.useRealTimers() }) @@ -111,6 +119,36 @@ describe('SSH relay runtime system-SSH file channel', () => { expect(channel.stderr.listenerCount('error')).toBe(0) }) + it('preserves POSIX wrapping and disables it only for complete PowerShell commands', async () => { + const posix = createChannel() + const posixConnection = createConnection(posix.channel) + const posixAdapted = await openSystemSshFileChannelWithDialect( + posixConnection, + 'cat command', + new AbortController().signal, + 'posix' + ) + + expect(posixConnection.exec).toHaveBeenCalledWith('cat command') + posix.channel.emit('close', 0, null) + await expect(posixAdapted.settled).resolves.toBeUndefined() + + const powershell = createChannel() + const powershellConnection = createConnection(powershell.channel) + const powershellAdapted = await openSystemSshFileChannelWithDialect( + powershellConnection, + 'powershell.exe -EncodedCommand AAAA', + new AbortController().signal, + 'powershell' + ) + + expect(powershellConnection.exec).toHaveBeenCalledWith('powershell.exe -EncodedCommand AAAA', { + wrapCommand: false + }) + powershell.channel.emit('close', 0, null) + await expect(powershellAdapted.settled).resolves.toBeUndefined() + }) + it('forwards the exact borrowed chunk callback and EOF to child stdin', async () => { const { channel } = createChannel() let retainedCallback: WriteCallback | undefined diff --git a/src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.ts b/src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.ts index daba2e3189c..d1ef217ad03 100644 --- a/src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.ts +++ b/src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.ts @@ -1,10 +1,12 @@ import type { ClientChannel } from 'ssh2' import type { SshRelayRuntimeCommandFileChannel } from './ssh-relay-runtime-command-file-destination' +import type { SshExecOptions } from './ssh-connection-utils' +import type { RemoteCommandDialect } from './ssh-remote-platform' import type { SystemSshCommandChannel } from './system-ssh-command' export type SshRelayRuntimeSystemSshConnection = Readonly<{ usesSystemSshTransport: () => boolean - exec: (command: string) => Promise + exec: (command: string, options?: Pick) => Promise }> export const SSH_RELAY_RUNTIME_SYSTEM_SSH_FILE_CHANNEL_LIMITS = Object.freeze({ @@ -17,7 +19,8 @@ type ChannelCloseSignal = NodeJS.Signals | null | undefined function validateInput( connection: SshRelayRuntimeSystemSshConnection, command: string, - signal: AbortSignal + signal: AbortSignal, + commandDialect: RemoteCommandDialect ): void { if ( !connection || @@ -25,7 +28,8 @@ function validateInput( typeof connection.exec !== 'function' || typeof command !== 'string' || command === '' || - !signal + !signal || + (commandDialect !== 'posix' && commandDialect !== 'powershell') ) { throw new Error('SSH relay runtime system SSH file channel input is invalid') } @@ -183,10 +187,14 @@ function adaptChannel(channel: SystemSshCommandChannel): SshRelayRuntimeCommandF export async function openSshRelayRuntimeSystemSshFileChannel( connection: SshRelayRuntimeSystemSshConnection, command: string, - signal: AbortSignal + signal: AbortSignal, + commandDialect: RemoteCommandDialect ): Promise { - validateInput(connection, command, signal) + validateInput(connection, command, signal, commandDialect) // Why: the file destination owns this signal's bounded teardown; forwarding it would double-kill. - const channel = (await connection.exec(command)) as SystemSshCommandChannel + // Why: Windows staging already supplies a complete PowerShell command; POSIX still needs /bin/sh. + const channel = (await (commandDialect === 'powershell' + ? connection.exec(command, { wrapCommand: false }) + : connection.exec(command))) as SystemSshCommandChannel return adaptChannel(channel) } diff --git a/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts b/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts index b6bcbd00b06..ab815835c97 100644 --- a/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +++ b/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts @@ -349,7 +349,7 @@ describe.skipIf(!hasLiveInput)( remoteRoot: cancelledStage, signal: new AbortController().signal, openChannel: (command, signal) => - openSshRelayRuntimeSystemSshFileChannel(connection, command, signal) + openSshRelayRuntimeSystemSshFileChannel(connection, command, signal, 'powershell') }) expect(connection.getState().status).toBe('connected') diff --git a/src/main/ssh/ssh-relay-runtime-windows-tree-transfer.ts b/src/main/ssh/ssh-relay-runtime-windows-tree-transfer.ts index bea22050d55..a6dbb2b53cf 100644 --- a/src/main/ssh/ssh-relay-runtime-windows-tree-transfer.ts +++ b/src/main/ssh/ssh-relay-runtime-windows-tree-transfer.ts @@ -131,7 +131,7 @@ export async function transferSshRelayRuntimeTreeViaWindowsSystemSsh( const { tree, connection, signal, onProgress } = options const remoteStagingRoot = normalizeStagingRoot(options.remoteStagingRoot) const openChannel: OpenChannel = (command, exactSignal) => - openSshRelayRuntimeSystemSshFileChannel(connection, command, exactSignal) + openSshRelayRuntimeSystemSshFileChannel(connection, command, exactSignal, 'powershell') const runControl = ( control: Omit ): Promise => runSshRelayRuntimeWindowsStagingControl({ ...control, signal, openChannel }) From 19683ba24e8af031a6a02583b4f33e0746efb9ac Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 17 Jul 2026 02:55:33 -0400 Subject: [PATCH 263/267] fix(ssh): frame Windows runtime transfer completion Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 25 +++--- ...github-release-implementation-checklist.md | 85 +++++++++++++++++-- ...y-runtime-windows-file-destination.test.ts | 43 ++++++++-- ...-relay-runtime-windows-file-destination.ts | 54 +++++++++++- ...ay-runtime-windows-staging-control.test.ts | 9 +- ...h-relay-runtime-windows-staging-control.ts | 13 ++- 6 files changed, 193 insertions(+), 36 deletions(-) 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 1f7a09df016..8a0f6a6e6cb 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 @@ -9,21 +9,16 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — explicit remote-command -dialect correction locally green, exact-head native rerun next, 2026-07-17, Codex implementation -owner.** Exact head -`34b51eb12640795d1cdd99a17c6fbadda79c2f60`, run `29558649829`, and Windows x64/ARM64 jobs -`87816177268`/`87816177284` prove pinned launcher control in 734.8928/681.1723 ms and advance the -injected connection beyond its probe. Both then fail the first Windows staging command because the -generic system-SSH file channel applies the POSIX `exec /bin/sh -c` wrapper to an already complete -PowerShell command; no payload byte or transfer metric runs. All four primary non-Windows jobs and -both Linux oldest-userland supplements pass. Add only an explicit `posix`/`powershell` command -dialect at the file-channel boundary, preserving the exact POSIX call and using `wrapCommand: false` -only for PowerShell. `E-M6-WINDOWS-COMMAND-DIALECT-CORRECTION-LOCAL-001` records 40 focused cases, -285 release contracts, typecheck, full lint/reliability/max-lines, formatting, exact call-site audit, -diff, and resolver isolation green. Commit/push this exact package and require native full-size, -cancellation, concurrency, reuse, and exact teardown proof. No product/default caller, Beta, -fallback, tuple, publication, or signing behavior is authorized. +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — explicit Windows +completion framing locally green, exact-head native rerun next, 2026-07-17, Codex implementation +owner.** Exact head `ad3c3975c099f0e197661091b1cec9b0a0d6d553`, run `29559577952`, and Windows +x64/ARM64 jobs `87818965702`/`87818965757` independently time out after 1,200,018/1,200,015 ms with +one fixture-user command chain retained. The isolated correction replaces final stdin EOF reads with +an exact `ORCAEND1` frame while preserving local size and remote frame validation. Local evidence is +green through 54 focused cases, 697 broad relay cases, 285 release contracts, typecheck, full +lint/reliability/max-lines, formatting, diff, and resolver isolation. Commit/push only this package +and require fresh x64/ARM64 full-size, cancellation, concurrency, reuse, and exact teardown proof. +No product/default caller, Beta, fallback, tuple, publication, or signing behavior is authorized. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain 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 1d5486fd8ad..31b087f415f 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-17
    -Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — explicit remote-command dialect correction exact-head native qualification, 2026-07-17, Codex implementation owner**. Exact head `34b51eb12640795d1cdd99a17c6fbadda79c2f60`, run `29558649829`, and x64/ARM64 jobs `87816177268`/`87816177284` select a required `posix`/`powershell` file-channel dialect. `E-M6-WINDOWS-COMMAND-DIALECT-CORRECTION-LOCAL-001` records the correction locally green: POSIX retains `connection.exec(command)` exactly and PowerShell alone uses `{ wrapCommand: false }`. Commit/push only this package and require fresh native x64/ARM64 proof. Legacy remains the sole production path; real Apple/SignPath rehearsals remain deferred.
    -Session checkpoint: **COMMAND DIALECT CORRECTION LOCAL GREEN / NATIVE RERUN NEXT — 2026-07-17, Codex implementation owner** — 40 focused cases with two declared live skips, 285 release contracts with eight declared skips, typecheck, lint/reliability/max-lines, formatting, exact call-site audit, diff, and protected-resolver isolation pass. Full-size transfer, cancellation, concurrency, reuse, and exact profile teardown remain unproven. No product caller, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — explicit Windows completion framing locally green, exact-head native qualification next, 2026-07-17, Codex implementation owner**. Exact correction head `ad3c3975c099f0e197661091b1cec9b0a0d6d553`, run `29559577952`, and Windows x64/ARM64 jobs `87818965702`/`87818965757` independently time out after 1,200,018/1,200,015 ms with a fixture-user command chain still waiting. `E-M6-WINDOWS-FRAMED-COMPLETION-LOCAL-001` replaces the receiver's EOF dependency with an exact `ORCAEND1` frame while retaining local byte-count and remote frame validation; focused, broad, release, typecheck, lint, formatting, diff, and resolver-isolation gates pass. Commit/push only this package and require fresh native full-size/cancellation/concurrency/reuse/teardown proof. Legacy remains the sole production path; real Apple/SignPath rehearsals remain deferred.
    +Session checkpoint: **WINDOWS FRAMED COMPLETION LOCAL GREEN / NATIVE RERUN NEXT — 2026-07-17, Codex implementation owner** — 54 focused cases with three declared skips, 697 broad relay cases with ten declared skips, 285 release contracts with eight declared skips, typecheck, lint/reliability/max-lines, formatting, diff, and protected-resolver isolation pass. Both PowerShell receivers are now length-delimited and completion-framed, with no stdin-EOF read; local short/long payload mismatches abort boundedly. Native proof remains required. No product caller, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
    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) @@ -20346,6 +20346,79 @@ lint` passes in 18.59 seconds real with the 26 existing warnings, all 41 reliabi require exact-head x64/ARM64 full-size serial/concurrent/collision/cancellation metrics, connection reuse, and exact fixture profile/directory teardown before advancing. +### E-M6-WINDOWS-COMMAND-DIALECT-CI-RED-001 — both architectures retain an EOF-dependent receiver + +- Date/owner/source/run: 2026-07-17, Codex implementation owner; exact head + `ad3c3975c099f0e197661091b1cec9b0a0d6d553`, artifact run + [29559577952](https://github.com/stablyai/orca/actions/runs/29559577952), Windows x64 job + [87818965702](https://github.com/stablyai/orca/actions/runs/29559577952/job/87818965702) on + `windows-2022`, and native ARM64 job + [87818965757](https://github.com/stablyai/orca/actions/runs/29559577952/job/87818965757) on + `windows-11-arm`; both use Node 24.18.0 and official OpenSSH 10.0p2. X64 uses PowerShell + 5.1.20348.4294 and ARM64 uses 5.1.26100.8655. +- Preserved gates: each Windows job passes 101 artifact files, 864 runnable cases, and 19 declared + skips before the live step, including launcher compilation/lifecycle, exact clean-build identity, + runtime smoke, and source/cache boundaries. X64 measures 42 files / 96,527,161 bytes; ARM64 + measures 42 / 85,213,511. Linux x64/ARM64 jobs `87818965677`/`87818965726`, Darwin x64/ARM64 jobs + `87818965712`/`87818965737`, and Linux oldest-userland supplements + `87819839680`/`87819839678` pass at the same exact head. +- Pinned trust: the standalone strict-trust launcher succeeds with sentinel, ended stdout, natural + process exit/channel close 0, and zero stderr in 400.4242 ms on x64 and 719.5839 ms on ARM64. + This independently preserves the earlier launcher/trust correction before the full-size test. +- Exact RED: the full-size test times out at its 1,200,000 ms ceiling after 1,200,018 ms on x64 and + 1,200,015 ms on ARM64. Neither emits serial/concurrent/collision/cancellation metrics. X64 + teardown finds fixture-owned `sshd-session.exe`, `cmd.exe`, `conhost.exe`, and `powershell.exe`; + ARM64 independently finds `cmd.exe`, `conhost.exe`, and `powershell.exe`. Both then correctly + report the still-locked `UsrClass.dat` instead of hiding fixture residue. +- Classification: command routing now reaches a complete PowerShell receiver, but both control and + file protocols still demand a final stdin EOF after already length-delimited bytes. The retained + command chains select that shared EOF-dependent completion boundary as the next isolated + hypothesis; they do not prove which transfer phase was active, so no full-size or teardown item is + credited. Replace only the final EOF check with an explicit fixed completion frame, retain exact + local byte-count and remote frame validation, and require fresh x64/ARM64 proof. + +### E-M6-WINDOWS-FRAMED-COMPLETION-LOCAL-001 — explicit completion framing is locally green + +- Date/owner/base/runner: 2026-07-17, Codex implementation owner; uncommitted correction atop exact + native RED head `ad3c3975c099f0e197661091b1cec9b0a0d6d553` on Apple arm64, macOS 26.2 build + 25C56, Node 26.5.0, and pnpm 10.24.0. The repository requests Node 24; native jobs remain + authoritative. +- Implementation boundary: Windows staging-control requests and file streams now end with the exact + eight-byte ASCII frame `ORCAEND1`; their PowerShell 5.1 receivers read and verify that frame after + their already bounded length fields and never wait for stdin EOF. The file destination counts + accepted payload bytes locally, aborts boundedly on short/long input, and writes the completion + frame only after the declared size is exact. The remote receiver still uses `CreateNew`, exact + payload length, fixed 64 KiB buffering, and failure cleanup. POSIX/SFTP, SSH authentication, + connection reuse, launcher/trust, product/default, Beta, fallback, tuple, publication, and signing + behavior are unchanged. +- Focused evidence: an initial focused run exposed two outdated test assumptions (closing without + the declared payload and asserting close synchronously); all product cases passed. After correcting + those framing oracles, `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts + --maxWorkers=1 src/main/ssh/ssh-relay-runtime-command-file-destination.test.ts + src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts + src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts + src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts + src/main/ssh/ssh-relay-runtime-windows-tree-transfer.test.ts + src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts` exits 0 with four + files passed, one live file skipped, 54 runnable cases passed, and three declared skips in 3.26 + seconds real; maximum RSS is 146,571,264 bytes and peak footprint is 97,474,416 bytes. +- Broad/release evidence: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts + --maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts` exits 0 with 53 files passed, six declared skipped, + 697 runnable cases passed, and ten declared skips in 23.32 seconds real; maximum RSS is + 301,826,048 bytes and peak footprint is 96,818,888 bytes. The exact release-contract command exits + 0 with 51 files, 285 runnable cases, and eight declared skips in 12.44 seconds real; maximum RSS is + 195,035,136 bytes and peak footprint is 97,294,144 bytes. +- Static/isolation evidence: `/usr/bin/time -l pnpm typecheck` exits 0 in 2.86 seconds real with + 1,242,497,024-byte maximum RSS and 97,031,904-byte peak footprint. `/usr/bin/time -l pnpm lint` + exits 0 in 13.52 seconds real with the existing 26 warnings, all 41 reliability gates, and the + 355-entry max-lines ratchet; maximum RSS is 2,110,308,352 bytes and peak footprint is 97,441,720 + bytes. Targeted `oxfmt --check`, `git diff --check`, exact no-`ReadByte()`/frame source audit, and + protected-resolver comparison pass. +- Residual gate: local Darwin declares the PowerShell/live cases skipped. Commit/push this exact + package and require fresh native x64/ARM64 receiver syntax, full-size serial/concurrent/collision/ + cancellation metrics, connection reuse, and exact fixture profile/directory teardown before + advancing. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -20404,10 +20477,10 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the explicit command-dialect correction recorded by -`E-M6-WINDOWS-COMMAND-DIALECT-CORRECTION-LOCAL-001`, then require exact-head Windows x64/ARM64 -full-size transfer, cancellation, concurrency, connection reuse, and exact profile/directory -teardown evidence. Do not check the broad Milestone 6 Windows system-SSH item or add an +Commit and push only the explicit Windows completion-framing correction recorded by +`E-M6-WINDOWS-FRAMED-COMPLETION-LOCAL-001`, then require exact-head Windows x64/ARM64 full-size +transfer, cancellation, concurrency, connection reuse, and exact profile/directory teardown +evidence. Do not check the broad Milestone 6 Windows system-SSH item or add an Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets diff --git a/src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts b/src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts index cb7b06d2f65..e5ecd0f97e6 100644 --- a/src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts +++ b/src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts @@ -38,6 +38,8 @@ function decodeHeader(frame: Buffer) { } } +const completionFrame = Buffer.from('ORCAEND1', 'ascii') + function decodePowerShellCommand(command: string): string { const encoded = command.match(/-EncodedCommand\s+([A-Za-z0-9+/=]+)$/u)?.[1] if (!encoded) { @@ -89,7 +91,8 @@ describe('SSH relay runtime Windows system-SSH file destination', () => { expect(script).toContain('[IO.FileMode]::CreateNew') expect(script).toContain('[IO.FileShare]::None') expect(script).toContain('New-Object byte[] 65536') - expect(script).toContain('$inputStream.ReadByte() -ne -1') + expect(script).toContain("-ne 'ORCAEND1'") + expect(script).not.toContain('$inputStream.ReadByte()') for (const forbidden of ['ReadToEnd', 'CopyTo(', 'FromBase64String', 'ConvertFrom-Json']) { expect(script).not.toContain(forbidden) } @@ -108,7 +111,10 @@ describe('SSH relay runtime Windows system-SSH file destination', () => { headerCallback?.() const destination = await opening + await destination.write(Buffer.alloc(65_537, 7)) const closing = destination.close() + await vi.waitFor(() => expect(channel.write).toHaveBeenCalledTimes(3)) + expect(channel.write.mock.calls[2]?.[0]).toEqual(completionFrame) resolve() await closing }) @@ -161,14 +167,33 @@ describe('SSH relay runtime Windows system-SSH file destination', () => { expect(channel.forceClose).not.toHaveBeenCalled() }) - it('propagates receiver failure only after EOF and remote settlement', async () => { + it('propagates receiver failure only after framed completion and remote settlement', async () => { const { channel, reject } = createChannel() const destination = await openDestination(channel, { expectedSize: 0 }) const closing = destination.close() - expect(channel.end).toHaveBeenCalledOnce() - reject(new Error('receiver rejected extra byte')) - await expect(closing).rejects.toThrow('receiver rejected extra byte') + await vi.waitFor(() => expect(channel.end).toHaveBeenCalledOnce()) + expect(channel.write.mock.calls[1]?.[0]).toEqual(completionFrame) + reject(new Error('receiver rejected completion frame')) + await expect(closing).rejects.toThrow('receiver rejected completion frame') + }) + + it.each([ + ['short', 2, Buffer.from([1])], + ['long', 1, Buffer.from([1, 2])] + ])('rejects a %s payload locally and aborts its channel', async (_kind, expectedSize, bytes) => { + const { channel, resolve } = createChannel() + channel.requestClose.mockImplementation(() => resolve()) + const destination = await openDestination(channel, { expectedSize }) + + if (bytes.length > expectedSize) { + await expect(destination.write(bytes)).rejects.toThrow(/payload size mismatch/i) + } else { + await destination.write(bytes) + await expect(destination.close()).rejects.toThrow(/payload size mismatch/i) + } + expect(channel.requestClose).toHaveBeenCalledOnce() + expect(channel.forceClose).not.toHaveBeenCalled() }) it.skipIf(process.platform !== 'win32')( @@ -211,8 +236,12 @@ describe('SSH relay runtime Windows system-SSH file destination', () => { signal: new AbortController().signal, openChannel: openPowerShellChannel }) - await destination.write(bytes) - await expect(destination.close()).rejects.toThrow(/PowerShell receiver failed/i) + if (bytes.length > expectedSize) { + await expect(destination.write(bytes)).rejects.toThrow(/payload size mismatch/i) + } else { + await destination.write(bytes) + await expect(destination.close()).rejects.toThrow(/payload size mismatch/i) + } await expect(readFile(remotePath)).rejects.toMatchObject({ code: 'ENOENT' }) } } finally { diff --git a/src/main/ssh/ssh-relay-runtime-windows-file-destination.ts b/src/main/ssh/ssh-relay-runtime-windows-file-destination.ts index 91397e742b6..b71017dacfe 100644 --- a/src/main/ssh/ssh-relay-runtime-windows-file-destination.ts +++ b/src/main/ssh/ssh-relay-runtime-windows-file-destination.ts @@ -14,12 +14,14 @@ export type OpenSshRelayRuntimeWindowsFileDestinationOptions = Readonly<{ }> const HEADER_MAGIC = Buffer.from('ORCARLY1', 'ascii') +const COMPLETION_MAGIC = Buffer.from('ORCAEND1', 'ascii') const FIXED_HEADER_BYTES = 20 const MAXIMUM_PATH_BYTES = 32 * 1024 const PAYLOAD_BUFFER_BYTES = 64 * 1024 export const SSH_RELAY_RUNTIME_WINDOWS_FILE_DESTINATION_LIMITS = Object.freeze({ maximumHeaderBytes: FIXED_HEADER_BYTES + MAXIMUM_PATH_BYTES, + completionBytes: COMPLETION_MAGIC.length, maximumPathBytes: MAXIMUM_PATH_BYTES, payloadBufferBytes: PAYLOAD_BUFFER_BYTES }) @@ -61,7 +63,9 @@ try { $outputStream.Write($buffer, 0, $read) $remaining -= $read } - if ($inputStream.ReadByte() -ne -1) { throw 'SSH relay runtime Windows receiver extra byte' } + [byte[]]$completion = New-Object byte[] ${COMPLETION_MAGIC.length} + Read-Exact $inputStream $completion ${COMPLETION_MAGIC.length} + if ([Text.Encoding]::ASCII.GetString($completion) -ne 'ORCAEND1') { throw 'bad completion' } $outputStream.Flush() $outputStream.Dispose() $outputStream = $null @@ -142,7 +146,6 @@ export async function openSshRelayRuntimeWindowsFileDestination( try { // Why: the receiver must authenticate framing before any source buffer can be borrowed. await destination.write(header) - return destination } catch (error) { try { await destination.abort(error) @@ -154,4 +157,51 @@ export async function openSshRelayRuntimeWindowsFileDestination( } throw error } + + let payloadBytes = 0 + const abortForSizeMismatch = async (): Promise => { + const error = new Error('SSH relay runtime Windows file destination payload size mismatch') + try { + await destination.abort(error) + } catch (cleanupError) { + throw new AggregateError( + [error, cleanupError], + 'SSH relay runtime Windows file size cleanup failed' + ) + } + throw error + } + return Object.freeze({ + write: async (chunk: Buffer): Promise => { + if (!Buffer.isBuffer(chunk) || chunk.length === 0) { + return destination.write(chunk) + } + if (payloadBytes + chunk.length > options.expectedSize) { + return abortForSizeMismatch() + } + await destination.write(chunk) + payloadBytes += chunk.length + }, + close: async (): Promise => { + if (payloadBytes !== options.expectedSize) { + return abortForSizeMismatch() + } + try { + // Why: Win32-OpenSSH may retain stdin EOF; an explicit frame ends verified bytes instead. + await destination.write(COMPLETION_MAGIC) + await destination.close() + } catch (error) { + try { + await destination.abort(error) + } catch (cleanupError) { + throw new AggregateError( + [error, cleanupError], + 'SSH relay runtime Windows file completion cleanup failed' + ) + } + throw error + } + }, + abort: (reason: unknown): Promise => destination.abort(reason) + }) } diff --git a/src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts b/src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts index 1338f22b712..522fd044b49 100644 --- a/src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts +++ b/src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts @@ -39,7 +39,8 @@ function decodeRequest(frame: Buffer) { operation: frame.readUInt8(8), reserved: frame.subarray(9, 12), remoteRoot: frame.subarray(rootStart, pathStart).toString('utf8'), - remotePath: frame.subarray(pathStart, pathStart + pathLength).toString('utf8') + remotePath: frame.subarray(pathStart, pathStart + pathLength).toString('utf8'), + completion: frame.subarray(pathStart + pathLength).toString('ascii') } } @@ -96,7 +97,8 @@ describe('SSH relay runtime Windows system-SSH staging control', () => { expect(script).toContain('New-Item -ItemType Directory -Path') expect(script).toContain('[IO.Path]::GetDirectoryName') expect(script).toContain('[IO.Directory]::Delete') - expect(script).toContain('$inputStream.ReadByte() -ne -1') + expect(script).toContain("-ne 'ORCAEND1'") + expect(script).not.toContain('$inputStream.ReadByte()') for (const forbidden of ['ReadToEnd', 'FromBase64String', 'ConvertFrom-Json']) { expect(script).not.toContain(forbidden) } @@ -111,7 +113,8 @@ describe('SSH relay runtime Windows system-SSH staging control', () => { operation: opcode, reserved: Buffer.alloc(3), remoteRoot: 'C:/Users/测试/.orca-remote/stage', - remotePath + remotePath, + completion: 'ORCAEND1' }) fixture.resolve() await expect(running).resolves.toBeUndefined() diff --git a/src/main/ssh/ssh-relay-runtime-windows-staging-control.ts b/src/main/ssh/ssh-relay-runtime-windows-staging-control.ts index 0de0db3c57c..5bd15059526 100644 --- a/src/main/ssh/ssh-relay-runtime-windows-staging-control.ts +++ b/src/main/ssh/ssh-relay-runtime-windows-staging-control.ts @@ -19,6 +19,7 @@ export type RunSshRelayRuntimeWindowsStagingControlOptions = Readonly<{ }> const REQUEST_MAGIC = Buffer.from('ORCACTL1', 'ascii') +const COMPLETION_MAGIC = Buffer.from('ORCAEND1', 'ascii') const FIXED_REQUEST_BYTES = 20 const MAXIMUM_PATH_BYTES = 32 * 1024 const COMMAND_TIMEOUT_MS = 30_000 @@ -32,7 +33,8 @@ const OPERATION_CODES: Record = export const SSH_RELAY_RUNTIME_WINDOWS_STAGING_CONTROL_LIMITS = Object.freeze({ commandTimeoutMs: COMMAND_TIMEOUT_MS, maximumPathBytes: MAXIMUM_PATH_BYTES, - maximumRequestBytes: FIXED_REQUEST_BYTES + MAXIMUM_PATH_BYTES * 2 + maximumRequestBytes: FIXED_REQUEST_BYTES + MAXIMUM_PATH_BYTES * 2 + COMPLETION_MAGIC.length, + completionBytes: COMPLETION_MAGIC.length }) const RECEIVER_SCRIPT = ` @@ -62,7 +64,9 @@ try { [byte[]]$pathBytes = New-Object byte[] ([int]$pathLength) Read-Exact $inputStream $rootBytes ([int]$rootLength) Read-Exact $inputStream $pathBytes ([int]$pathLength) - if ($inputStream.ReadByte() -ne -1) { throw 'extra byte' } + [byte[]]$completion = New-Object byte[] ${COMPLETION_MAGIC.length} + Read-Exact $inputStream $completion ${COMPLETION_MAGIC.length} + if ([Text.Encoding]::ASCII.GetString($completion) -ne 'ORCAEND1') { throw 'bad completion' } $utf8 = New-Object Text.UTF8Encoding($false, $true) $root = $utf8.GetString($rootBytes) $path = if ($pathLength -eq 0) { '' } else { $utf8.GetString($pathBytes) } @@ -159,13 +163,16 @@ function buildRequest( ): Buffer { const rootBytes = Buffer.from(remoteRoot, 'utf8') const pathBytes = Buffer.from(remotePath, 'utf8') - const request = Buffer.alloc(FIXED_REQUEST_BYTES + rootBytes.length + pathBytes.length) + const request = Buffer.alloc( + FIXED_REQUEST_BYTES + rootBytes.length + pathBytes.length + COMPLETION_MAGIC.length + ) REQUEST_MAGIC.copy(request, 0) request.writeUInt8(OPERATION_CODES[operation], 8) request.writeUInt32LE(rootBytes.length, 12) request.writeUInt32LE(pathBytes.length, 16) rootBytes.copy(request, FIXED_REQUEST_BYTES) pathBytes.copy(request, FIXED_REQUEST_BYTES + rootBytes.length) + COMPLETION_MAGIC.copy(request, FIXED_REQUEST_BYTES + rootBytes.length + pathBytes.length) return request } From ce4c7c481c790eacbf73162e5e03570fc2d3b490 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 17 Jul 2026 03:40:55 -0400 Subject: [PATCH 264/267] test(ssh): bound Windows transfer stall diagnostics Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 21 +-- ...github-release-implementation-checklist.md | 165 ++++++++++++------ ...ndows-system-ssh-openssh-full-size.test.ts | 110 +++++++++--- 3 files changed, 213 insertions(+), 83 deletions(-) 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 8a0f6a6e6cb..e6ce929194a 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 @@ -9,16 +9,17 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — explicit Windows -completion framing locally green, exact-head native rerun next, 2026-07-17, Codex implementation -owner.** Exact head `ad3c3975c099f0e197661091b1cec9b0a0d6d553`, run `29559577952`, and Windows -x64/ARM64 jobs `87818965702`/`87818965757` independently time out after 1,200,018/1,200,015 ms with -one fixture-user command chain retained. The isolated correction replaces final stdin EOF reads with -an exact `ORCAEND1` frame while preserving local size and remote frame validation. Local evidence is -green through 54 focused cases, 697 broad relay cases, 285 release contracts, typecheck, full -lint/reliability/max-lines, formatting, diff, and resolver isolation. Commit/push only this package -and require fresh x64/ARM64 full-size, cancellation, concurrency, reuse, and exact teardown proof. -No product/default caller, Beta, fallback, tuple, publication, or signing behavior is authorized. +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — Windows live-transfer +phase/stall diagnostic locally green, exact-head native qualification next, 2026-07-17, Codex +implementation owner.** Exact completion-framing head `19683ba24e8af031a6a02583b4f33e0746efb9ac`, +run `29561480245`, and Windows x64/ARM64 jobs `87824691503`/`87824691472` independently time out +after 1,200,020/1,200,022 ms with the same live fixture-user command chain, so final EOF is not the +retained boundary. The test-only diagnostic records privacy-safe phase/file-progress evidence and +aborts through the existing signal after 60 seconds without progress. Focused, broad, release, +typecheck, lint/reliability/max-lines, diff, and protected-resolver isolation gates pass. Commit and +push only this package; the next native RED must name and join the exact stalled operation before +behavior changes. No product/default caller, Beta, fallback, tuple, publication, or signing behavior +is authorized. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain 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 31b087f415f..609a1554bfd 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-17
    -Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — explicit Windows completion framing locally green, exact-head native qualification next, 2026-07-17, Codex implementation owner**. Exact correction head `ad3c3975c099f0e197661091b1cec9b0a0d6d553`, run `29559577952`, and Windows x64/ARM64 jobs `87818965702`/`87818965757` independently time out after 1,200,018/1,200,015 ms with a fixture-user command chain still waiting. `E-M6-WINDOWS-FRAMED-COMPLETION-LOCAL-001` replaces the receiver's EOF dependency with an exact `ORCAEND1` frame while retaining local byte-count and remote frame validation; focused, broad, release, typecheck, lint, formatting, diff, and resolver-isolation gates pass. Commit/push only this package and require fresh native full-size/cancellation/concurrency/reuse/teardown proof. Legacy remains the sole production path; real Apple/SignPath rehearsals remain deferred.
    -Session checkpoint: **WINDOWS FRAMED COMPLETION LOCAL GREEN / NATIVE RERUN NEXT — 2026-07-17, Codex implementation owner** — 54 focused cases with three declared skips, 697 broad relay cases with ten declared skips, 285 release contracts with eight declared skips, typecheck, lint/reliability/max-lines, formatting, diff, and protected-resolver isolation pass. Both PowerShell receivers are now length-delimited and completion-framed, with no stdin-EOF read; local short/long payload mismatches abort boundedly. Native proof remains required. No product caller, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — Windows live-transfer phase/stall diagnostic locally green, exact-head native qualification next, 2026-07-17, Codex implementation owner**. Exact completion-framing head `19683ba24e8af031a6a02583b4f33e0746efb9ac`, run `29561480245`, and Windows x64/ARM64 jobs `87824691503`/`87824691472` independently time out after 1,200,020/1,200,022 ms with the same fixture-user command chain, falsifying final EOF as the retained boundary. `E-M6-WINDOWS-PHASE-STALL-DIAGNOSTIC-LOCAL-001` adds only privacy-safe phase/progress evidence and a 60-second inactivity abort through the existing transfer signal. Commit and push only this diagnostic package so the next native RED identifies and joins the exact operation. Legacy remains the sole production path; real Apple/SignPath rehearsals remain deferred.
    +Session checkpoint: **PHASE/STALL DIAGNOSTIC LOCAL GREEN / NATIVE RUN NEXT — 2026-07-17, Codex implementation owner** — focused, broad relay, release-contract, typecheck, targeted/full lint, reliability/max-lines, diff, and protected-resolver isolation gates pass. No transfer protocol, authentication, connection reuse, product caller, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
    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) @@ -20083,20 +20083,20 @@ lint` passes in 18.59 seconds real with the 26 existing warnings, all 41 reliabi No packaged asset, Electron/startup caller, target setting, fallback, Beta, tuple, publication, default, signing, or SignPath path is connected. - Syntax/focused evidence: `node --check - config/scripts/build-windows-ssh-no-input-launcher.mjs` and `node --check - config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs` pass. `/usr/bin/time -l pnpm - exec vitest run --config config/vitest.config.ts --maxWorkers=1 - src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts - src/main/ssh/system-ssh-command-launcher.test.ts - src/main/ssh/ssh-system-no-input-windows-openssh.test.ts - src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts - config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs - config/scripts/ssh-relay-runtime-workflow.test.mjs` passes six files with one native file skipped, +config/scripts/build-windows-ssh-no-input-launcher.mjs` and `node --check +config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs` pass. `/usr/bin/time -l pnpm +exec vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/system-ssh-command-launcher.test.ts +src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes six files with one native file skipped, 124 runnable cases, and ten declared native/live skips in 4.35 seconds real; maximum RSS is 150,519,808 bytes and peak footprint is 97,687,504 bytes. - Release/static evidence: `/usr/bin/time -l pnpm exec vitest run --config - config/vitest.config.ts --maxWorkers=1 --reporter=dot - config/scripts/ssh-relay-runtime-*.test.mjs` passes 51 files and 285 runnable cases with eight +config/vitest.config.ts --maxWorkers=1 --reporter=dot +config/scripts/ssh-relay-runtime-*.test.mjs` passes 51 files and 285 runnable cases with eight declared Windows skips in 17.95 seconds real; maximum RSS is 195,674,112 bytes and peak footprint is 97,228,728 bytes. `/usr/bin/time -l pnpm typecheck` passes in 2.77 seconds real with 1,267,253,248-byte maximum RSS and 97,277,808-byte peak footprint. `/usr/bin/time -l pnpm lint` @@ -20107,8 +20107,8 @@ lint` passes in 18.59 seconds real with the 26 existing warnings, all 41 reliabi `system-ssh-command-launcher.test.ts`, after which lint passes without a disable or ratchet change. - Formatting/isolation: `pnpm exec oxfmt --write` passes for all modified TypeScript and MJS files; `git diff --check` and exact `git diff --exit-code HEAD -- - src/main/ssh/ssh-remote-node-resolution.ts - src/main/ssh/ssh-remote-node-resolution.test.ts` pass. The protected Milestone 0 resolver files +src/main/ssh/ssh-remote-node-resolution.ts +src/main/ssh/ssh-remote-node-resolution.test.ts` pass. The protected Milestone 0 resolver files remain byte-for-byte unchanged. - Residual gate: local mocks do not prove the adapter settles native Win32-OpenSSH or that the owned Windows fixture profile is released. Commit/push this isolated package, then require exact-head @@ -20171,20 +20171,20 @@ lint` passes in 18.59 seconds real with the 26 existing warnings, all 41 reliabi signing remain unchanged. Unit contracts prove explicit Windows injection reports `windows-no-input-launcher`, while POSIX ignores the supplied Windows path and reports `direct`. - Syntax/focused evidence: both launcher scripts pass `node --check`. `/usr/bin/time -l pnpm exec - vitest run --config config/vitest.config.ts --maxWorkers=1 - src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts - src/main/ssh/system-ssh-command-launcher.test.ts - src/main/ssh/ssh-system-no-input-windows-openssh.test.ts - src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts - config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs - config/scripts/ssh-relay-runtime-workflow.test.mjs` passes six files with one skipped, 124 +vitest run --config config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/system-ssh-command-launcher.test.ts +src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs +config/scripts/ssh-relay-runtime-workflow.test.mjs` passes six files with one skipped, 124 runnable cases, and ten declared native/live skips in 18.18 seconds real; maximum RSS is 139,968,512 bytes and peak footprint is 97,785,640 bytes. - Release/static evidence: the exact unquoted release-contract command passes 51 files and 285 runnable cases with eight declared Windows skips in 383.15 seconds real; maximum RSS is 189,906,944 bytes and peak footprint is 97,523,424 bytes. The machine was under sustained load, so this timing is retained as observed evidence rather than a performance baseline. `/usr/bin/time - -l pnpm typecheck` passes in 110.83 seconds real with 1,795,964,928-byte maximum RSS and +-l pnpm typecheck` passes in 110.83 seconds real with 1,795,964,928-byte maximum RSS and 98,195,288-byte peak footprint. `/usr/bin/time -l pnpm lint` passes in 199.22 seconds real with the same 26 existing warnings, all 41 reliability gates, and the 355-entry max-lines ratchet; maximum RSS is 2,102,296,576 bytes and peak footprint is 97,589,080 bytes. @@ -20249,16 +20249,16 @@ lint` passes in 18.59 seconds real with the 26 existing warnings, all 41 reliabi preventing the following full-size test from running. - Focused evidence: after an initial run exposed and corrected three assertion/source-oracle mismatches, `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts - --maxWorkers=1 src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts - src/main/ssh/system-ssh-command-launcher.test.ts - src/main/ssh/ssh-system-no-input-windows-openssh.test.ts - src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts - config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs - config/scripts/ssh-relay-runtime-workflow.test.mjs` exits 0 with six files passed, one native file +--maxWorkers=1 src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts +src/main/ssh/system-ssh-command-launcher.test.ts +src/main/ssh/ssh-system-no-input-windows-openssh.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs +config/scripts/ssh-relay-runtime-workflow.test.mjs` exits 0 with six files passed, one native file skipped, 126 runnable cases passed, and ten declared native/live skips in 22.65 seconds real; maximum RSS is 150,585,344 bytes and peak footprint is 97,769,280 bytes. - Release/static evidence: `/usr/bin/time -l pnpm exec vitest run --config - config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs` exits 0 with 51 +config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs` exits 0 with 51 files, 285 runnable cases passed, and eight declared skips in 58.62 seconds real; maximum RSS is 195,690,496 bytes and peak footprint is 97,539,832 bytes. `/usr/bin/time -l pnpm typecheck` exits 0 in 11.38 seconds real with 1,774,403,584-byte maximum RSS and 98,293,520-byte peak footprint. @@ -20295,7 +20295,7 @@ lint` passes in 18.59 seconds real with the 26 existing warnings, all 41 reliabi command, proving the constructor-injected launcher plus strict trust no longer blocks the probe. - Exact transfer RED: x64 fails after 1.520 seconds and ARM64 after 1.667 seconds with `SSH relay runtime system SSH file command failed (exit 1): 'exec' is not recognized as an - internal or external command, operable program or batch file.` The Windows staging layer already +internal or external command, operable program or batch file.` The Windows staging layer already produced a complete encoded PowerShell command, but the generic system-SSH file channel called `connection.exec(command)` with its default POSIX wrapper. No staging root, payload byte, serial/concurrent/collision/cancellation measurement, or connection-reuse metric runs. @@ -20320,17 +20320,17 @@ lint` passes in 18.59 seconds real with the 26 existing warnings, all 41 reliabi connection reuse, channel adaptation, byte streaming, cancellation ownership, diagnostics, launcher selection, strict trust, and every product/default caller remain unchanged. - Focused evidence: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts - --maxWorkers=1 src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts - src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts - src/main/ssh/ssh-relay-runtime-windows-tree-transfer.test.ts - src/main/ssh/ssh-relay-runtime-posix-system-ssh-openssh-full-size.test.ts - src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts - config/scripts/ssh-relay-runtime-workflow.test.mjs` exits 0 with four files passed, two live files +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts +src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts +src/main/ssh/ssh-relay-runtime-windows-tree-transfer.test.ts +src/main/ssh/ssh-relay-runtime-posix-system-ssh-openssh-full-size.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` exits 0 with four files passed, two live files skipped, 40 runnable cases passed, and two declared skips in 4.35 seconds real; maximum RSS is 173,981,696 bytes and peak footprint is 97,801,928 bytes. The routing contract independently asserts the exact POSIX one-argument call and PowerShell `{ wrapCommand: false }` call. - Release/static evidence: `/usr/bin/time -l pnpm exec vitest run --config - config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs` exits 0 with 51 +config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs` exits 0 with 51 files, 285 runnable cases passed, and eight declared skips in 20.51 seconds real; maximum RSS is 195,592,192 bytes and peak footprint is 97,146,712 bytes. `/usr/bin/time -l pnpm typecheck` exits 0 in 7.16 seconds real with 1,229,029,376-byte maximum RSS and 97,998,656-byte peak footprint. @@ -20394,16 +20394,16 @@ lint` passes in 18.59 seconds real with the 26 existing warnings, all 41 reliabi - Focused evidence: an initial focused run exposed two outdated test assumptions (closing without the declared payload and asserting close synchronously); all product cases passed. After correcting those framing oracles, `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts - --maxWorkers=1 src/main/ssh/ssh-relay-runtime-command-file-destination.test.ts - src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts - src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts - src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts - src/main/ssh/ssh-relay-runtime-windows-tree-transfer.test.ts - src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts` exits 0 with four +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-command-file-destination.test.ts +src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts +src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts +src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts +src/main/ssh/ssh-relay-runtime-windows-tree-transfer.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts` exits 0 with four files passed, one live file skipped, 54 runnable cases passed, and three declared skips in 3.26 seconds real; maximum RSS is 146,571,264 bytes and peak footprint is 97,474,416 bytes. - Broad/release evidence: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts - --maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts` exits 0 with 53 files passed, six declared skipped, +--maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts` exits 0 with 53 files passed, six declared skipped, 697 runnable cases passed, and ten declared skips in 23.32 seconds real; maximum RSS is 301,826,048 bytes and peak footprint is 96,818,888 bytes. The exact release-contract command exits 0 with 51 files, 285 runnable cases, and eight declared skips in 12.44 seconds real; maximum RSS is @@ -20419,6 +20419,69 @@ lint` passes in 18.59 seconds real with the 26 existing warnings, all 41 reliabi cancellation metrics, connection reuse, and exact fixture profile/directory teardown before advancing. +### E-M6-WINDOWS-FRAMED-COMPLETION-CI-RED-001 — explicit framing does not resolve the native stall + +- Date/owner/source/run: 2026-07-17, Codex implementation owner; exact head + `19683ba24e8af031a6a02583b4f33e0746efb9ac`, artifact run + [29561480245](https://github.com/stablyai/orca/actions/runs/29561480245), Windows x64 job + [87824691503](https://github.com/stablyai/orca/actions/runs/29561480245/job/87824691503) on + `windows-2022`, and native ARM64 job + [87824691472](https://github.com/stablyai/orca/actions/runs/29561480245/job/87824691472) on + `windows-11-arm`. Both use Node 24.18.0 and official OpenSSH 10.0p2; x64 uses PowerShell + 5.1.20348.4294 and ARM64 uses 5.1.26100.8655. +- Preserved gates: each Windows job passes 101 artifact-contract files, 866 runnable cases, and 19 + declared skips before live transfer. The isolated pinned-trust control proves sentinel, ended + stdout, natural process exit/channel close 0, and zero stderr in 739.3636 ms on x64 and 704.6447 + ms on ARM64. +- Exact RED: the full-size test still reaches its 1,200,000 ms outer ceiling after 1,200,020 ms on + x64 and 1,200,022 ms on ARM64 without emitting a serial transfer metric. X64 teardown finds + fixture-owned `sshd-session.exe`, `cmd.exe`, `conhost.exe`, and `powershell.exe`; ARM64 finds + `cmd.exe`, `conhost.exe`, and `powershell.exe`. Both then correctly fail on the locked + `UsrClass.dat` profile instead of hiding residue. +- Classification: identical failure after the exact `ORCAEND1` correction falsifies final stdin EOF + as the retained boundary. The run still does not identify whether the stall is connection, tree + preparation, channel start, payload progress, validation, or cleanup, so no second speculative + behavior change is authorized. Add privacy-safe phase/progress evidence and abort/join after a + shorter inactivity ceiling, then require both architectures to select the same exact boundary. + +### E-M6-WINDOWS-PHASE-STALL-DIAGNOSTIC-LOCAL-001 — bounded native diagnostic is locally green + +- Date/owner/base/runner: 2026-07-17, Codex implementation owner; uncommitted diagnostic atop exact + native RED head `19683ba24e8af031a6a02583b4f33e0746efb9ac` on Apple arm64, macOS 26.2 build + 25C56, Node 26.5.0, and pnpm 10.24.0. The repository requests Node 24; native jobs remain + authoritative. +- Implementation boundary: only the opt-in native full-size test changes. It records connection, + serial/concurrent transfer, validation, collision, cancellation, connection-reuse cleanup, + disconnect, and fixture cleanup phases. Progress contains only declared aggregate file/byte counts + and active-file concurrency; it never logs target paths, commands, usernames, keys, or transferred + contents. Serial and concurrent transfers reset a 60-second inactivity watchdog on progress and + abort through the existing bounded transfer signal; `finally` always disposes the timer. Transfer + framing, byte protocol, SSH authentication, connection reuse, product/default, Beta, fallback, + tuple, publication, and signing behavior remain unchanged. +- Focused evidence: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts +src/main/ssh/ssh-relay-runtime-posix-tree-transfer.test.ts +src/main/ssh/ssh-relay-runtime-windows-tree-transfer.test.ts +src/main/ssh/ssh-relay-runtime-posix-system-ssh-openssh-full-size.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +config/scripts/ssh-relay-runtime-workflow.test.mjs` exits 0 with four files passed, two live files + skipped, 40 runnable cases passed, and two declared skips in 14.41 seconds real; maximum RSS is + 154,386,432 bytes and peak footprint is 97,834,672 bytes. +- Broad/release evidence: `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts +--maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts` exits 0 with 53 files passed, six declared skipped, + 697 runnable cases passed, and ten declared skips in 26.78 seconds real; maximum RSS is 302,252,032 + bytes and peak footprint is 98,342,624 bytes. The exact release-contract command exits 0 with 51 + files, 285 runnable cases passed, and eight declared skips in 12.88 seconds real; maximum RSS is + 195,756,032 bytes and peak footprint is 97,146,664 bytes. +- Static/isolation evidence: `/usr/bin/time -l pnpm typecheck` exits 0 in 7.08 seconds real with + 1,210,384,384-byte maximum RSS and 97,900,424-byte peak footprint. Targeted `oxlint` exits 0. + `/usr/bin/time -l pnpm lint` exits 0 in 14.36 seconds real with the existing 26 warnings, all 41 + reliability gates, and the 355-entry max-lines ratchet; maximum RSS is 1,964,048,384 bytes and peak + footprint is 97,179,408 bytes. `git diff --check` and the exact protected-resolver comparison pass. +- Residual gate: local Darwin skips both live files and cannot execute the watchdog. Commit/push only + this diagnostic and require exact-head x64/ARM64 phase/progress, inactivity settlement, connection + reuse, and exact fixture profile/directory teardown evidence before changing transfer behavior. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -20477,12 +20540,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the explicit Windows completion-framing correction recorded by -`E-M6-WINDOWS-FRAMED-COMPLETION-LOCAL-001`, then require exact-head Windows x64/ARM64 full-size -transfer, cancellation, concurrency, connection reuse, and exact profile/directory teardown -evidence. Do not check the broad Milestone 6 Windows system-SSH item or add an -Electron/startup/product importer, per-target mode wiring, fallback, tuple -enablement, release publication, or default behavior. +Commit and push only the 60-second phase/stall diagnostic recorded by +`E-M6-WINDOWS-PHASE-STALL-DIAGNOSTIC-LOCAL-001`, then require exact-head Windows x64/ARM64 phase, +progress, inactivity settlement, connection reuse, and exact profile/directory teardown evidence. +Do not check the broad Milestone 6 Windows system-SSH item or add an Electron/startup/product +importer, per-target mode wiring, fallback, tuple enablement, release publication, or default +behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. diff --git a/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts b/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts index ab815835c97..d121c5346b7 100644 --- a/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +++ b/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts @@ -60,6 +60,7 @@ const powerShellVersion = process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_POW const runtimeRoot = process.env.ORCA_SSH_RELAY_FULL_SIZE_RUNTIME_ROOT const identityPath = process.env.ORCA_SSH_RELAY_FULL_SIZE_IDENTITY const port = Number.parseInt(process.env.ORCA_SSH_RELAY_LIVE_WINDOWS_SYSTEM_SSH_PORT ?? '', 10) +const LIVE_STALL_TIMEOUT_MS = 60_000 const hasLiveInput = Boolean( host && user && @@ -203,23 +204,64 @@ function stagePath(name: string, identity: MeasurementIdentity): string { return join(remoteRoot as string, `${name}-${identity.contentId.slice('sha256:'.length, 23)}`) } -function createProgressRecorder(): { +function logLivePhase(phase: string, details: Record = {}): void { + console.log(`ssh_relay_live_windows_system_ssh_phase=${JSON.stringify({ phase, ...details })}`) +} + +function createProgressRecorder( + phase: string, + onActivity: () => void +): { record: (progress: SshRelayRuntimeSourceStreamProgress) => void snapshot: () => { bytes: number; updates: number; peakActiveFiles: number } } { let bytes = 0 let updates = 0 let peakActiveFiles = 0 + let lastLoggedAt = 0 + let lastLoggedFiles = -1 return { record: (progress) => { + onActivity() bytes = progress.bytesTransferred updates += 1 peakActiveFiles = Math.max(peakActiveFiles, progress.activeFiles) + const now = performance.now() + if (progress.filesCompleted !== lastLoggedFiles || now - lastLoggedAt >= 5_000) { + lastLoggedAt = now + lastLoggedFiles = progress.filesCompleted + logLivePhase(`${phase}-progress`, { + filesCompleted: progress.filesCompleted, + totalFiles: progress.totalFiles, + bytesTransferred: progress.bytesTransferred, + totalBytes: progress.totalBytes, + activeFiles: progress.activeFiles + }) + } }, snapshot: () => ({ bytes, updates, peakActiveFiles }) } } +function createLiveStallWatchdog(phase: string): { + signal: AbortSignal + activity: () => void + dispose: () => void +} { + const controller = new AbortController() + let timeout: ReturnType + const activity = (): void => { + clearTimeout(timeout) + timeout = setTimeout(() => { + const error = new Error(`Live Windows system-SSH ${phase} stalled without progress`) + logLivePhase(`${phase}-stalled`, { inactivityMs: LIVE_STALL_TIMEOUT_MS }) + controller.abort(error) + }, LIVE_STALL_TIMEOUT_MS) + } + activity() + return { signal: controller.signal, activity, dispose: () => clearTimeout(timeout) } +} + async function transfer( connection: SshConnection, tree: SshRelayRuntimeScannedSourceTree, @@ -238,6 +280,27 @@ async function transfer( }) } +async function measureWatchedTransfer( + connection: SshConnection, + tree: SshRelayRuntimeScannedSourceTree, + stage: string, + phase: string, + maximumConcurrency?: number +) { + const watchdog = createLiveStallWatchdog(phase) + const progress = createProgressRecorder(phase, watchdog.activity) + logLivePhase(`${phase}-start`) + try { + const measurement = await measure(() => + transfer(connection, tree, stage, watchdog.signal, progress.record, maximumConcurrency) + ) + logLivePhase(`${phase}-transfer-complete`, { elapsedMs: measurement.elapsedMs }) + return { measurement, progress } + } finally { + watchdog.dispose() + } +} + describe.skipIf(!hasLiveInput)( 'SSH relay full-size Windows system-SSH transfer through native OpenSSH', () => { @@ -282,44 +345,40 @@ describe.skipIf(!hasLiveInput)( await Promise.all(stages.map((stage) => rm(stage, { recursive: true, force: true }))) try { + logLivePhase('connect-start') await connection.connect() expect(connection.usesSystemSshTransport()).toBe(true) + logLivePhase('connect-complete') - const serialProgress = createProgressRecorder() - const serial = await measure(() => - transfer( - connection, - tree, - serialStage, - new AbortController().signal, - serialProgress.record - ) + const { measurement: serial, progress: serialProgress } = await measureWatchedTransfer( + connection, + tree, + serialStage, + 'serial' ) + logLivePhase('serial-validation-start') await validateTransferredTree(tree, serialStage) + logLivePhase('serial-validation-complete') expect(serialProgress.snapshot().peakActiveFiles).toBe(1) - const concurrentProgress = createProgressRecorder() - const concurrent = await measure(() => - transfer( - connection, - tree, - concurrentStage, - new AbortController().signal, - concurrentProgress.record, - 4 - ) - ) + const { measurement: concurrent, progress: concurrentProgress } = + await measureWatchedTransfer(connection, tree, concurrentStage, 'concurrent', 4) + logLivePhase('concurrent-validation-start') await validateTransferredTree(tree, concurrentStage) + logLivePhase('concurrent-validation-complete') expect(concurrentProgress.snapshot().peakActiveFiles).toBeGreaterThan(1) expect(concurrentProgress.snapshot().peakActiveFiles).toBeLessThanOrEqual(4) + logLivePhase('collision-start') await expect( transfer(connection, tree, concurrentStage, new AbortController().signal, () => {}) ).rejects.toBeTruthy() await validateTransferredTree(tree, concurrentStage) + logLivePhase('collision-complete') const controller = new AbortController() - const cancelledProgress = createProgressRecorder() + const cancelledProgress = createProgressRecorder('cancellation', () => {}) let abortRequestedAt = 0 + logLivePhase('cancellation-start') const cancelled = await measure(async () => { const outcome = transfer( connection, @@ -343,7 +402,9 @@ describe.skipIf(!hasLiveInput)( await new Promise((resolve) => setTimeout(resolve, 500)) await expect(lstat(cancelledStage)).rejects.toMatchObject({ code: 'ENOENT' }) expect(cancelledProgress.snapshot().updates).toBe(updatesAfterSettlement) + logLivePhase('cancellation-complete', { settlementMs: cancelled.result }) + logLivePhase('reuse-cleanup-start') await runSshRelayRuntimeWindowsStagingControl({ operation: 'remove-root', remoteRoot: cancelledStage, @@ -352,6 +413,7 @@ describe.skipIf(!hasLiveInput)( openSshRelayRuntimeSystemSshFileChannel(connection, command, signal, 'powershell') }) expect(connection.getState().status).toBe('connected') + logLivePhase('reuse-cleanup-complete') const metrics = { tupleId: identity.tupleId, @@ -387,8 +449,12 @@ describe.skipIf(!hasLiveInput)( ) expect(cancelled.result).toBeLessThan(10_000) } finally { + logLivePhase('disconnect-start') await connection.disconnect() + logLivePhase('disconnect-complete') + logLivePhase('final-cleanup-start') await Promise.all(stages.map((stage) => rm(stage, { recursive: true, force: true }))) + logLivePhase('final-cleanup-complete') } } ) From c93111a14bdb5390f99aa44a9004449625cbdcb5 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 17 Jul 2026 04:01:00 -0400 Subject: [PATCH 265/267] fix(ssh): bound Windows runtime pipe writes Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 21 +++-- ...github-release-implementation-checklist.md | 89 +++++++++++++++++-- ...6-07-14-ssh-relay-github-release-plan.html | 11 ++- ...y-runtime-windows-file-destination.test.ts | 43 ++++++++- ...-relay-runtime-windows-file-destination.ts | 10 ++- 5 files changed, 148 insertions(+), 26 deletions(-) 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 e6ce929194a..ecd15f44519 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 @@ -9,17 +9,16 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — Windows live-transfer -phase/stall diagnostic locally green, exact-head native qualification next, 2026-07-17, Codex -implementation owner.** Exact completion-framing head `19683ba24e8af031a6a02583b4f33e0746efb9ac`, -run `29561480245`, and Windows x64/ARM64 jobs `87824691503`/`87824691472` independently time out -after 1,200,020/1,200,022 ms with the same live fixture-user command chain, so final EOF is not the -retained boundary. The test-only diagnostic records privacy-safe phase/file-progress evidence and -aborts through the existing signal after 60 seconds without progress. Focused, broad, release, -typecheck, lint/reliability/max-lines, diff, and protected-resolver isolation gates pass. Commit and -push only this package; the next native RED must name and join the exact stalled operation before -behavior changes. No product/default caller, Beta, fallback, tuple, publication, or signing behavior -is authorized. +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — Windows borrowed-chunk +pipe-write correction locally green, exact-head native qualification next, 2026-07-17, Codex +implementation owner.** Exact diagnostic head +`ce4c7c481c790eacbf73162e5e03570fc2d3b490`, run `29563868935`, and Windows x64/ARM64 jobs +`87832001707`/`87832001736` independently complete the 18-byte first file and stall before accepting +any bytes of `THIRD_PARTY_LICENSES.txt`. The isolated correction uses sequential zero-copy 16 KiB +Windows pipe views while retaining the borrowed source buffer until all subwrites settle. Focused, +broad, release, typecheck, lint/reliability/max-lines, formatting, diff, and resolver isolation are +green. Commit/push only this package for fresh native proof. No product/default caller, Beta, +fallback, tuple, publication, or signing behavior is authorized. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain 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 609a1554bfd..6fc0009ee8a 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-17
    -Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — Windows live-transfer phase/stall diagnostic locally green, exact-head native qualification next, 2026-07-17, Codex implementation owner**. Exact completion-framing head `19683ba24e8af031a6a02583b4f33e0746efb9ac`, run `29561480245`, and Windows x64/ARM64 jobs `87824691503`/`87824691472` independently time out after 1,200,020/1,200,022 ms with the same fixture-user command chain, falsifying final EOF as the retained boundary. `E-M6-WINDOWS-PHASE-STALL-DIAGNOSTIC-LOCAL-001` adds only privacy-safe phase/progress evidence and a 60-second inactivity abort through the existing transfer signal. Commit and push only this diagnostic package so the next native RED identifies and joins the exact operation. Legacy remains the sole production path; real Apple/SignPath rehearsals remain deferred.
    -Session checkpoint: **PHASE/STALL DIAGNOSTIC LOCAL GREEN / NATIVE RUN NEXT — 2026-07-17, Codex implementation owner** — focused, broad relay, release-contract, typecheck, targeted/full lint, reliability/max-lines, diff, and protected-resolver isolation gates pass. No transfer protocol, authentication, connection reuse, product caller, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — Windows borrowed-chunk pipe-write correction locally green, exact-head native qualification next, 2026-07-17, Codex implementation owner**. Exact diagnostic head `ce4c7c481c790eacbf73162e5e03570fc2d3b490`, run `29563868935`, and Windows x64/ARM64 jobs `87832001707`/`87832001736` independently prove the first borrowed 64 KiB payload write stalls. `E-M6-WINDOWS-PIPE-SUBWRITE-LOCAL-001` subdivides only that Windows destination write into sequential zero-copy 16 KiB views while retaining the source buffer until every subwrite settles. Commit/push only this correction and require fresh native proof. Legacy remains the sole production path; real Apple/SignPath rehearsals remain deferred.
    +Session checkpoint: **WINDOWS 16 KIB PIPE SUBWRITE LOCAL GREEN / NATIVE RUN NEXT — 2026-07-17, Codex implementation owner** — 55 focused cases with three declared skips, 698 broad relay cases with ten declared skips, 285 release contracts with eight declared skips, typecheck, targeted/full lint, reliability/max-lines, formatting, diff, and resolver isolation pass. The remote 64 KiB .NET buffer, source 64 KiB worker buffer, every other transfer family, and all product/default behavior remain unchanged.
    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) @@ -20482,6 +20482,79 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` exits 0 with four files pass this diagnostic and require exact-head x64/ARM64 phase/progress, inactivity settlement, connection reuse, and exact fixture profile/directory teardown evidence before changing transfer behavior. +### E-M6-WINDOWS-PHASE-STALL-DIAGNOSTIC-CI-RED-001 — both architectures stall on the first 64 KiB payload write + +- Date/owner/source/run: 2026-07-17, Codex implementation owner; exact diagnostic head + `ce4c7c481c790eacbf73162e5e03570fc2d3b490`, artifact run + [29563868935](https://github.com/stablyai/orca/actions/runs/29563868935), Windows x64 job + [87832001707](https://github.com/stablyai/orca/actions/runs/29563868935/job/87832001707), and + native ARM64 job + [87832001736](https://github.com/stablyai/orca/actions/runs/29563868935/job/87832001736). Both jobs + pass 101 artifact-contract files, 866 runnable cases, and 19 declared skips before live transfer; + all Linux, Darwin, and Linux oldest-userland jobs pass at the same head. +- Pinned trust and phase evidence: x64 pinned trust passes in 948.0027 ms and ARM64 in 715.4503 ms. + Connection completes in 1,166/1,046 ms. Both serial transfers then report exactly two progress + states: 18 bytes with one active file, followed by one completed file and zero active files. No + further byte is accepted before each 60,000 ms inactivity abort. The x64 test finishes in 72,398 + ms and ARM64 in 76,250 ms instead of reaching the 1,200,000 ms outer ceiling. +- Exact retained boundary: the first 18-byte `.version` file succeeds. Both local cleanup attempts + then fail with `EBUSY` on the next lexicographic file, `THIRD_PARTY_LICENSES.txt`, while the fixture + owns `sshd-session.exe`, `cmd.exe`, `conhost.exe`, and `powershell.exe`. Both profile teardowns + correctly fail on locked `UsrClass.dat`. This independently selects the first borrowed 64 KiB + payload write after successful header/open as the shared stall; it is not connection, staging-root, + first-file, final EOF, validation, concurrency, collision, or cancellation behavior. +- Plan correction: retain the source stream's one reusable 64 KiB buffer and its borrowed-buffer + lifetime, but permit the Windows destination to subdivide that borrowed view into smaller awaited + pipe writes. It must not resolve the source write until every subwrite settles, copy or retain the + source buffer, change the receiver's fixed 64 KiB .NET buffer, or affect POSIX/SFTP. The HTML plan, + detailed checklist, and short checklist record this evidence-driven clarification before code + changes. +- Residual gate: implement and locally prove only that boundary, then require fresh exact-head x64/ + ARM64 full-size serial/concurrent/collision/cancellation metrics, bounded memory, connection reuse, + and exact fixture profile/directory teardown. No product/default/Beta/fallback/tuple/publication or + signing behavior is authorized. + +### E-M6-WINDOWS-PIPE-SUBWRITE-LOCAL-001 — borrowed 64 KiB chunks use awaited 16 KiB Windows pipe views + +- Date/owner/base/runner: 2026-07-17, Codex implementation owner; uncommitted correction atop exact + diagnostic head `ce4c7c481c790eacbf73162e5e03570fc2d3b490` on Apple arm64, macOS 26.2 build + 25C56, Node 26.5.0, and pnpm 10.24.0. The repository requests Node 24; native jobs remain + authoritative. +- Implementation boundary: the Windows file destination alone subdivides each borrowed source view + into sequential subviews of at most 16,384 bytes and awaits each existing channel callback before + issuing the next. It does not copy the chunk, increment accepted payload size, or resolve the + source write until all subwrites settle. Header/completion framing, exact declared size, remote + fixed 64 KiB .NET buffer, source fixed 64 KiB worker buffer, cancellation owner, POSIX, SFTP, SSH + authentication/reuse, product/default, Beta, fallback, tuple, publication, and signing behavior + remain unchanged. The HTML and both Markdown checklists record the evidence-driven clarification. +- Purpose/focused evidence: a new retained-callback oracle proves 16,384/16,384/3-byte sequential + subviews share the original payload `ArrayBuffer`, no later subwrite starts early, and the source + write resolves only after all three callbacks. `/usr/bin/time -l pnpm exec vitest run --config +config/vitest.config.ts --maxWorkers=1 +src/main/ssh/ssh-relay-runtime-command-file-destination.test.ts +src/main/ssh/ssh-relay-runtime-system-ssh-file-channel.test.ts +src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts +src/main/ssh/ssh-relay-runtime-windows-staging-control.test.ts +src/main/ssh/ssh-relay-runtime-windows-tree-transfer.test.ts +src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts` exits 0 with four files + passed, one live file skipped, 55 runnable cases passed, and three declared skips in 4.56 seconds + real; maximum RSS is 153,255,936 bytes and peak footprint is 97,113,968 bytes. +- Broad/release evidence: the exact broad relay command exits 0 with 53 files passed, six declared + skipped, 698 runnable cases passed, and ten declared skips in 25.87 seconds real; maximum RSS is + 304,594,944 bytes and peak footprint is 98,162,448 bytes. The exact release-contract command exits + 0 with 51 files, 285 runnable cases passed, and eight declared skips in 16.42 seconds real; maximum + RSS is 195,739,648 bytes and peak footprint is 98,834,264 bytes. +- Static evidence: the first typecheck reports only an unused test mock parameter and no production + error. After naming that deliberate unused parameter, `/usr/bin/time -l pnpm typecheck` exits 0 in + 2.78 seconds real with 1,239,203,840-byte maximum RSS and 98,178,880-byte peak footprint. Targeted + `oxlint` exits 0. `/usr/bin/time -l pnpm lint` exits 0 in 15.80 seconds real with the existing 26 + warnings, all 41 reliability gates, and the 355-entry max-lines ratchet; maximum RSS is + 2,080,964,608 bytes and peak footprint is 97,032,048 bytes. +- Residual gate: local Darwin skips native PowerShell/OpenSSH. Commit/push only this package and + require exact-head x64/ARM64 serial/concurrent/collision/cancellation timing and memory, complete + byte/tree validation, connection reuse, and exact fixture profile/directory teardown before + advancing. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -20540,12 +20613,12 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the 60-second phase/stall diagnostic recorded by -`E-M6-WINDOWS-PHASE-STALL-DIAGNOSTIC-LOCAL-001`, then require exact-head Windows x64/ARM64 phase, -progress, inactivity settlement, connection reuse, and exact profile/directory teardown evidence. -Do not check the broad Milestone 6 Windows system-SSH item or add an Electron/startup/product -importer, per-target mode wiring, fallback, tuple enablement, release publication, or default -behavior. +Commit and push only the Windows pipe-subwrite correction recorded by +`E-M6-WINDOWS-PIPE-SUBWRITE-LOCAL-001`, then require exact-head Windows x64/ARM64 full-size +serial/concurrent/collision/cancellation metrics, bounded memory, connection reuse, and exact +profile/directory teardown evidence. Do not check the broad Milestone 6 Windows system-SSH item or +add an Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release +publication, or default behavior. Keep Node upstream `.tar.xz` inputs, Windows ZIP, `ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding, Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication, and every tuple separately gated. diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index 40481f3de6f..e4cb3357ea4 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1002,10 +1002,13 @@

    Deploy and validate the runtime

    streaming is required and uses the SSH channel as a byte-preserving stdin stream into exclusive per-file staging with only the declared shell/file primitives; Windows uses a bounded binary PowerShell/.NET path and must not materialize the - runtime as one JSON/base64 buffer; SFTP uses bounded concurrency. After locally - verifying signed metadata and archive/tree hashes, transfer through authenticated - SSH, restore every declared executable bit, then execute the transferred bundled - Node to hash the entire staging tree. Verify the full tree, run bundled + runtime as one JSON/base64 buffer. The client may subdivide each borrowed 64 KiB + source chunk into smaller awaited Windows pipe writes, but must not release or + reuse that source buffer until every subwrite settles; the receiver retains its + fixed 64 KiB .NET buffer. SFTP uses bounded concurrency. After locally verifying + signed metadata and archive/tree hashes, transfer through authenticated SSH, + restore every declared executable bit, then execute the transferred bundled Node + to hash the entire staging tree. Verify the full tree, run bundled Node/native-module and minimal PTY/watcher probes, write a structured .install-complete containing schema, mode, and digest, atomically publish the immutable install, and only then launch. Preserve current locks, diff --git a/src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts b/src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts index e5ecd0f97e6..49d4030c1f1 100644 --- a/src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts +++ b/src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts @@ -113,8 +113,49 @@ describe('SSH relay runtime Windows system-SSH file destination', () => { const destination = await opening await destination.write(Buffer.alloc(65_537, 7)) const closing = destination.close() + await vi.waitFor(() => expect(channel.write).toHaveBeenCalledTimes(7)) + expect(channel.write.mock.calls[6]?.[0]).toEqual(completionFrame) + resolve() + await closing + }) + + it('subdivides a borrowed source chunk into sequential zero-copy pipe writes', async () => { + const { channel, resolve } = createChannel() + const callbacks: Callback[] = [] + channel.write.mockImplementation((_chunk, callback) => { + if (channel.write.mock.calls.length === 1) { + callback() + return + } + callbacks.push(callback) + }) + const maximum = SSH_RELAY_RUNTIME_WINDOWS_FILE_DESTINATION_LIMITS.maximumPipeWriteBytes + const payload = Buffer.alloc(maximum * 2 + 3, 9) + const destination = await openDestination(channel, { expectedSize: payload.length }) + let settled = false + const writing = destination.write(payload).then(() => { + settled = true + }) + + await vi.waitFor(() => expect(channel.write).toHaveBeenCalledTimes(2)) + expect(channel.write.mock.calls[1]?.[0]).toHaveLength(maximum) + expect(channel.write.mock.calls[1]?.[0].buffer).toBe(payload.buffer) + callbacks.shift()?.() await vi.waitFor(() => expect(channel.write).toHaveBeenCalledTimes(3)) - expect(channel.write.mock.calls[2]?.[0]).toEqual(completionFrame) + expect(settled).toBe(false) + expect(channel.write.mock.calls[2]?.[0]).toHaveLength(maximum) + expect(channel.write.mock.calls[2]?.[0].buffer).toBe(payload.buffer) + callbacks.shift()?.() + await vi.waitFor(() => expect(channel.write).toHaveBeenCalledTimes(4)) + expect(settled).toBe(false) + expect(channel.write.mock.calls[3]?.[0]).toHaveLength(3) + expect(channel.write.mock.calls[3]?.[0].buffer).toBe(payload.buffer) + callbacks.shift()?.() + await writing + expect(settled).toBe(true) + const closing = destination.close() + await vi.waitFor(() => expect(channel.write).toHaveBeenCalledTimes(5)) + callbacks.shift()?.() resolve() await closing }) diff --git a/src/main/ssh/ssh-relay-runtime-windows-file-destination.ts b/src/main/ssh/ssh-relay-runtime-windows-file-destination.ts index b71017dacfe..286193430e0 100644 --- a/src/main/ssh/ssh-relay-runtime-windows-file-destination.ts +++ b/src/main/ssh/ssh-relay-runtime-windows-file-destination.ts @@ -18,12 +18,14 @@ const COMPLETION_MAGIC = Buffer.from('ORCAEND1', 'ascii') const FIXED_HEADER_BYTES = 20 const MAXIMUM_PATH_BYTES = 32 * 1024 const PAYLOAD_BUFFER_BYTES = 64 * 1024 +const MAXIMUM_PIPE_WRITE_BYTES = 16 * 1024 export const SSH_RELAY_RUNTIME_WINDOWS_FILE_DESTINATION_LIMITS = Object.freeze({ maximumHeaderBytes: FIXED_HEADER_BYTES + MAXIMUM_PATH_BYTES, completionBytes: COMPLETION_MAGIC.length, maximumPathBytes: MAXIMUM_PATH_BYTES, - payloadBufferBytes: PAYLOAD_BUFFER_BYTES + payloadBufferBytes: PAYLOAD_BUFFER_BYTES, + maximumPipeWriteBytes: MAXIMUM_PIPE_WRITE_BYTES }) const RECEIVER_SCRIPT = ` @@ -179,7 +181,11 @@ export async function openSshRelayRuntimeWindowsFileDestination( if (payloadBytes + chunk.length > options.expectedSize) { return abortForSizeMismatch() } - await destination.write(chunk) + // Why: native Win32-OpenSSH stalls on one borrowed 64 KiB pipe write; awaited subviews keep + // the source buffer borrowed until every byte has crossed the same retention boundary. + for (let offset = 0; offset < chunk.length; offset += MAXIMUM_PIPE_WRITE_BYTES) { + await destination.write(chunk.subarray(offset, offset + MAXIMUM_PIPE_WRITE_BYTES)) + } payloadBytes += chunk.length }, close: async (): Promise => { From d7fcdb25972390bee5b97d495bf605845bcee364 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 17 Jul 2026 04:20:41 -0400 Subject: [PATCH 266/267] fix(ssh): match Windows runtime pipe buffers Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 20 ++++----- ...github-release-implementation-checklist.md | 43 +++++++++++++++++-- ...6-07-14-ssh-relay-github-release-plan.html | 14 +++--- ...y-runtime-windows-file-destination.test.ts | 9 ++-- ...-relay-runtime-windows-file-destination.ts | 8 ++-- 5 files changed, 67 insertions(+), 27 deletions(-) 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 ecd15f44519..284af9b6c37 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 @@ -9,16 +9,16 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — Windows borrowed-chunk -pipe-write correction locally green, exact-head native qualification next, 2026-07-17, Codex -implementation owner.** Exact diagnostic head -`ce4c7c481c790eacbf73162e5e03570fc2d3b490`, run `29563868935`, and Windows x64/ARM64 jobs -`87832001707`/`87832001736` independently complete the 18-byte first file and stall before accepting -any bytes of `THIRD_PARTY_LICENSES.txt`. The isolated correction uses sequential zero-copy 16 KiB -Windows pipe views while retaining the borrowed source buffer until all subwrites settle. Focused, -broad, release, typecheck, lint/reliability/max-lines, formatting, diff, and resolver isolation are -green. Commit/push only this package for fresh native proof. No product/default caller, Beta, -fallback, tuple, publication, or signing behavior is authorized. +Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — matched 4 KiB Windows +pipe correction locally green, native qualification next, 2026-07-17, Codex implementation +owner.** Exact head +`c93111a14bdb5390f99aa44a9004449625cbdcb5`, run `29564980476`, and x64/ARM64 jobs +`87835460661`/`87835460642` falsify client-only 16 KiB subdivision: x64 stalls after the 18-byte +first file, while ARM64 completes the 180,827-byte license and stalls on `node.exe`. Client pipe +writes and remote .NET reads now match at 4 KiB while preserving the source 64 KiB buffer; focused, +broad, release, typecheck, lint/reliability/max-lines, formatting, diff, and resolver isolation pass. +Commit/push only this package. No product/default caller, Beta, fallback, tuple, publication, or +signing behavior is authorized. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain 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 6fc0009ee8a..08c3096d140 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-17
    -Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — Windows borrowed-chunk pipe-write correction locally green, exact-head native qualification next, 2026-07-17, Codex implementation owner**. Exact diagnostic head `ce4c7c481c790eacbf73162e5e03570fc2d3b490`, run `29563868935`, and Windows x64/ARM64 jobs `87832001707`/`87832001736` independently prove the first borrowed 64 KiB payload write stalls. `E-M6-WINDOWS-PIPE-SUBWRITE-LOCAL-001` subdivides only that Windows destination write into sequential zero-copy 16 KiB views while retaining the source buffer until every subwrite settles. Commit/push only this correction and require fresh native proof. Legacy remains the sole production path; real Apple/SignPath rehearsals remain deferred.
    -Session checkpoint: **WINDOWS 16 KIB PIPE SUBWRITE LOCAL GREEN / NATIVE RUN NEXT — 2026-07-17, Codex implementation owner** — 55 focused cases with three declared skips, 698 broad relay cases with ten declared skips, 285 release contracts with eight declared skips, typecheck, targeted/full lint, reliability/max-lines, formatting, diff, and resolver isolation pass. The remote 64 KiB .NET buffer, source 64 KiB worker buffer, every other transfer family, and all product/default behavior remain unchanged.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — matched 4 KiB Windows pipe correction locally green, native qualification next, 2026-07-17, Codex implementation owner**. Exact head `c93111a14bdb5390f99aa44a9004449625cbdcb5`, run `29564980476`, and Windows x64/ARM64 jobs `87835460661`/`87835460642` falsify client-only subdivision. `E-M6-WINDOWS-MATCHED-PIPE-LOCAL-001` matches client writes and remote .NET reads at 4 KiB while preserving the source 64 KiB buffer. Commit/push only this correction and require fresh native proof. Legacy remains the sole production path; real Apple/SignPath rehearsals remain deferred.
    +Session checkpoint: **MATCHED 4 KIB PIPE LOCAL GREEN / NATIVE RUN NEXT — 2026-07-17, Codex implementation owner** — focused, broad relay, release-contract, typecheck, full lint/reliability/max-lines, formatting, diff, and resolver isolation pass. No product caller, default/fallback, Beta, tuple publication, or signing behavior changed.
    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) @@ -20555,6 +20555,41 @@ src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts` exi byte/tree validation, connection reuse, and exact fixture profile/directory teardown before advancing. +### E-M6-WINDOWS-PIPE-SUBWRITE-CI-RED-001 — client-only subdivision is insufficient + +- Date/source/run: 2026-07-17; exact head `c93111a14bdb5390f99aa44a9004449625cbdcb5`, run + [29564980476](https://github.com/stablyai/orca/actions/runs/29564980476), x64 job + [87835460661](https://github.com/stablyai/orca/actions/runs/29564980476/job/87835460661), and + ARM64 job [87835460642](https://github.com/stablyai/orca/actions/runs/29564980476/job/87835460642). +- Exact RED: x64 completes only the 18-byte `.version` file before the 60-second watchdog and retains + `THIRD_PARTY_LICENSES.txt`. ARM64 completes `.version` plus the 180,827-byte license file, then + stalls and retains `bin/node.exe` at 180,845 aggregate bytes. Tests finish in 72,580/77,910 ms; + both profile teardowns correctly expose retained fixture processes and locked `UsrClass.dat`. +- Classification/correction: architecture-variable progress falsifies client-only 16 KiB subdivision + and file-specific failure. Match Windows pipe writes and PowerShell/.NET reads at a conservative + 4,096-byte quantum, keep the source worker's 65,536-byte reusable buffer and zero-copy borrowed + views, and require fresh two-architecture full-size metrics and clean teardown. The HTML plan and + both Markdown checklists are updated before code. + +### E-M6-WINDOWS-MATCHED-PIPE-LOCAL-001 — matched 4 KiB Windows pipe quantum is locally green + +- Date/base/runner: 2026-07-17; uncommitted correction atop exact RED head + `c93111a14bdb5390f99aa44a9004449625cbdcb5` on Apple arm64, macOS 26.2, Node 26.5.0, pnpm + 10.24.0. Native Node 24 jobs remain authoritative. +- Boundary: client zero-copy subwrites and PowerShell/.NET reads are both capped at 4,096 bytes; the + source worker still owns one 65,536-byte buffer and releases it only after every subwrite callback. + Framing, size/tree verification, cancellation, POSIX/SFTP, SSH behavior, and product/default paths + are unchanged. +- Evidence: focused Windows tests pass 55 runnable cases with three skips in 3.31 seconds real + (141,262,848-byte max RSS). Broad relay passes 698 cases with ten skips in 34.40 seconds real + (306,413,568-byte max RSS). Release contracts pass 285 cases with eight skips in 28.62 seconds; + typecheck passes in 4.30 seconds; full lint passes with the existing 26 warnings, 41 reliability + gates, and 355-entry max-lines ratchet in 26.32 seconds. Formatting, diff, and resolver isolation + pass. The initial focused run failed only because its expected write count still encoded 16 KiB; + the limit-derived oracle is green. +- Residual: local Darwin skips native OpenSSH/PowerShell. Fresh x64/ARM64 full-size metrics and exact + teardown remain mandatory. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -20613,8 +20648,8 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the Windows pipe-subwrite correction recorded by -`E-M6-WINDOWS-PIPE-SUBWRITE-LOCAL-001`, then require exact-head Windows x64/ARM64 full-size +Commit and push only the matched 4 KiB Windows pipe correction recorded by +`E-M6-WINDOWS-MATCHED-PIPE-LOCAL-001`, then require exact-head Windows x64/ARM64 full-size serial/concurrent/collision/cancellation metrics, bounded memory, connection reuse, and exact profile/directory teardown evidence. Do not check the broad Milestone 6 Windows system-SSH item or add an Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html index e4cb3357ea4..0cbcebacfd1 100644 --- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html +++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html @@ -1004,12 +1004,14 @@

    Deploy and validate the runtime

    Windows uses a bounded binary PowerShell/.NET path and must not materialize the runtime as one JSON/base64 buffer. The client may subdivide each borrowed 64 KiB source chunk into smaller awaited Windows pipe writes, but must not release or - reuse that source buffer until every subwrite settles; the receiver retains its - fixed 64 KiB .NET buffer. SFTP uses bounded concurrency. After locally verifying - signed metadata and archive/tree hashes, transfer through authenticated SSH, - restore every declared executable bit, then execute the transferred bundled Node - to hash the entire staging tree. Verify the full tree, run bundled - Node/native-module and minimal PTY/watcher probes, write a structured + reuse that source buffer until every subwrite settles. Windows pipe writes and + .NET reads use the same conservative 4 KiB quantum to avoid platform pipe + backpressure cycles; the source worker retains its fixed 64 KiB buffer. SFTP uses + bounded concurrency. After locally verifying signed metadata and archive/tree + hashes, transfer through authenticated SSH, restore every declared executable bit, + then execute the transferred bundled Node to hash the entire staging tree. Verify + the full tree, run bundled Node/native-module and minimal PTY/watcher probes, + write a structured .install-complete containing schema, mode, and digest, atomically publish the immutable install, and only then launch. Preserve current locks, reconnect, and garbage collection. diff --git a/src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts b/src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts index 49d4030c1f1..42806251ef2 100644 --- a/src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts +++ b/src/main/ssh/ssh-relay-runtime-windows-file-destination.test.ts @@ -90,7 +90,7 @@ describe('SSH relay runtime Windows system-SSH file destination', () => { expect(script).toContain('[Console]::OpenStandardInput()') expect(script).toContain('[IO.FileMode]::CreateNew') expect(script).toContain('[IO.FileShare]::None') - expect(script).toContain('New-Object byte[] 65536') + expect(script).toContain('New-Object byte[] 4096') expect(script).toContain("-ne 'ORCAEND1'") expect(script).not.toContain('$inputStream.ReadByte()') for (const forbidden of ['ReadToEnd', 'CopyTo(', 'FromBase64String', 'ConvertFrom-Json']) { @@ -113,8 +113,11 @@ describe('SSH relay runtime Windows system-SSH file destination', () => { const destination = await opening await destination.write(Buffer.alloc(65_537, 7)) const closing = destination.close() - await vi.waitFor(() => expect(channel.write).toHaveBeenCalledTimes(7)) - expect(channel.write.mock.calls[6]?.[0]).toEqual(completionFrame) + const expectedWrites = + Math.ceil(65_537 / SSH_RELAY_RUNTIME_WINDOWS_FILE_DESTINATION_LIMITS.maximumPipeWriteBytes) + + 2 + await vi.waitFor(() => expect(channel.write).toHaveBeenCalledTimes(expectedWrites)) + expect(channel.write.mock.calls.at(-1)?.[0]).toEqual(completionFrame) resolve() await closing }) diff --git a/src/main/ssh/ssh-relay-runtime-windows-file-destination.ts b/src/main/ssh/ssh-relay-runtime-windows-file-destination.ts index 286193430e0..6dd0f656acf 100644 --- a/src/main/ssh/ssh-relay-runtime-windows-file-destination.ts +++ b/src/main/ssh/ssh-relay-runtime-windows-file-destination.ts @@ -17,8 +17,8 @@ const HEADER_MAGIC = Buffer.from('ORCARLY1', 'ascii') const COMPLETION_MAGIC = Buffer.from('ORCAEND1', 'ascii') const FIXED_HEADER_BYTES = 20 const MAXIMUM_PATH_BYTES = 32 * 1024 -const PAYLOAD_BUFFER_BYTES = 64 * 1024 -const MAXIMUM_PIPE_WRITE_BYTES = 16 * 1024 +const PAYLOAD_BUFFER_BYTES = 4 * 1024 +const MAXIMUM_PIPE_WRITE_BYTES = PAYLOAD_BUFFER_BYTES export const SSH_RELAY_RUNTIME_WINDOWS_FILE_DESTINATION_LIMITS = Object.freeze({ maximumHeaderBytes: FIXED_HEADER_BYTES + MAXIMUM_PATH_BYTES, @@ -181,8 +181,8 @@ export async function openSshRelayRuntimeWindowsFileDestination( if (payloadBytes + chunk.length > options.expectedSize) { return abortForSizeMismatch() } - // Why: native Win32-OpenSSH stalls on one borrowed 64 KiB pipe write; awaited subviews keep - // the source buffer borrowed until every byte has crossed the same retention boundary. + // Why: matched 4 KiB reads/writes avoid Win32 pipe backpressure cycles while zero-copy + // subviews keep the source's 64 KiB buffer borrowed until every byte settles. for (let offset = 0; offset < chunk.length; offset += MAXIMUM_PIPE_WRITE_BYTES) { await destination.write(chunk.subarray(offset, offset + MAXIMUM_PIPE_WRITE_BYTES)) } From c85a900a1df17c09a79ae73e2a1f96a8a4dd6247 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 17 Jul 2026 04:38:15 -0400 Subject: [PATCH 267/267] test(ssh): classify Windows Node transfer stall Co-authored-by: Orca --- ...elease-implementation-checklist-summary.md | 18 ++++----- ...github-release-implementation-checklist.md | 39 +++++++++++++++++-- ...ndows-system-ssh-openssh-full-size.test.ts | 39 ++++++++++++++++++- 3 files changed, 81 insertions(+), 15 deletions(-) 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 284af9b6c37..24ff0085c36 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 @@ -9,16 +9,14 @@ keeps commands, hashes, runner identities, timings, and failure details. A checked box means the work has evidence in the detailed ledger. Design approval alone does not complete a box. -Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — matched 4 KiB Windows -pipe correction locally green, native qualification next, 2026-07-17, Codex implementation -owner.** Exact head -`c93111a14bdb5390f99aa44a9004449625cbdcb5`, run `29564980476`, and x64/ARM64 jobs -`87835460661`/`87835460642` falsify client-only 16 KiB subdivision: x64 stalls after the 18-byte -first file, while ARM64 completes the 180,827-byte license and stalls on `node.exe`. Client pipe -writes and remote .NET reads now match at 4 KiB while preserving the source 64 KiB buffer; focused, -broad, release, typecheck, lint/reliability/max-lines, formatting, diff, and resolver isolation pass. -Commit/push only this package. No product/default caller, Beta, fallback, tuple, publication, or -signing behavior is authorized. +Active checkpoint: **Milestone 6 / Work Package 5 — executable-name versus large-file diagnostic +locally green, native qualification next, 2026-07-17, Codex implementation owner.** Exact matched-buffer head +`d7fcdb25972390bee5b97d495bf605845bcee364`, run `29566111327`, and x64/ARM64 jobs +`87838990171`/`87838990156` both transfer `.version` and the full license, then stall at +180,843/180,845 bytes on `bin/node.exe`. Transfer the exact authenticated Node bytes once to a +neutral `.bin` path; focused, broad, typecheck, lint/reliability/max-lines, formatting, diff, and +resolver isolation pass. Do not change staging semantics before both architectures classify the cause. +No product/default caller, Beta, fallback, tuple, publication, or signing behavior is authorized. `E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain 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 08c3096d140..872d4f4a4b9 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-17
    -Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — matched 4 KiB Windows pipe correction locally green, native qualification next, 2026-07-17, Codex implementation owner**. Exact head `c93111a14bdb5390f99aa44a9004449625cbdcb5`, run `29564980476`, and Windows x64/ARM64 jobs `87835460661`/`87835460642` falsify client-only subdivision. `E-M6-WINDOWS-MATCHED-PIPE-LOCAL-001` matches client writes and remote .NET reads at 4 KiB while preserving the source 64 KiB buffer. Commit/push only this correction and require fresh native proof. Legacy remains the sole production path; real Apple/SignPath rehearsals remain deferred.
    -Session checkpoint: **MATCHED 4 KIB PIPE LOCAL GREEN / NATIVE RUN NEXT — 2026-07-17, Codex implementation owner** — focused, broad relay, release-contract, typecheck, full lint/reliability/max-lines, formatting, diff, and resolver isolation pass. No product caller, default/fallback, Beta, tuple publication, or signing behavior changed.
    +Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **In progress — Windows executable-name versus large-file diagnostic locally green, native qualification next, 2026-07-17, Codex implementation owner**. Exact matched-buffer head `d7fcdb25972390bee5b97d495bf605845bcee364`, run `29566111327`, and x64/ARM64 jobs `87838990171`/`87838990156` both select `bin/node.exe`. `E-M6-WINDOWS-NEUTRAL-NODE-DIAGNOSTIC-LOCAL-001` adds an exact authenticated Node transfer to a neutral `.bin` path before the unchanged full-tree test. Commit/push only this diagnostic. Legacy remains the sole production path; real Apple/SignPath rehearsals remain deferred.
    +Session checkpoint: **NEUTRAL NODE NAME DIAGNOSTIC LOCAL GREEN / NATIVE RUN NEXT — 2026-07-17, Codex implementation owner** — focused, broad relay, typecheck, full lint/reliability/max-lines, formatting, diff, and resolver isolation pass. No production or default behavior changed.
    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) @@ -20590,6 +20590,36 @@ src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts` exi - Residual: local Darwin skips native OpenSSH/PowerShell. Fresh x64/ARM64 full-size metrics and exact teardown remain mandatory. +### E-M6-WINDOWS-MATCHED-PIPE-CI-RED-001 — both architectures select the node.exe boundary + +- Date/source/run: 2026-07-17; exact head `d7fcdb25972390bee5b97d495bf605845bcee364`, run + [29566111327](https://github.com/stablyai/orca/actions/runs/29566111327), x64 job + [87838990171](https://github.com/stablyai/orca/actions/runs/29566111327/job/87838990171), ARM64 job + [87838990156](https://github.com/stablyai/orca/actions/runs/29566111327/job/87838990156). +- Result: both transfer `.version` and the full 180,825/180,827-byte license file, reaching + 180,843/180,845 aggregate bytes, then make no progress on `bin/node.exe` before the 60-second + watchdog. Both retain only the active node file and fixture process/profile chain; bounded failure + remains fail-closed. Pinned trust passes in 550.8297/868.8028 ms. +- Next gate: transfer the exact scanned/authenticated Node file to a neutral `.bin` name using the + same connection, framing, 4 KiB quantum, source snapshots, size, and hash. If it passes on both + architectures, executable-name/native endpoint protection is selected; if it fails, large-file + streaming remains selected. Do not alter staging/publish semantics before this evidence. + +### E-M6-WINDOWS-NEUTRAL-NODE-DIAGNOSTIC-LOCAL-001 — exact Node bytes get a neutral-name probe + +- Date/base: 2026-07-17; uncommitted test-only diagnostic atop exact head + `d7fcdb25972390bee5b97d495bf605845bcee364`. +- Boundary: derive a one-file scanned tree from the already hashed `bin/node.exe` entry, preserve its + local path/state/size/hash, transfer it to `bin/orca-node-payload.bin`, validate the exact remote + tree and bytes, remove that owned root, then run the unchanged full-tree test. Phase logs contain + only aggregate counts/timing. No production module or behavior changes. +- Evidence: focused Windows tests pass 25 cases with two skips in 3.91 seconds real; broad relay + passes 698 cases with ten skips in 27.04 seconds; typecheck passes in 3.57 seconds; full lint passes + with 26 existing warnings, 41 reliability gates, and the 355-entry max-lines ratchet in 17.53 + seconds. Formatting, diff, and protected-resolver isolation pass. +- Residual: local Darwin skips the native probe. Both native architectures must agree before staging + semantics change. + ## Accepted Gaps No product gap is accepted merely because it appears in this list. Each entry requires explicit @@ -20648,8 +20678,9 @@ The project is not complete until every applicable item below is checked with ev ## Next Required Action -Commit and push only the matched 4 KiB Windows pipe correction recorded by -`E-M6-WINDOWS-MATCHED-PIPE-LOCAL-001`, then require exact-head Windows x64/ARM64 full-size +Commit and push only the neutral-name Node transfer diagnostic recorded by +`E-M6-WINDOWS-NEUTRAL-NODE-DIAGNOSTIC-LOCAL-001`, then require exact-head Windows x64/ARM64 evidence before +changing staging semantics. Ultimately require full-size serial/concurrent/collision/cancellation metrics, bounded memory, connection reuse, and exact profile/directory teardown evidence. Do not check the broad Milestone 6 Windows system-SSH item or add an Electron/startup/product importer, per-target mode wiring, fallback, tuple enablement, release diff --git a/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts b/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts index d121c5346b7..031228940c8 100644 --- a/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts +++ b/src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts @@ -204,6 +204,23 @@ function stagePath(name: string, identity: MeasurementIdentity): string { return join(remoteRoot as string, `${name}-${identity.contentId.slice('sha256:'.length, 23)}`) } +function neutralNodeTree(tree: SshRelayRuntimeScannedSourceTree): SshRelayRuntimeScannedSourceTree { + const node = tree.files.find((file) => file.path === 'bin/node.exe') + const bin = tree.directories.find((directory) => directory.path === 'bin') + if (!node || !bin) { + throw new Error('Live Windows system-SSH runtime Node entry is missing') + } + // Why: identical authenticated bytes under a non-executable name distinguish streaming from + // native endpoint-protection behavior before staging semantics are redesigned. + return Object.freeze({ + ...tree, + directories: Object.freeze([bin]), + files: Object.freeze([Object.freeze({ ...node, path: 'bin/orca-node-payload.bin' })]), + fileCount: 1, + expandedBytes: node.size + }) +} + function logLivePhase(phase: string, details: Record = {}): void { console.log(`ssh_relay_live_windows_system_ssh_phase=${JSON.stringify({ phase, ...details })}`) } @@ -339,9 +356,10 @@ describe.skipIf(!hasLiveInput)( } ) const serialStage = stagePath('system-serial', identity) + const neutralNodeStage = stagePath('system-neutral-node', identity) const concurrentStage = stagePath('system-concurrent', identity) const cancelledStage = stagePath('system-cancelled', identity) - const stages = [serialStage, concurrentStage, cancelledStage] + const stages = [neutralNodeStage, serialStage, concurrentStage, cancelledStage] await Promise.all(stages.map((stage) => rm(stage, { recursive: true, force: true }))) try { @@ -350,6 +368,25 @@ describe.skipIf(!hasLiveInput)( expect(connection.usesSystemSshTransport()).toBe(true) logLivePhase('connect-complete') + const neutralTree = neutralNodeTree(tree) + const { measurement: neutralNode } = await measureWatchedTransfer( + connection, + neutralTree, + neutralNodeStage, + 'neutral-node' + ) + logLivePhase('neutral-node-validation-start') + await validateTransferredTree(neutralTree, neutralNodeStage) + logLivePhase('neutral-node-validation-complete', { elapsedMs: neutralNode.elapsedMs }) + await runSshRelayRuntimeWindowsStagingControl({ + operation: 'remove-root', + remoteRoot: neutralNodeStage, + signal: new AbortController().signal, + openChannel: (command, signal) => + openSshRelayRuntimeSystemSshFileChannel(connection, command, signal, 'powershell') + }) + logLivePhase('neutral-node-cleanup-complete') + const { measurement: serial, progress: serialProgress } = await measureWatchedTransfer( connection, tree,