Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions config/scripts/build-orcad.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,25 @@
*/
import { fork, spawnSync } from 'node:child_process'
import { build } from 'esbuild'
import { chmodSync, copyFileSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs'
import { createHash } from 'node:crypto'
import {
chmodSync,
copyFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync
} from 'node:fs'
import { arch, platform, tmpdir } from 'node:os'
import { join } from 'node:path'
import process from 'node:process'
import {
ORCAD_VERSION,
ORCAD_VERSION_FILENAME,
orcadArtifactFilenames
} from '../../src/shared/orcad-artifacts.ts'

const ROOT = join(import.meta.dirname, '..', '..')
const OUT_DIR = join(ROOT, 'out', 'orcad')
Expand Down Expand Up @@ -226,9 +241,26 @@ if (graphErrors.length > 0) {
}
}

// Why a content hash and not ORCAD_VERSION alone: the remote install directory is keyed on
// this string, so two different builds carrying one version would share a directory — and an
// already-`.install-complete` dir is never re-uploaded. The deploy would silently run stale
// bytes while reporting the new version.
if (process.exitCode !== 1) {
const hash = createHash('sha256')
for (const filename of orcadArtifactFilenames()) {
const artifactPath = join(OUT_DIR, filename)
if (!existsSync(artifactPath)) {
throw new Error(
`orcad declares ${filename} in ORCAD_ARTIFACTS but never emitted it. Add the build ` +
'step, or drop it from src/shared/orcad-artifacts.ts.'
)
}
hash.update(readFileSync(artifactPath))
}
const fullVersion = `${ORCAD_VERSION}+${hash.digest('hex').slice(0, 12)}`
writeFileSync(join(OUT_DIR, ORCAD_VERSION_FILENAME), fullVersion)
console.log(
`[build-orcad] ok — ${(output.bytes / 1024 / 1024).toFixed(2)} MB, ${Object.keys(output.inputs).length} modules, zero electron and node:sqlite imports.`
`[build-orcad] ok — ${fullVersion}, ${(output.bytes / 1024 / 1024).toFixed(2)} MB, ${Object.keys(output.inputs).length} modules, zero electron and node:sqlite imports.`
)
}

Expand Down
164 changes: 164 additions & 0 deletions src/main/ssh/orcad-activation-gate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { describe, expect, it } from 'vitest'

import { evaluateOrcadActivation } from './orcad-activation-gate'
import type { ServeReadiness } from '../server/serve-readiness'
import type { OrcadHealth, TerminalDaemonHealth } from '../orcad/orcad-health'

const EXPECTED = { buildHash: 'abc123def4567890', fullVersion: '0.2.0+bb01' }

function daemon(overrides: Partial<TerminalDaemonHealth> = {}): TerminalDaemonHealth {
return {
state: 'live',
ownsFreshSessions: true,
pid: 4242,
buildVersion: '0.2.0+bb01',
entryPath: '/home/u/.orca-remote/orcad-0.2.0+bb01/daemon-entry.js',
protocolVersion: 3,
selfTest: { ok: true, coverage: 'pty-spawn', verdict: 'healthy', durationMs: 12 },
...overrides
}
}

function health(overrides: Partial<OrcadHealth> = {}): OrcadHealth {
return {
buildHash: EXPECTED.buildHash,
buildVersion: EXPECTED.fullVersion,
nodeVersion: '20.11.0',
nodeAbi: '115',
platform: 'linux',
arch: 'x64',
pid: 4200,
terminalDaemon: daemon(),
...overrides
}
}

function readiness(overrides: Partial<ServeReadiness> = {}): ServeReadiness {
return {
runtimeId: 'runtime-1',
boundEndpoint: 'ws://127.0.0.1:7777',
advertisedEndpoint: null,
managedWslCliReconciliation: 'settled',
pairing: { available: false, reason: 'disabled_by_operator', guidance: 'n/a' },
health: health(),
...overrides
}
}

