mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 16:02:55 +00:00
build(ssh): verify runtime release bytes
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -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 `
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
+12
-8
@@ -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.
|
||||
|
||||
+160
-13
@@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps
|
||||
|
||||
Date created: 2026-07-14<br>
|
||||
Last updated: 2026-07-15<br>
|
||||
Current phase: Milestone 4 / Work Package 3 credential-free release/signing DAG failure contracts — **In progress — 2026-07-15, Codex implementation owner**. Work Package 2's target-native Windows official-Node and preserved-upstream source-signature gate is closed under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001. Exact-floor gaps and real native signing/trust remain open external gates, so the active package is limited to disconnected fail-closed release-DAG contracts and tests; it may not publish, connect desktop consumers, or enable a tuple. Production/default behavior is unchanged, no bundled-runtime path is enabled, and no artifact is published.<br>
|
||||
Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — 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.<br>
|
||||
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.<br>
|
||||
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.<br>
|
||||
Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)<br>
|
||||
Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693)
|
||||
|
||||
@@ -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 <job-id> --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 <eight touched implementation/workflow/checklist files>
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user