fix(orcad): preserve main contracts after runtime rebase

This commit is contained in:
Neil
2026-09-19 00:13:02 -07:00
parent cc40ca287b
commit a68b6f3531
12 changed files with 146 additions and 164 deletions
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { test } from 'node:test'
import { test } from 'vitest'
import { buildProfileLifetimeLock } from './build-profile-lifetime-lock.mjs'
test('stages only a successful native build and cleans its owned build directory', () => {
@@ -15,12 +15,13 @@ export function runtimeEnvironmentRevisionFailure(
if (!pairingChanged && !runtimeChanged) {
return null
}
return runtimeEnvironmentChangedFailure(environment, method)
return runtimeEnvironmentChangedFailure(environment, method, pairingChanged)
}
export function runtimeEnvironmentChangedFailure(
environment: Pick<KnownRuntimeEnvironment, 'runtimeId'>,
method: string
method: string,
pairingChanged = false
): RuntimeRpcResponse<never> {
return {
id: method,
@@ -2,7 +2,6 @@ import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../../shared/ex
import { throwIfSignalAborted, waitForPromiseWithSignal } from '../../shared/abort-signal-reason'
import { mapSettledWithConcurrency } from '../../shared/map-with-concurrency'
import { parsePtySessionId } from '../../shared/pty-session-id-format'
import { folderWorkspaceToWorktree } from '../../shared/folder-workspace-worktree'
import { isFolderRepo } from '../../shared/repo-kind'
import type { Repo } from '../../shared/repo-types'
import { splitWorktreeId, worktreeIdComparisonKey } from '../../shared/worktree/id'
@@ -226,17 +225,6 @@ function getVerifiedFolderWorktreeIds(
repoCatalog: LocalRepoCatalog
): Set<string> {
const verified = new Set<string>()
const folders = store.getFolderWorkspaces()
const counts = new Map<string, number>()
for (const folder of folders) {
counts.set(folder.id, (counts.get(folder.id) ?? 0) + 1)
}
for (const folder of folders) {
const worktree = folderWorkspaceToWorktree(folder)
if (counts.get(folder.id) === 1 && worktree.hostId === LOCAL_EXECUTION_HOST_ID) {
verified.add(worktree.id)
}
}
const metadata = readAllWorktreeMetaForHost(store, LOCAL_EXECUTION_HOST_ID)
for (const [worktreeId, meta] of Object.entries(metadata)) {
const parsed = splitWorktreeId(worktreeId)
@@ -309,10 +309,7 @@ function mapUnifiedTab(tab: Tab, projection: SessionOwnerProjection): Tab {
}
function mapTabGroup(group: TabGroup, projection: SessionOwnerProjection): TabGroup {
return {
...structuredClone(group),
worktreeId: projection.mapWorktreeId(group.worktreeId)
}
return { ...structuredClone(group), worktreeId: projection.mapWorktreeId(group.worktreeId) }
}
function paneBelongsToTabs(paneKey: string, tabIds: ReadonlySet<string>): boolean {
+8 -1
View File
@@ -1,4 +1,11 @@
import type { OrcadOptions } from './orcad-entry'
export type OrcadOptions = {
port?: number
json?: boolean
noPairing?: boolean
pairingAddress?: string
/** Literal IP to bind. Defaults to loopback; see orcad-bind-address.ts. */
bind?: string
}
/**
* orcad's flags. A value-taking flag consumes the next token whatever it looks
+11 -92
View File
@@ -13,34 +13,27 @@
*/
import process from 'node:process'
import { join } from 'node:path'
import { setAppEnvironment, type AppEnvironment } from '../../shared/app-environment'
import { setSecretStore, type SecretStore } from '../../shared/secret-store'
import type { ServeReadiness } from '../server/serve-readiness'
import type { OrcadManagedStopRequestContext } from './orcad-managed-stop-request'
import { installOrcadHostAdapters, runOrcadQuitHandlers } from './orcad-host-adapters'
export { installOrcadHostAdapters } from './orcad-host-adapters'
import { setRuntimeBrowserCommandsFactory } from '../runtime/runtime-browser-commands-factory'
import { startOrcadBrowserProvider } from './orcad-browser-startup'
import { OrcadRuntimeLifetime } from './orcad-runtime-lifetime'
import { recoverOrcadRuntimeTerminals } from './orcad-runtime-terminal-recovery'
import { configureOrcadHostDecommission } from './orcad-host-decommission'
import { parseArgs, type OrcadOptions } from './orcad-launch-options'
export { parseArgs, type OrcadOptions } from './orcad-launch-options'
import { resolveOrcadInstallRoot, resolveOrcadPath, resolveUserDataPath } from './orcad-app-paths'
import {
describeOrcadBindExposure,
OrcadBindAddressError,
resolveOrcadBindHost
} from './orcad-bind-address'
import { acquireOrcadInstanceLock, OrcadInstanceLockError } from './orcad-instance-lock'
import type { OrcadManagedStopRequestContext } from './orcad-managed-stop-request'
import {
installOrcadProcessShutdown,
ORCAD_EXIT_CONFIGURATION,
ORCAD_EXIT_FAILED
} from './orcad-process-shutdown'
import { parseArgs, type OrcadOptions } from './orcad-command-arguments'
export { parseArgs, type OrcadOptions } from './orcad-command-arguments'
import { resolveOrcadInstallRoot, resolveUserDataPath } from './orcad-app-paths'
import { describeOrcadBindExposure, resolveOrcadBindHost } from './orcad-bind-address'
import { acquireOrcadInstanceLock } from './orcad-instance-lock'
import { installOrcadProcessShutdown } from './orcad-process-shutdown'
export {
ORCAD_EXIT_OK,
ORCAD_EXIT_FAILED,
ORCAD_EXIT_CONFIGURATION,
ORCAD_SHUTDOWN_DEADLINE_MS
ORCAD_SHUTDOWN_DEADLINE_MS,
resolveOrcadExitCode
} from './orcad-process-shutdown'
import { isPtyOwnershipTransferMutationEnabled } from '../../shared/pty-ownership-transfer-release-gate'
import { initializeProfileLifetimeAdmission } from '../ssh/profile-lifetime-admission'
@@ -49,66 +42,6 @@ import {
type AiVaultSearchSettings
} from '../../shared/ai-vault-search-settings'
let runOrcadQuitHandlers = (): void => {}
function createNodeAppEnvironment(): AppEnvironment {
const quitHandlers: (() => void)[] = []
// The main signal handler awaits runtime and browser teardown before process.exit.
// Keep will-quit callbacks synchronous, but never let them pre-empt that async barrier.
runOrcadQuitHandlers = (): void => {
const errors: unknown[] = []
for (const handler of quitHandlers.splice(0)) {
try {
handler()
} catch (error) {
errors.push(error)
}
}
if (errors.length) {
throw new AggregateError(errors, 'orcad_quit_handlers_failed')
}
}
return {
getPath: resolveOrcadPath,
getAppPath: () => resolveOrcadInstallRoot(),
getVersion: () => process.env.ORCA_VERSION ?? '0.0.0-orcad',
// Why still true: consumers read this as "production build, not a dev checkout" —
// it gates HTTPS-only skill downloads, the real CLI command name, and shell-PATH
// hydration. Answering false to satisfy a path resolver would relax a security
// posture. Layout questions must ask whether the app root is an asar archive
// instead (see parcel-watcher-entry-path.ts).
isPackaged: () => true,
onWillQuit: (handler) => quitHandlers.push(handler),
exit: (code = 0) => process.exit(code),
// Why []: there are no Chromium processes on this host to measure.
getAppMetrics: () => []
}
}
/**
* Why not silently plaintext: `isEncryptionAvailable() === false` already makes every
* caller fall back to unsealed storage, which is a security posture, not a detail.
* `describeProtectionGap()` gives the reason a client can surface.
*/
function createNodeSecretStore(): SecretStore {
return {
isEncryptionAvailable: () => false,
encryptString: () => {
throw new Error('orcad_secret_sealing_unavailable')
},
decryptString: () => {
throw new Error('orcad_secret_sealing_unavailable')
},
describeProtectionGap: () =>
'This host has no OS keyring, so credentials are stored unencrypted. Pair from a desktop to manage secrets, or install and unlock a keyring.'
}
}
export function installOrcadHostAdapters(): void {
setAppEnvironment(createNodeAppEnvironment())
setSecretStore(createNodeSecretStore())
}
export type OrcadHandle = {
readiness: ServeReadiness
managedStop: OrcadManagedStopRequestContext
@@ -415,20 +348,6 @@ async function startOrcadRuntime(
}
}
/**
* Exit codes a supervisor can act on. Closed set — see docs/reference/orcad-operations.md.
*
* `ORCAD_EXIT_CONFIGURATION` is the load-bearing one: a data root owned by someone else, or
* held by another orcad, is not fixed by restarting. Restarting on it is the crash-loop the
* supervision contract has to prevent, so systemd's `RestartPreventExitStatus` needs a code
* that means "do not retry" and nothing else does.
*/
export function resolveOrcadExitCode(error: unknown): number {
return error instanceof OrcadInstanceLockError || error instanceof OrcadBindAddressError
? ORCAD_EXIT_CONFIGURATION
: ORCAD_EXIT_FAILED
}
export async function main(argv: string[] = process.argv.slice(2)): Promise<void> {
const handle = await startOrcad(parseArgs(argv))
installOrcadProcessShutdown(handle, resolveOrcadInstallRoot(), handle.managedStop)
+64
View File
@@ -0,0 +1,64 @@
import process from 'node:process'
import { setAppEnvironment, type AppEnvironment } from '../../shared/app-environment'
import { setSecretStore, type SecretStore } from '../../shared/secret-store'
import { resolveOrcadInstallRoot, resolveOrcadPath } from './orcad-app-paths'
export let runOrcadQuitHandlers = (): void => {}
function createNodeAppEnvironment(): AppEnvironment {
const quitHandlers: (() => void)[] = []
// The main signal handler awaits runtime and browser teardown before process.exit.
// Keep will-quit callbacks synchronous, but never let them pre-empt that async barrier.
runOrcadQuitHandlers = (): void => {
const errors: unknown[] = []
for (const handler of quitHandlers.splice(0)) {
try {
handler()
} catch (error) {
errors.push(error)
}
}
if (errors.length) {
throw new AggregateError(errors, 'orcad_quit_handlers_failed')
}
}
return {
getPath: resolveOrcadPath,
getAppPath: () => resolveOrcadInstallRoot(),
getVersion: () => process.env.ORCA_VERSION ?? '0.0.0-orcad',
// Why still true: consumers read this as "production build, not a dev checkout" —
// it gates HTTPS-only skill downloads, the real CLI command name, and shell-PATH
// hydration. Answering false to satisfy a path resolver would relax a security
// posture. Layout questions must ask whether the app root is an asar archive
// instead (see parcel-watcher-entry-path.ts).
isPackaged: () => true,
onWillQuit: (handler) => quitHandlers.push(handler),
exit: (code = 0) => process.exit(code),
// Why []: there are no Chromium processes on this host to measure.
getAppMetrics: () => []
}
}
/**
* Why not silently plaintext: `isEncryptionAvailable() === false` already makes every
* caller fall back to unsealed storage, which is a security posture, not a detail.
* `describeProtectionGap()` gives the reason a client can surface.
*/
function createNodeSecretStore(): SecretStore {
return {
isEncryptionAvailable: () => false,
encryptString: () => {
throw new Error('orcad_secret_sealing_unavailable')
},
decryptString: () => {
throw new Error('orcad_secret_sealing_unavailable')
},
describeProtectionGap: () =>
'This host has no OS keyring, so credentials are stored unencrypted. Pair from a desktop to manage secrets, or install and unlock a keyring.'
}
}
export function installOrcadHostAdapters(): void {
setAppEnvironment(createNodeAppEnvironment())
setSecretStore(createNodeSecretStore())
}
-45
View File
@@ -1,45 +0,0 @@
export type OrcadOptions = {
port?: number
json?: boolean
noPairing?: boolean
pairingAddress?: string
/** Literal IP to bind. Defaults to loopback; see orcad-bind-address.ts. */
bind?: string
}
export function parseArgs(argv: string[]): OrcadOptions {
const options: OrcadOptions = {}
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i]
if (arg === '--port') {
const raw = argv[i + 1]
const port = Number(raw)
if (!Number.isInteger(port) || port < 0 || port > 65535) {
throw new Error(`--port expects an integer 0-65535, got ${raw ?? "''"}`)
}
options.port = port
i += 1
} else if (arg === '--json') {
options.json = true
} else if (arg === '--no-pairing') {
options.noPairing = true
} else if (arg === '--bind') {
const value = argv[i + 1]
if (value === undefined) {
throw new Error('--bind expects a value')
}
options.bind = value
i += 1
} else if (arg === '--pairing-address') {
const value = argv[i + 1]
if (!value) {
throw new Error('--pairing-address expects a value')
}
options.pairingAddress = value
i += 1
} else {
throw new Error(`Unknown argument: ${arg}`)
}
}
return options
}
+8
View File
@@ -4,12 +4,20 @@ import {
type OrcadStopRequestListener
} from './orcad-stop-request-listener'
import type { OrcadManagedStopRequestContext } from './orcad-managed-stop-request'
import { OrcadBindAddressError } from './orcad-bind-address'
import { OrcadInstanceLockError } from './orcad-instance-lock'
export const ORCAD_EXIT_OK = 0
export const ORCAD_EXIT_FAILED = 1
export const ORCAD_EXIT_CONFIGURATION = 78
export const ORCAD_SHUTDOWN_DEADLINE_MS = 15_000
export function resolveOrcadExitCode(error: unknown): number {
return error instanceof OrcadInstanceLockError || error instanceof OrcadBindAddressError
? ORCAD_EXIT_CONFIGURATION
: ORCAD_EXIT_FAILED
}
export function installOrcadProcessShutdown(
handle: { stop(): Promise<void> },
installRoot: string,
@@ -39,7 +39,18 @@ it.each(['folder', 'worktree'] as const)('admits the real %s reconnect binding w
const sessions = {
getWorkspaceSession: () => state.workspaceSession
} as ConstructorParameters<typeof PtyBindingPersistenceOperations>[1]
const writer = new PtyBindingPersistenceOperations({ state, flushOrThrow }, sessions)
const writer = new PtyBindingPersistenceOperations(
{
state,
flushOrThrow,
writeTimer: null,
pendingWrite: null,
writeGeneration: 1,
quitFlushStarted: false,
lastDurableWriteGeneration: 0
},
sessions
)
const { identity, surfaceBinding } = f.binding
expect(
writer.persistPtyBinding({
@@ -52,7 +63,10 @@ it.each(['folder', 'worktree'] as const)('admits the real %s reconnect binding w
})
).toBe(true)
expect(flushOrThrow).toHaveBeenCalledOnce()
expect(Object.values(state.workspaceSession.tabsByWorktree).flat()[0].ptyId).toBe(f.tab.ptyId)
// A split-pane rebind must not let a later leaf steal the tab row's first-pane PTY.
expect(Object.values(state.workspaceSession.tabsByWorktree).flat()[0].ptyId).toBe(
Object.values(f.original.workspaceSession.tabsByWorktree).flat()[0].ptyId
)
expect(() =>
assertOrcadLiveSuccessorTabBindingChange(
f.before,
@@ -22,7 +22,7 @@ import {
} from '../../../../shared/pty-ownership-transfer-control-wire'
import { parsePtyOwnershipTransferReconnectRekeyRequest } from '../../../../shared/pty-ownership-transfer-reconnect-rekey-wire'
import type { RuntimePtyOwnershipTransferSourceAdapter } from '../../../providers/runtime-pty-ownership-transfer-source-adapter'
import { defineMethod, type RpcMethod } from '../core'
import { defineMethod, type RpcTypedMethod } from '../core'
import { parsePtyOwnershipTransferSourceGrantRequest } from '../../../../shared/pty-ownership-transfer-source-grant'
const RuntimeOwnedPtyPreflight = z.object({
@@ -153,8 +153,8 @@ export const PTY_OWNERSHIP_TRANSFER_METHODS = [
PTY_TRANSFER_SOURCE_STREAM_METHOD
]
function sourceMethod<T>(
name: string,
function sourceMethod<TName extends string, T>(
name: TName,
params: z.ZodType,
parse: (value: unknown) => T,
invoke: (
@@ -162,7 +162,7 @@ function sourceMethod<T>(
request: T,
binding: Parameters<RuntimePtyOwnershipTransferSourceAdapter['prepare']>[1]
) => unknown
): RpcMethod {
): RpcTypedMethod<TName, z.ZodType, unknown> {
return defineMethod({
name,
params,
+30 -1
View File
@@ -8,7 +8,36 @@ import type { ALL_RPC_METHODS } from './methods'
type RegisteredMethod = (typeof ALL_RPC_METHODS)[number]
// These schemas reach into src/main and have no shared catalog entry.
type UncataloguedMethod = 'emulator.install' | 'orchestration.send' | 'orchestration.taskUpdate'
type UncataloguedMethod =
| 'emulator.install'
| 'orcad.migration.abortCatalog'
| 'orcad.migration.catalogState'
| 'orcad.migration.commitCatalog'
| 'orcad.migration.importCatalog'
| 'orcad.migration.stageCatalog'
| 'orcad.terminalCensus'
| 'orchestration.send'
| 'orchestration.taskUpdate'
| 'pty.ownershipTransfer.abortSource'
| 'pty.ownershipTransfer.acknowledgeOutputSource'
| 'pty.ownershipTransfer.attachSource'
| 'pty.ownershipTransfer.capturedDestinationCapabilities'
| 'pty.ownershipTransfer.commitSource'
| 'pty.ownershipTransfer.controlSource'
| 'pty.ownershipTransfer.grantSource'
| 'pty.ownershipTransfer.inputSource'
| 'pty.ownershipTransfer.inspectCapturedCatalogActivation'
| 'pty.ownershipTransfer.inspectCapturedCatalogOutputCoverage'
| 'pty.ownershipTransfer.preflightSource'
| 'pty.ownershipTransfer.prepareCapturedDestination'
| 'pty.ownershipTransfer.prepareSource'
| 'pty.ownershipTransfer.publishSource'
| 'pty.ownershipTransfer.rekeyReconnectSource'
| 'pty.ownershipTransfer.replaySource'
| 'pty.ownershipTransfer.retireCapturedSourceDelivery'
| 'pty.ownershipTransfer.retireInputSource'
| 'pty.ownershipTransfer.statusSource'
| 'pty.ownershipTransfer.streamSource'
type IsAny<T> = 0 extends 1 & T ? true : false