describe('evaluateOrcadActivation', () => {
it('activates a candidate that proved a real PTY round trip', () => {
const verdict = evaluateOrcadActivation(readiness(), EXPECTED)
expect(verdict).toEqual({ decision: 'activate', coverage: 'pty-spawn', warnings: [] })
})

it('refuses when the candidate never published readiness', () => {
const verdict = evaluateOrcadActivation(null, EXPECTED)
expect(verdict).toMatchObject({ decision: 'reject', code: 'orcad_activation_no_readiness' })
})

it('refuses a readiness payload with no health, rather than reading silence as healthy', () => {
const { health: _dropped, ...withoutHealth } = readiness()
const verdict = evaluateOrcadActivation(withoutHealth as ServeReadiness, EXPECTED)
expect(verdict).toMatchObject({ decision: 'reject', code: 'orcad_activation_no_health' })
})

it('refuses when a different build answered — a stale process holding the port', () => {
const verdict = evaluateOrcadActivation(
readiness({ health: health({ buildHash: '0000000000000000' }) }),
EXPECTED
)
expect(verdict).toMatchObject({ decision: 'reject', code: 'orcad_activation_build_mismatch' })
})

it('refuses a listening orcad whose terminal daemon is absent', () => {
const verdict = evaluateOrcadActivation(
readiness({
health: health({
terminalDaemon: daemon({
state: 'absent',
ownsFreshSessions: false,
selfTest: { ok: false, coverage: 'pty-spawn', verdict: 'no-daemon', durationMs: 1 }
})
})
}),
EXPECTED
)
expect(verdict).toMatchObject({ decision: 'reject', code: 'orcad_activation_daemon_absent' })
})

it('refuses a degraded daemon, whose fresh terminals would not survive a restart', () => {
const verdict = evaluateOrcadActivation(
readiness({
health: health({ terminalDaemon: daemon({ state: 'degraded', ownsFreshSessions: false }) })
}),
EXPECTED
)
expect(verdict).toMatchObject({ decision: 'reject', code: 'orcad_activation_daemon_degraded' })
})

it('refuses a live daemon that failed its PTY spawn probe', () => {
const verdict = evaluateOrcadActivation(
readiness({
health: health({
terminalDaemon: daemon({
selfTest: {
ok: false,
coverage: 'pty-spawn',
verdict: 'pty-spawn-unhealthy',
durationMs: 30
}
})
})
}),
EXPECTED
)
expect(verdict).toMatchObject({
decision: 'reject',
code: 'orcad_activation_pty_self_test_failed'
})
})

it('refuses a green daemon that does not own fresh sessions', () => {
const verdict = evaluateOrcadActivation(
readiness({ health: health({ terminalDaemon: daemon({ ownsFreshSessions: false }) }) }),
EXPECTED
)
expect(verdict).toMatchObject({
decision: 'reject',
code: 'orcad_activation_no_persistent_terminals'
})
})

it('refuses a candidate that is not listening', () => {
const verdict = evaluateOrcadActivation(readiness({ boundEndpoint: null }), EXPECTED)
expect(verdict).toMatchObject({ decision: 'reject', code: 'orcad_activation_not_listening' })
})

it('activates handshake-only coverage but never records it as a proven PTY', () => {
const verdict = evaluateOrcadActivation(
readiness({
health: health({
platform: 'win32',
terminalDaemon: daemon({
selfTest: { ok: true, coverage: 'handshake', verdict: 'healthy', durationMs: 5 }
})
})
}),
EXPECTED
)
expect(verdict).toMatchObject({ decision: 'activate', coverage: 'handshake' })
expect(verdict.decision === 'activate' && verdict.warnings[0]).toContain(
'covered the daemon handshake only'
)
})

it('checks identity before health, so a wrong-build green payload cannot pass', () => {
const verdict = evaluateOrcadActivation(
readiness({
health: health({ buildHash: 'ffffffffffffffff', terminalDaemon: daemon() })
}),
EXPECTED
)
expect(verdict).toMatchObject({ code: 'orcad_activation_build_mismatch' })
})
})
155 changes: 155 additions & 0 deletions src/main/ssh/orcad-activation-gate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/**
* Whether a freshly launched orcad has earned the right to become the active one.
*
* The failure this exists to prevent is the one `docs/design/shipping-orcad.html` names
* throughout: a deployment that reports success because a port opened. orcad answers RPC
* from its own process, so "listening" stays true while the terminal daemon that owns every
* terminal is dead — a green host that cannot run a single command. Activation therefore
* reads the cross-process health payload the candidate published, not the exit code of the
* command that started it.
*
* A refusal here is not a failure to deploy. The bytes are installed and the previous
* version is still active; nothing was lost. Activating on a bad verdict is what loses
* things.
*/
import type { ServeReadiness } from '../server/serve-readiness'

export type OrcadActivationRejectCode =
| 'orcad_activation_no_readiness'
| 'orcad_activation_no_health'
| 'orcad_activation_build_mismatch'
| 'orcad_activation_not_listening'
| 'orcad_activation_daemon_absent'
| 'orcad_activation_daemon_degraded'
| 'orcad_activation_pty_self_test_failed'
| 'orcad_activation_no_persistent_terminals'

export type OrcadActivationVerdict =
| {
decision: 'activate'
/**
* `pty-spawn` means a real PTY was created and torn down inside the daemon.
* `handshake` means the daemon answered but its spawn probe is a no-op on this
* platform (win32). Carried through so an activation is never recorded as proving
* more than it did.
*/
coverage: 'pty-spawn' | 'handshake'
warnings: string[]
}
| { decision: 'reject'; code: OrcadActivationRejectCode; reason: string }

export type OrcadActivationExpectation = {
/** sha256(orcad.js).slice(0,16) computed from the bytes this client just uploaded. */
buildHash: string
/** The full content-hashed version this deploy installed. */
fullVersion: string
}

/**
* Gate an activation on what the candidate actually reported.
*
* Order matters: identity before health. A health payload from the wrong process is worse
* than no payload, because it is green and about something else.
*/
export function evaluateOrcadActivation(
readiness: ServeReadiness | null,
expected: OrcadActivationExpectation
): OrcadActivationVerdict {
if (!readiness) {
return {
decision: 'reject',
code: 'orcad_activation_no_readiness',
reason:
'The candidate orcad never published an `orca_server_ready` line. It may have exited, ' +
'failed to bind, or be wedged before readiness. Nothing was activated.'
}
}
const health = readiness.health
if (!health) {
return {
decision: 'reject',
code: 'orcad_activation_no_health',
reason:
'The candidate published readiness without a health payload, so its terminal daemon ' +
'is unverified. Absence of a verdict is not a healthy verdict — treat this build as ' +
'too old to gate on and do not activate it.'
}
}
// Why identity first: a stale orcad already holding the port would answer readiness and
// report its own (healthy) daemon. Activating on that record points the pointer at bytes
// nobody is running.
if (health.buildHash !== expected.buildHash) {
return {
decision: 'reject',
code: 'orcad_activation_build_mismatch',
reason:
`The process that answered is running build ${health.buildHash}, not the ` +
`${expected.buildHash} this deploy installed. Something else owns that port, or the ` +
'upload did not land. Nothing was activated.'
}
}
if (!readiness.boundEndpoint) {
return {
decision: 'reject',
code: 'orcad_activation_not_listening',
reason:
'The candidate reported no bound endpoint, so no client could reach it. Nothing was ' +
'activated.'
}
}
const daemon = health.terminalDaemon
if (daemon.state === 'absent') {
return {
decision: 'reject',
code: 'orcad_activation_daemon_absent',
reason:
'The candidate has no terminal daemon. Every terminal on this host would run in the ' +
'orcad process and die with it, which is the exact regression the daemon exists to ' +
'prevent. Nothing was activated.'
}
}
if (daemon.state === 'degraded') {
return {
decision: 'reject',
code: 'orcad_activation_daemon_degraded',
reason:
`The candidate's terminal daemon is degraded (self-test: ${daemon.selfTest.verdict}). ` +
'Existing sessions keep working, but fresh terminals would not survive a restart. ' +
'Nothing was activated; the previous version is still serving.'
}
}
if (!daemon.selfTest.ok) {
return {
decision: 'reject',
code: 'orcad_activation_pty_self_test_failed',
reason:
`The candidate's PTY self-test failed (${daemon.selfTest.verdict}). The host is ` +
'listening but cannot create a terminal. Nothing was activated.'
}
}
if (!daemon.ownsFreshSessions) {
return {
decision: 'reject',
code: 'orcad_activation_no_persistent_terminals',
reason:
'The candidate answered healthy but does not own fresh sessions, so new terminals ' +
'would not survive its own restart. Nothing was activated.'
}
}
const warnings: string[] = []
if (daemon.selfTest.coverage === 'handshake') {
warnings.push(
'The PTY self-test covered the daemon handshake only — this platform does not spawn a ' +
'probe PTY. Terminal creation is unproven on this host.'
)
}
if (health.buildVersion !== expected.fullVersion) {
// Not a rejection: the hash already proved identity, and ORCA_VERSION is whatever the
// launch command exported. Worth saying, because a mismatch means the launch env is wrong.
warnings.push(
`The candidate reports version ${health.buildVersion} but was installed as ` +
`${expected.fullVersion}; check ORCA_VERSION in the launch command.`
)
}
return { decision: 'activate', coverage: daemon.selfTest.coverage, warnings }
}
Loading
Loading