fix(cli): preserve runtime access across Linux PID namespaces (#35769)

* fix(cli): preserve runtime access across Linux PID namespaces

* fix(cli): give retry advice after runtime replacement

* fix(cli): preserve private offline admission policy

* test(cli): align admission and launcher contracts

* test(cli): prune resolved wall-clock allowance

* test(cli): narrow dropped runtime errors without casts

* test(cli): use explicit extension for hook service import

* test(cli): replay recorded Linux sandbox denial

* test: verify packaged Linux desktop CLI ownership

* test: handle denied FUSE mounts in desktop fixture

* test: use privileged PID namespace in desktop fixture
This commit is contained in:
Brennan Benson
2026-10-29 22:55:19 -07:00
committed by GitHub
parent 6efdd36dcb
commit a856bc8af2
32 changed files with 1350 additions and 326 deletions
@@ -12,6 +12,8 @@ RUN for attempt in 1 2 3 4 5; do \
bash \
ca-certificates \
dbus-x11 \
fuse \
git \
iproute2 \
jq \
libatk-bridge2.0-0 \
@@ -8,21 +8,7 @@ if [[ $# -gt 1 ]]; then
exit 64
fi
if ((EUID == 0)); then
if ! state_dir=$(mktemp -d /tmp/orca-appimage-startup.XXXXXX); then
echo 'FAIL: unable to create the AppImage startup state directory' >&2
exit 1
fi
if ! chown orca:orca "$state_dir"; then
echo "FAIL: unable to hand the AppImage startup state directory to orca: $state_dir" >&2
rm -rf -- "$state_dir" || true
exit 1
fi
exec runuser --user orca --preserve-environment -- env \
ORCA_STARTUP_STATE_DIR="$state_dir" \
ORCA_STARTUP_STATE_DIR_CLEANUP=1 \
"$0" "$@"
fi
((EUID == 0)) || { echo 'FAIL: PID namespace assertion requires container root' >&2; exit 1; }
remove_state_dir_on_exit=${ORCA_STARTUP_STATE_DIR_CLEANUP:-0}
if [[ -n "${ORCA_STARTUP_STATE_DIR:-}" ]]; then
@@ -41,6 +27,8 @@ launcher_start_ticks=
launcher_pgid=
launcher_status=
launcher_waited=false
bundled_cli=
terminal_handle=
tree_pids=()
declare -A tree_start_ticks=()
@@ -178,6 +166,13 @@ report_launcher_exit() {
cleanup() {
local status=$?
trap - EXIT
if [[ -n "$terminal_handle" && -x "$bundled_cli" ]]; then
ORCA_USER_DATA_PATH="$profile" timeout 15s "$bundled_cli" terminal close \
--terminal "$terminal_handle" --json >/dev/null 2>&1 || true
fi
if identity_alive "$launcher_pid" "$launcher_start_ticks"; then
collect_process_tree
fi
signal_process_group TERM || true
signal_owned_processes TERM || true
if ! wait_for_owned_exit 10; then
@@ -198,9 +193,21 @@ cleanup() {
}
trap cleanup EXIT
mkdir -p "$state_dir/home" "$state_dir/config" "$state_dir/cache" "$state_dir/runtime"
profile="$state_dir/profile"
mkdir -p "$state_dir/home" "$state_dir/config" "$state_dir/cache" "$state_dir/runtime" "$profile"
chmod 700 "$state_dir/runtime"
export HOME="$state_dir/home"
export USERPROFILE="$HOME"
export XDG_DATA_HOME="$state_dir/data"
export XDG_STATE_HOME="$state_dir/state"
export CODEX_HOME="$HOME/.codex"
export CLAUDE_CONFIG_DIR="$HOME/.claude"
export ORCA_E2E_HOME_DIR="$HOME"
export ORCA_E2E_USER_DATA_DIR="$profile"
export ORCA_BACKGROUND_LAUNCH=1
export ORCA_DISABLE_MACOS_LOGIN_SHELL=1
# The desktop must produce these values itself for its packaged PTY.
unset ORCA_CLI_COMMAND ORCA_USER_DATA_PATH
export XDG_CONFIG_HOME="$state_dir/config"
export XDG_CACHE_HOME="$state_dir/cache"
export XDG_RUNTIME_DIR="$state_dir/runtime"
@@ -211,7 +218,13 @@ ulimit -c 0
[[ -r "$appimage" ]] || { echo "FAIL: AppImage is not readable: $appimage" >&2; exit 1; }
[[ -x "$appimage" ]] || { echo "FAIL: AppImage is not executable: $appimage" >&2; exit 1; }
setsid --wait dbus-run-session -- xvfb-run -a "$appimage" --appimage-extract-and-run --no-sandbox \
appimage_args=(--appimage-extract-and-run)
if [[ -c /dev/fuse && ${ORCA_STARTUP_APPIMAGE_EXTRACT_AND_RUN:-0} != 1 ]]; then
appimage_args=()
fi
printf 'AppImage desktop launch mode: %s\n' "${appimage_args[*]:-fuse}"
setsid --wait dbus-run-session -- xvfb-run -a "$appimage" "${appimage_args[@]}" --no-sandbox \
--password-store=basic --use-mock-keychain \
>"$stdout_log" 2>"$stderr_log" &
launcher_pid=$!
launcher_start_ticks=$(read_start_ticks "$launcher_pid" 2>/dev/null || true)
@@ -244,6 +257,112 @@ if ! identity_alive "$launcher_pid" "$launcher_start_ticks"; then
exit 1
fi
collect_process_tree
shim="$profile/linux-orca-cli-shim/orca"
[[ -x "$shim" ]] || { echo 'FAIL: first-use desktop shim was not written' >&2; exit 1; }
# A cached dispatcher would miss the first-use live-launcher boundary.
grep -q '^expected_runtime_identity=' "$shim"
grep -q '^expected_launcher_identity=' "$shim"
bundled_cli=$(sed -n "s/^launcher='\([^']*\)'$/\1/p" "$shim")
[[ "$bundled_cli" == */resources/bin/orca-ide && -x "$bundled_cli" ]]
[[ "$bundled_cli" != "$XDG_CACHE_HOME/"* ]]
runtime_root=$(dirname "$(dirname "$(dirname "$bundled_cli")")")
# Window/updater readiness runs in parallel with RPC startup and metadata publication.
runtime_ready=false
deadline=$((SECONDS + 30))
while ((SECONDS < deadline)); do
if ! identity_alive "$launcher_pid" "$launcher_start_ticks"; then
report_launcher_exit 'runtime transport readiness'
fi
if read -r runtime_pid runtime_id < <(jq -er '[.pid, .runtimeId] | @tsv' \
"$profile/orca-runtime.json" 2>/dev/null) &&
[[ "$runtime_pid" =~ ^[1-9][0-9]*$ && -n "$runtime_id" ]] &&
ORCA_USER_DATA_PATH="$profile" timeout --kill-after=1s 2s "$bundled_cli" status --json \
> "$state_dir/readiness-status.json" 2> "$state_dir/readiness-status.stderr" &&
jq -e --arg id "$runtime_id" \
'.ok == true and .result.runtime.reachable == true and .result.runtime.runtimeId == $id' \
"$state_dir/readiness-status.json" >/dev/null 2>&1; then
runtime_ready=true
break
fi
sleep 0.2
done
if [[ "$runtime_ready" != true ]]; then
echo 'FAIL: desktop runtime metadata/status did not become ready within 30s' >&2
cat "$state_dir/readiness-status.stderr" >&2 2>/dev/null || true
exit 1
fi
identity_alive "$launcher_pid" "$launcher_start_ticks"
[[ $(readlink -f "/proc/$runtime_pid/exe") == "$runtime_root/orca-ide" ]]
parent_pid_namespace=$(readlink /proc/self/ns/pid)
namespace_command=(unshare --pid --fork --kill-child --mount-proc)
if ! timeout --kill-after=5s 10s "${namespace_command[@]}" true 2> "$state_dir/pidns-setup.stderr"; then
cat "$state_dir/pidns-setup.stderr" >&2
bwrap_path=$(command -v bwrap || true)
printf 'bwrap availability: %s\n' "${bwrap_path:-unavailable}" >&2
if [[ -z "$bwrap_path" ]] ||
! grep -Eq '^unshare: .* (Operation not permitted|Permission denied)$' "$state_dir/pidns-setup.stderr"; then
exit 1
fi
namespace_command=("$bwrap_path" --unshare-pid --bind / / --proc /proc --die-with-parent --)
fi
ORCA_USER_DATA_PATH="$profile" timeout --kill-after=5s 30s "${namespace_command[@]}" \
bash -euc '
[[ $(readlink /proc/self/ns/pid) != "$4" ]]
[[ ! -e "/proc/$1" ]]
if kill -0 "$1" 2>/dev/null; then exit 1; fi
"$2" status --json > "$3"
[[ ! -e "/proc/$1" ]]
if kill -0 "$1" 2>/dev/null; then exit 1; fi
' -- "$runtime_pid" "$shim" "$state_dir/namespace-status.json" "$parent_pid_namespace"
jq -e --arg id "$runtime_id" \
'.ok == true and .result.runtime.reachable == true and .result.runtime.runtimeId == $id' \
"$state_dir/namespace-status.json" >/dev/null
jq -cn --arg shim "$shim" --arg id "$runtime_id" --argjson pid "$runtime_pid" \
'{assertion:"appimage-live-shim-hidden-pid", shim:$shim, runtimeId:$id, publishedPid:$pid, pidHidden:true, reachable:true}'
repo="$HOME/desktop-oracle-repo"
git init --quiet "$repo"
git -C "$repo" -c user.name=DesktopOracle -c user.email=desktop-oracle@example.invalid \
commit --quiet --allow-empty -m 'Initialize scratch repository'
cli() {
ORCA_USER_DATA_PATH="$profile" timeout 30s "$bundled_cli" "$@" --json
}
cli repo add --path "$repo" > "$state_dir/repo-add.json"
# Only the real terminal shell expands the two public path variables; no inherited override.
cat > "$HOME/desktop-oracle-env.sh" <<'TERMINAL_SCRIPT'
#!/usr/bin/env bash
printf '%s\n' "$ORCA_CLI_COMMAND" "$ORCA_USER_DATA_PATH" > "$HOME/desktop-oracle-env.txt"
exec bash --noprofile --norc
TERMINAL_SCRIPT
if ! cli terminal create --worktree "path:$repo" \
--command "bash '$HOME/desktop-oracle-env.sh'" > "$state_dir/terminal-create.json"; then
echo 'FAIL: actual packaged terminal creation failed under Xvfb; no alternate terminal used' >&2
cat "$state_dir/terminal-create.json" >&2
exit 1
fi
terminal_handle=$(jq -er '.result.terminal.handle' "$state_dir/terminal-create.json")
jq -e '.ok == true and (.result.terminal.ptyId | type == "string" and length > 0)' \
"$state_dir/terminal-create.json" >/dev/null
deadline=$((SECONDS + 30))
while [[ ! -s "$HOME/desktop-oracle-env.txt" ]] && ((SECONDS < deadline)); do
sleep 0.2
done
if [[ ! -s "$HOME/desktop-oracle-env.txt" ]]; then
echo 'FAIL: actual packaged PTY did not execute the path-variable query' >&2
cli terminal read --terminal "$terminal_handle" >&2 || true
exit 1
fi
mapfile -t terminal_env < "$HOME/desktop-oracle-env.txt"
if [[ ${#terminal_env[@]} != 2 || ${terminal_env[0]} != "$shim" || ${terminal_env[1]} != "$profile" ]]; then
echo 'FAIL: packaged PTY owning CLI command/profile mismatch' >&2
cat "$HOME/desktop-oracle-env.txt" >&2
exit 1
fi
jq -cn --slurpfile created "$state_dir/terminal-create.json" \
--arg command "${terminal_env[0]}" --arg profile "${terminal_env[1]}" \
'{assertion:"packaged-terminal-owning-cli", terminal:$created[0].result.terminal.handle, ptyId:$created[0].result.terminal.ptyId, ORCA_CLI_COMMAND:$command, ORCA_USER_DATA_PATH:$profile}'
collect_process_tree
xvfb_pids=()
for pid in "${tree_pids[@]}"; do
-1
View File
@@ -769,7 +769,6 @@
4 test-no-wall-clock:src/cli/runtime/serve-bundle-park.test.ts
5 test-no-wall-clock:src/cli/runtime/serve-pid1-init-reaper.test.ts
1 test-no-wall-clock:src/cli/runtime/status-caller.test.ts
2 test-no-wall-clock:src/cli/runtime/status.test.ts
1 test-no-wall-clock:src/main/acp/acp-structured-mail-admission.test.ts
2 test-no-wall-clock:src/main/agent-hooks/managed-hook-timeout.test.ts
2 test-no-wall-clock:src/main/agent-hooks/manual-compact-hook-stream.test.ts
@@ -50,6 +50,8 @@ describe('packaged shutdown matrix', () => {
args.some((arg) => arg.includes('120s /input/orca.AppImage --appimage-extract'))
)
expect(startup).toHaveLength(1)
expect(startup[0][startup[0].indexOf('--user') + 1]).toBe('0')
expect(startup[0][startup[0].indexOf('--cap-add') + 1]).toBe('SYS_ADMIN')
expect(extraction).toHaveLength(1)
expect(commands().indexOf(startup[0])).toBeLessThan(commands().indexOf(extraction[0]))
expect(signalRuns()).toHaveLength(6)
@@ -91,6 +93,64 @@ describe('packaged shutdown matrix', () => {
])
})
it.each(['mount-denied', 'generic-exit', 'app-started', 'extract-assertion-failed'])(
'bounds desktop extraction fallback: %s',
async (scenario) => {
// Mount diagnostics captured in package job123685326492; Docker execution here is mocked.
const mountFailure = {
status: 127,
stdout: 'AppImage desktop launch mode: fuse\n',
stderr:
'FAIL: desktop launcher exited before the updater-setup-done marker (status=127)\n' +
'Cannot mount AppImage, please check your FUSE setup.\n' +
'/usr/bin/fusermount3: mount failed: Permission denied\n'
}
let attempts = 0
spawnSync.mockImplementation((_, args) => {
if (!args.includes('/usr/local/bin/run-appimage-desktop-startup-case')) {
return succeeded
}
attempts++
if (attempts > 1) {
return scenario === 'extract-assertion-failed'
? {
...succeeded,
status: 1,
stderr: 'FAIL: packaged PTY owning CLI command/profile mismatch'
}
: succeeded
}
if (scenario === 'generic-exit') {
return { ...mountFailure, stderr: 'Unexpected crash\n' }
}
if (scenario === 'app-started') {
return { ...mountFailure, stderr: `${mountFailure.stderr}[startup] app-ready t=10\n` }
}
return mountFailure
})
await (scenario === 'mount-denied'
? run('--all-entrypoints')
: expect(run('--all-entrypoints')).rejects.toThrow(
`failed with status ${scenario === 'extract-assertion-failed' ? 1 : 127}`
))
const startup = commands().filter((args) =>
args.includes('/usr/local/bin/run-appimage-desktop-startup-case')
)
const retries = scenario === 'mount-denied' || scenario === 'extract-assertion-failed'
expect(startup).toHaveLength(retries ? 2 : 1)
expect(startup[0]).not.toContain('ORCA_STARTUP_APPIMAGE_EXTRACT_AND_RUN=1')
if (retries) {
expect(startup[1]).toContain('ORCA_STARTUP_APPIMAGE_EXTRACT_AND_RUN=1')
expect(startup.every((args) => args.includes('--rm'))).toBe(true)
expect(startup.every((args) => args.includes(`${artifact}:/input/orca.AppImage:ro`))).toBe(
true
)
}
expect(signalRuns()).toHaveLength(scenario === 'mount-denied' ? 6 : 0)
expect(commands().at(-2).slice(0, 2)).toEqual(['volume', 'rm'])
}
)
it('attributes failures and still attempts later cases before cleanup', async () => {
spawnSync.mockImplementation((_, args) =>
args.at(-1) === 'INT' ? { ...succeeded, status: 7 } : succeeded
@@ -176,7 +176,8 @@ try {
function runDesktopStartupOracle({ image, appImage, platform }) {
console.log('Running original AppImage desktop startup oracle...')
docker([
const fuseDevicePresent = existsSync('/dev/fuse')
const startupOptions = [
'run',
'--rm',
'--init',
@@ -191,19 +192,52 @@ function runDesktopStartupOracle({ image, appImage, platform }) {
'256m',
'--cap-drop',
'ALL',
'--cap-add',
'SYS_ADMIN',
// Only this oracle needs a nested PID namespace; the restricted CLI cases stay unchanged.
'--security-opt',
'no-new-privileges',
'seccomp=unconfined',
'--security-opt',
'apparmor=unconfined',
...(fuseDevicePresent ? ['--device', '/dev/fuse'] : ['--security-opt', 'no-new-privileges']),
'--user',
'orca',
'0',
'--entrypoint',
'/usr/local/bin/run-appimage-desktop-startup-case',
'-e',
'ORCA_STARTUP_DIAGNOSTICS=1',
'-v',
`${appImage}:/input/orca.AppImage:ro`,
image,
'/input/orca.AppImage'
])
`${appImage}:/input/orca.AppImage:ro`
]
const launch = (extraOptions = []) =>
docker([...startupOptions, ...extraOptions, image, '/input/orca.AppImage'], {
allowFailure: true
})
let result = launch()
process.stdout.write(result.stdout)
process.stderr.write(result.stderr)
const output = result.stdout + result.stderr
// Device access alone does not prove that this fixture may mount the AppImage.
if (
result.status === 127 &&
result.stdout.includes('AppImage desktop launch mode: fuse\n') &&
output.includes(
'FAIL: desktop launcher exited before the updater-setup-done marker (status=127)'
) &&
output.includes('Cannot mount AppImage, please check your FUSE setup.') &&
/^\/usr\/bin\/fusermount3?: mount failed: Permission denied\r?$/m.test(output) &&
!output.includes('[startup]')
) {
console.log(
'FUSE mount unavailable (permission denied); trying original AppImage --appimage-extract-and-run in a fresh container.'
)
result = launch(['-e', 'ORCA_STARTUP_APPIMAGE_EXTRACT_AND_RUN=1'])
process.stdout.write(result.stdout)
process.stderr.write(result.stderr)
}
if (result.status !== 0) {
fail(`Desktop AppImage startup oracle failed with status ${result.status}`)
}
}
function valueAfter(flag) {
+8 -1
View File
@@ -57,7 +57,14 @@ const {
vi.mock('../runtime-client', () => {
class RuntimeClient {
call = callMock
getCliStatus = getCliStatusMock
async getOfflineRuntimeAdmission() {
const status = await getCliStatusMock()
return {
reachable: status.result.runtime.reachable,
legacyPidRunning: status.result.app.running,
pid: status.result.app.pid ?? null
}
}
}
class RuntimeClientError extends Error {
+3 -3
View File
@@ -114,9 +114,9 @@ async function updateAdmittedEnabledOnDisk(enabled: boolean): Promise<{
}
async function updateRunningRuntime(client: RuntimeClient, enabled: boolean): Promise<boolean> {
const status = await client.getCliStatus()
if (!status.result.runtime.reachable) {
if (status.result.app.running) {
const status = await client.getOfflineRuntimeAdmission()
if (!status.reachable) {
if (status.legacyPidRunning) {
throw new RuntimeClientError(
'runtime_error',
'Orca is running but unavailable. Retry when it responds, or stop Orca before changing agent hooks offline.'
@@ -42,7 +42,14 @@ const mocks = vi.hoisted(() => ({ root: vi.fn(), status: vi.fn() }))
vi.mock('../runtime-client', () => ({
getDefaultUserDataPath: mocks.root,
RuntimeClient: class {
getCliStatus = mocks.status
async getOfflineRuntimeAdmission() {
const status = await mocks.status()
return {
reachable: status.result.runtime.reachable,
legacyPidRunning: status.result.app.running,
pid: status.result.app.pid ?? null
}
}
},
RuntimeClientError: class extends Error {
constructor(
+8 -1
View File
@@ -43,7 +43,14 @@ vi.mock('../runtime-client', () => {
}
class RuntimeClient {
getCliStatus = getCliStatusMock
async getOfflineRuntimeAdmission() {
const status = await getCliStatusMock()
return {
reachable: status.result.runtime.reachable,
legacyPidRunning: status.result.app.running,
pid: status.result.app.pid ?? null
}
}
constructor(
_userDataPath?: string,
+2 -2
View File
@@ -83,8 +83,8 @@ function rejectProfileStateRemoteSelection(flags: ReadonlyMap<string, string | b
}
async function requireStoppedRuntime(client: RuntimeClient): Promise<void> {
const status = await client.getCliStatus()
if (status.result.runtime.reachable || status.result.app.running) {
const status = await client.getOfflineRuntimeAdmission()
if (status.reachable || status.legacyPidRunning) {
throw new RuntimeClientError(
'runtime_error',
'Stop Orca before profile-state rollback so no process can write the SQLite database.'
+8 -6
View File
@@ -8,14 +8,14 @@ const {
constructorArgsMock,
moduleEvaluationMock,
callMock,
getCliStatusMock,
getOfflineRuntimeAdmissionMock,
testUserDataPathRef
} = vi.hoisted(() => ({
applyAgentStatusHooksEnabledMock: vi.fn(async () => []),
constructorArgsMock: vi.fn(),
moduleEvaluationMock: vi.fn(),
callMock: vi.fn(),
getCliStatusMock: vi.fn(),
getOfflineRuntimeAdmissionMock: vi.fn(),
testUserDataPathRef: { current: '' }
}))
@@ -38,7 +38,7 @@ vi.mock('./runtime-client', () => {
moduleEvaluationMock()
class RuntimeClient {
call = callMock
getCliStatus = getCliStatusMock
getOfflineRuntimeAdmission = getOfflineRuntimeAdmissionMock
openOrca = vi.fn()
constructor(...args: unknown[]) {
@@ -62,7 +62,7 @@ describe('RuntimeClient module-graph deferral', () => {
applyAgentStatusHooksEnabledMock.mockClear()
constructorArgsMock.mockClear()
callMock.mockReset()
getCliStatusMock.mockReset()
getOfflineRuntimeAdmissionMock.mockReset()
logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
})
@@ -104,8 +104,10 @@ describe('RuntimeClient module-graph deferral', () => {
async (_name, argv, constructs) => {
vi.stubEnv('ORCA_PAIRING_CODE', 'pairing-code')
vi.stubEnv('ORCA_ENVIRONMENT', 'some-environment')
getCliStatusMock.mockResolvedValue({
result: { runtime: { reachable: false }, app: { running: false } }
getOfflineRuntimeAdmissionMock.mockResolvedValue({
reachable: false,
legacyPidRunning: false,
pid: null
})
await main(argv, '/tmp/repo')
+16 -11
View File
@@ -6,6 +6,7 @@ import { afterEach, describe, expect, it, onTestFinished, vi } from 'vitest'
import { ORCHESTRATION_CONTRACT_RUNTIME_CAPABILITY } from '../shared/protocol-version'
import { RuntimeClient, RuntimeClientError, RuntimeRpcFailureError } from './runtime-client'
import { launchOrcaApp } from './runtime/launch'
import { formatCliError } from './cli-error'
vi.mock('./runtime/launch', () => ({
launchOrcaApp: vi.fn()
@@ -235,15 +236,16 @@ describe.skipIf(process.platform === 'win32')('RuntimeClient', () => {
})
})
it('reports stale_bootstrap when bootstrap artifacts exist but no runtime is reachable', async () => {
it('reports an unavailable endpoint and lets explicit open recover from it', async () => {
const userDataPath = makeRuntimeClientFixture()
writeMetadata(userDataPath, join(userDataPath, 'missing.sock'), 'token', findUnusedPid())
const client = new RuntimeClient(userDataPath, 100)
const status = await client.getCliStatus()
expect(status.result.runtime.state).toBe('stale_bootstrap')
expect(status.result.runtime.reachable).toBe(false)
await expect(client.getCliStatus()).rejects.toMatchObject({ code: 'runtime_unavailable' })
const error = await client.getCliStatus().catch((failure: unknown) => failure)
expect(formatCliError(error)).not.toContain('Orca is not running')
await expect(client.openOrca(1)).rejects.toMatchObject({ code: 'runtime_open_timeout' })
expect(launchOrcaApp).toHaveBeenCalledTimes(1)
})
it('reports graph_not_ready when the runtime is reachable but graph is unavailable', async () => {
@@ -424,12 +426,12 @@ describe.skipIf(process.platform === 'win32')('RuntimeClient', () => {
it('preserves a dropped read-only orchestration failure exactly', async () => {
const userDataPath = makeRuntimeClientFixture()
const endpoint = join(userDataPath, 'runtime.sock')
let request: Record<string, unknown> | undefined
let request: unknown
const server = createServer((socket) => {
sockets.add(socket)
socket.once('close', () => sockets.delete(socket))
socket.once('data', (data) => {
request = JSON.parse(String(data).trim()) as Record<string, unknown>
request = JSON.parse(String(data).trim())
socket.end()
})
})
@@ -443,10 +445,13 @@ describe.skipIf(process.platform === 'win32')('RuntimeClient', () => {
.catch((error: unknown) => error)
expect(failure).toBeInstanceOf(RuntimeClientError)
expect((failure as RuntimeClientError).message).toBe(
'The Orca runtime closed the connection before responding. Restart Orca and try again.'
)
expect((failure as RuntimeClientError).data).toBeUndefined()
if (!(failure instanceof RuntimeClientError)) {
throw new Error('Expected a runtime client error', { cause: failure })
}
expect(failure.message).toBe('Orca closed the connection before responding.')
expect(failure.data).toEqual({
nextSteps: ['Check that Orca is open and that this terminal can access it.']
})
expect(request).toMatchObject({ method: 'orchestration.workerShow' })
expect(request).not.toHaveProperty('orchestrationRequestId')
})
+20 -41
View File
@@ -9,9 +9,14 @@ import {
orchestrationMigrationData
} from '../../shared/orchestration-rpc-contract'
import type { PairedRuntimeConnection } from '../../shared/paired-runtime-connection'
import { launchOrcaApp } from './launch'
import { openLocalOrca } from './open-local-orca'
import { getDefaultUserDataPath, readMetadata } from './metadata'
import { getCliStatus, projectRemoteAppStatus } from './status'
import {
getCliStatus,
getOfflineRuntimeAdmission,
type OfflineRuntimeAdmission,
projectRemoteAppStatus
} from './status'
import { sendRequest } from './transport'
import { RuntimeClientError, RuntimeRpcFailureError, type RuntimeRpcSuccess } from './types'
import {
@@ -249,6 +254,18 @@ export class RuntimeClient {
return getCliStatus(this.userDataPath)
}
async getOfflineRuntimeAdmission(): Promise<OfflineRuntimeAdmission> {
if (this.remote) {
const status = await this.getCliStatus()
return {
reachable: status.result.runtime.reachable,
legacyPidRunning: status.result.app.running,
pid: status.result.app.pid ?? null
}
}
return getOfflineRuntimeAdmission(this.userDataPath)
}
private async ensureOrchestrationContractCompatible(timeoutMs: number): Promise<void> {
if (!this.orchestrationContractCheck) {
this.orchestrationContractCheck = this.checkOrchestrationContractCompatibility(
@@ -277,44 +294,6 @@ export class RuntimeClient {
}
async openOrca(timeoutMs = 15_000): Promise<RuntimeRpcSuccess<CliStatusResult>> {
const initial = await this.getCliStatus()
if (this.remote) {
return initial
}
// A blocked runtime cannot open a window; a second app would only hit its instance lock.
if (initial.result.app.desktopWindowStatus === 'blocked') {
throwDesktopActivationBlocked()
}
launchOrcaApp()
if (initial.result.app.desktopWindowStatus === 'available') {
return initial
}
const startedAt = Date.now()
while (Date.now() - startedAt < timeoutMs) {
const status = await this.getCliStatus()
if (status.result.app.desktopWindowStatus === 'blocked') {
throwDesktopActivationBlocked()
}
if (status.result.app.desktopWindowStatus === 'available') {
return status
}
await delay(250)
}
throw new RuntimeClientError(
'runtime_open_timeout',
'Timed out waiting for an Orca desktop window. The runtime may still be running headlessly.'
)
return this.remote ? this.getCliStatus() : openLocalOrca(() => this.getCliStatus(), timeoutMs)
}
}
function throwDesktopActivationBlocked(): never {
throw new RuntimeClientError(
'desktop_activation_blocked',
'Orca is running headlessly, but it cannot open a desktop window safely because the persistent terminal provider is unavailable. Quit Orca normally and start the app again; do not use open -n.'
)
}
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
@@ -0,0 +1,36 @@
{
"provenance": {
"capture": "QA3286, real rendered remote Orca terminal, Linux arm64 glibc",
"codexVersion": "0.165.0",
"command": ["orca", "status", "--json"],
"policy": {
"sandbox_mode": "workspace-write",
"sandbox_workspace_write.network_access": false
},
"publishedPidProbeSystemCode": "ESRCH",
"source": "linux-container/query-codex-restricted-kernel.txt",
"rawSha256": "2b0af9d523c949d747b17ffe88364659c3be772a08b4a379807a39104e6128ae",
"cliSha256": "c4520c5afe063accedb22175f7d75f2f99f90fa6ea72bf7530be456050a0d97a",
"deployedVersion": "0.1.0+da3cb3b77bd0",
"sanitization": "Command executable path reduced to orca; response content unchanged."
},
"exitCode": 1,
"response": {
"id": "local",
"ok": false,
"error": {
"code": "runtime_access_denied",
"message": "Permission denied connecting to Orca (EPERM). Orca may be running normally; this command's sandbox or OS permissions block the connection.",
"data": {
"systemCode": "EPERM",
"nextSteps": [
"Re-run this command outside its sandbox, or as a user allowed to reach the Orca runtime.",
"Do not restart Orca or run 'orca open'; a restart cannot grant this command access."
]
}
},
"_meta": {
"runtimeId": null
}
}
}
+67 -3
View File
@@ -1,6 +1,10 @@
import { EventEmitter } from 'node:events'
import { resolve } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { createServer, type Socket } from 'node:net'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as ServePreflight from './serve-electron-preflight'
import { MacServeStartupAbortError } from './serve-signal-exit-diagnostic'
const { spawnMock, preflightMock, recordAbortMock, superviseMock } = vi.hoisted(() => ({
@@ -11,7 +15,8 @@ const { spawnMock, preflightMock, recordAbortMock, superviseMock } = vi.hoisted(
}))
vi.mock('child_process', () => ({ spawn: spawnMock, spawnSync: vi.fn() }))
vi.mock('./serve-electron-preflight', () => ({
vi.mock('./serve-electron-preflight', async (original) => ({
...(await original<typeof ServePreflight>()),
preflightElectronServe: preflightMock,
recordElectronServeStartupAbort: recordAbortMock
}))
@@ -22,6 +27,11 @@ vi.mock('./serve-update-supervisor', async (importOriginal) => ({
import { serveOrcaApp } from './launch'
let actualPreflight: typeof ServePreflight
beforeAll(async () => {
actualPreflight = await vi.importActual<typeof ServePreflight>('./serve-electron-preflight')
})
class FakeChildProcess extends EventEmitter {
stdout = new EventEmitter()
kill = vi.fn()
@@ -69,3 +79,57 @@ describe('serveOrcaApp: Electron serve preflight', () => {
expect(recordAbortMock).toHaveBeenCalledWith(PROFILE, 'aborted at startup')
})
})
describe.skipIf(process.platform === 'win32')(
'serve launch with actual preflight dependencies',
() => {
it.each(['absent', 'accepting'])(
'reaches the existing launch/refusal seam for a %s endpoint',
async (state) => {
const root = mkdtempSync(join(tmpdir(), 'orca-serve-launch-'))
const endpoint = join(root, 'runtime.sock')
const sockets = new Set<Socket>()
const server = createServer((socket) => {
sockets.add(socket)
socket.once('close', () => sockets.delete(socket))
socket.resume()
})
try {
vi.clearAllMocks()
vi.stubEnv('ORCA_USER_DATA_PATH', root)
vi.stubEnv('ORCA_APP_EXECUTABLE', '/opt/orca/orca')
vi.stubEnv('ORCA_SERVE_RUNTIME', 'electron')
writeFileSync(
join(root, 'orca-runtime.json'),
JSON.stringify({
runtimeId: 'leftover',
pid: 2147483647,
authToken: 'fixture',
transports: [{ kind: 'unix', endpoint }],
startedAt: 1
})
)
if (state === 'accepting') {
await new Promise<void>((resolve) => server.listen(endpoint, resolve))
}
preflightMock.mockImplementation(actualPreflight.preflightElectronServe)
superviseMock.mockResolvedValue(0)
spawnMock.mockReturnValue(new FakeChildProcess())
vi.spyOn(process.stderr, 'write').mockImplementation(() => true)
expect(await serveOrcaApp({})).toBe(state === 'absent' ? 0 : 3)
expect(spawnMock).toHaveBeenCalledTimes(state === 'absent' ? 1 : 0)
} finally {
for (const socket of sockets) {
socket.destroy()
}
if (server.listening) {
await new Promise<void>((resolve) => server.close(() => resolve()))
}
vi.restoreAllMocks()
vi.unstubAllEnvs()
rmSync(root, { recursive: true, force: true })
}
}
)
}
)
@@ -0,0 +1,305 @@
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
realpathSync,
renameSync,
rmSync,
writeFileSync
} from 'node:fs'
import { createServer, type Socket } from 'node:net'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { setAppEnvironment } from '../../shared/app-environment'
import { readAgentHookSettingsFromProfileState } from '../../main/persistence/profile-state/profile-state-offline-settings'
import { acquireProfileStateMaintenance } from '../../main/persistence/profile-state/profile-state-access'
import { PROFILE_STATE_RECOVERY_RESULT_PREFIX } from '../../shared/profile-state-recovery-command'
import type * as ProcessHost from '@orca/process-host'
import { main } from '../index'
const { runChild } = vi.hoisted(() => ({ runChild: vi.fn() }))
vi.mock('@orca/process-host', async (original) => ({
...(await original<typeof ProcessHost>()),
runProcess: runChild
}))
// Exercise the existing hook installer on one isolated target; do not discover installed agents.
vi.mock('../../main/agent-hooks/managed-agent-hook-controls', async () => {
const { claudeHookService } = await import('../../main/claude/hook-service.js')
return {
applyAgentStatusHooksEnabled: vi.fn(async (enabled: boolean) => [
enabled ? claudeHookService.install({ claudeVersion: '2.1.0' }) : claudeHookService.remove()
]),
getManagedAgentHookStatuses: () => [claudeHookService.getStatus()]
}
})
const priorExit = process.exitCode
const sockets = new Set<Socket>()
let root: string
let profile: string
let endpoint: string
let server: ReturnType<typeof createServer> | undefined
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'orca-offline-command-'))
profile = join(root, 'profiles', 'fixture')
endpoint = join(root, 'runtime.sock')
mkdirSync(profile, { recursive: true })
writeFileSync(
join(root, 'orca-profile-index.json'),
JSON.stringify({ activeProfileId: 'fixture', profiles: [{ id: 'fixture' }] })
)
writeFileSync(
join(profile, 'orca-data.json'),
JSON.stringify({ settings: { agentStatusHooksEnabled: false }, retained: { value: 7 } })
)
for (const key of [
'ORCA_PAIRING_CODE',
'ORCA_REMOTE_PAIRING',
'ORCA_ENVIRONMENT',
'ELECTRON_RUN_AS_NODE',
'ORCA_APP_EXECUTABLE',
'WSL_DISTRO_NAME'
]) {
vi.stubEnv(key, undefined)
}
for (const key of [
'HOME',
'USERPROFILE',
'XDG_CONFIG_HOME',
'XDG_DATA_HOME',
'XDG_CACHE_HOME',
'CODEX_HOME',
'CLAUDE_CONFIG_DIR'
]) {
vi.stubEnv(key, join(root, key))
}
vi.stubEnv('ORCA_USER_DATA_PATH', root)
setAppEnvironment({
getPath: () => root,
getAppPath: () => root,
getVersion: () => 'test',
isPackaged: () => false,
onWillQuit: () => {},
exit: () => {},
getAppMetrics: () => []
})
runChild.mockReset()
process.exitCode = 0
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
})
afterEach(async () => {
for (const socket of sockets) {
socket.destroy()
}
sockets.clear()
if (server) {
const listening = server
await new Promise<void>((resolve) => listening.close(() => resolve()))
}
server = undefined
vi.restoreAllMocks()
vi.unstubAllEnvs()
process.exitCode = priorExit
rmSync(root, { recursive: true, force: true })
})
function metadata(shape = 'complete', pid = 2147483647) {
writeFileSync(
join(root, 'orca-runtime.json'),
JSON.stringify({
runtimeId: 'runtime-before',
pid,
authToken: shape === 'incomplete' ? null : 'fixture-token',
startedAt: 1,
...(shape === 'singular'
? { transport: { kind: 'unix', endpoint } }
: { transports: [{ kind: 'unix', endpoint }] })
})
)
}
function settings() {
return readAgentHookSettingsFromProfileState({
dataFile: join(profile, 'orca-data.json'),
databaseFile: join(profile, 'profile-state.db'),
profileId: 'fixture'
})
}
function output() {
return JSON.parse(String(vi.mocked(console.log).mock.calls.at(-1)?.[0]))
}
async function refusedEndpoint() {
const listener = createServer()
await new Promise<void>((resolve) => listener.listen(endpoint, resolve))
renameSync(endpoint, `${endpoint}.stale`)
await new Promise<void>((resolve) => listener.close(() => resolve()))
renameSync(`${endpoint}.stale`, endpoint)
}
describe.skipIf(process.platform === 'win32')(
'offline CLI admission through dispatch and persistence',
() => {
it.each(['complete', 'singular', 'incomplete', 'refused'])(
'recovers JSON and toggles hooks both ways with %s stopped metadata',
async (shape) => {
metadata(shape)
if (shape === 'refused') {
await refusedEndpoint()
}
await main(['profile', 'state', 'rollback', '--current-json', '--json'], root)
expect(process.exitCode).toBe(0)
expect(output()).toMatchObject({ ok: true, result: { storage: 'json' } })
expect(JSON.parse(readFileSync(join(profile, 'orca-data.json'), 'utf8'))).toMatchObject({
retained: { value: 7 }
})
for (const enabled of [true, false]) {
await main(['agent', 'hooks', enabled ? 'on' : 'off', '--json'], root)
expect(process.exitCode).toBe(0)
expect(output()).toMatchObject({
ok: true,
result: {
enabled,
appliedBy: 'offline',
statuses: [expect.objectContaining({ agent: 'claude', managedHooksPresent: enabled })]
}
})
expect(settings().agentStatusHooksEnabled).toBe(enabled)
const config = JSON.parse(
readFileSync(join(root, 'HOME', '.claude', 'settings.json'), 'utf8')
)
expect(JSON.stringify(config.hooks ?? {}).includes('orca')).toBe(enabled)
}
}
)
it.each(['packaged', 'dev', 'older-override'])(
'retains the %s helper selection and never replays an uncertain child result',
async (kind) => {
metadata()
const executable = kind === 'older-override' ? join(root, 'older-Orca') : process.execPath
if (kind === 'older-override') {
vi.stubEnv('ORCA_APP_EXECUTABLE', executable)
} else {
vi.stubEnv('ELECTRON_RUN_AS_NODE', '1')
}
vi.stubEnv('ORCA_APP_EXECUTABLE_NEEDS_APP_ROOT', kind === 'dev' ? '1' : undefined)
const result = {
profileId: 'fixture',
dataFile: join(profile, 'orca-data.json'),
databaseFile: join(profile, 'profile-state.db'),
exportPaths: [],
backups: [],
revision: null,
quarantineDirectory: profile,
removedDatabaseFiles: [],
storage: 'json',
restoredPath: join(profile, 'orca-data.json')
}
runChild.mockImplementationOnce(async () => {
// Parent observation precedes the existing child, with no new parent lease.
acquireProfileStateMaintenance(root).release()
return {
code: 0,
signal: null,
timedOut: false,
stdout: PROFILE_STATE_RECOVERY_RESULT_PREFIX + JSON.stringify({ ok: true, result }),
stderr: ''
}
})
await main(['profile', 'state', 'rollback', '--current-json', '--json'], root)
expect(process.exitCode).toBe(0)
expect(runChild).toHaveBeenCalledOnce()
expect(runChild.mock.calls[0][0]).toMatchObject({
program: executable,
timeoutMs: null,
env: { ORCA_BACKGROUND_LAUNCH: '1', ORCA_USER_DATA_PATH: realpathSync(root) }
})
expect(runChild.mock.calls[0][0].args).toContain('--serve')
expect(JSON.parse(runChild.mock.calls[0][0].args.at(-1))).toMatchObject({
selector: { kind: 'current-json' }
})
runChild.mockClear().mockResolvedValue({
code: 1,
signal: null,
timedOut: false,
stdout: 'partial',
stderr: 'uncertain result'
})
await main(['profile', 'state', 'rollback', '--current-json', '--json'], root)
expect(process.exitCode).toBe(1)
expect(runChild).toHaveBeenCalledOnce()
expect(existsSync(join(profile, 'profile-state.db'))).toBe(false)
}
)
it('refuses a visible PID without creating a database or changing profile and hook files', async () => {
metadata('complete', process.pid)
const before = readFileSync(join(profile, 'orca-data.json'))
for (const args of [
['profile', 'state', 'rollback', '--current-json'],
['agent', 'hooks', 'on'],
['agent', 'hooks', 'off']
]) {
process.exitCode = 0
await main([...args, '--json'], root)
expect(process.exitCode).toBe(1)
expect(output()).toMatchObject({ ok: false, error: { code: 'runtime_error' } })
expect(readFileSync(join(profile, 'orca-data.json'))).toEqual(before)
expect(existsSync(join(profile, 'profile-state.db'))).toBe(false)
expect(existsSync(join(root, 'HOME', '.claude'))).toBe(false)
}
})
it.each(['close', 'server-error'])(
'never replays a failed online settings update offline (%s)',
async (failure) => {
metadata()
const methods: string[] = []
const listener = createServer((socket) => {
sockets.add(socket)
socket.once('close', () => sockets.delete(socket))
socket.once('data', (data) => {
const request = JSON.parse(String(data))
methods.push(request.method)
if (request.method === 'settings.update' && failure === 'close') {
socket.end()
return
}
socket.end(
`${JSON.stringify({
id: request.id,
...(request.method === 'status.get'
? {
ok: true,
result: {
runtimeId: 'runtime-before',
graphStatus: 'ready',
authoritativeWindowId: null
}
}
: {
ok: false,
error: { code: 'runtime_unavailable', message: 'update rejected' }
}),
_meta: { runtimeId: 'runtime-before' }
})}\n`
)
})
})
server = listener
await new Promise<void>((resolve) => listener.listen(endpoint, resolve))
const before = readFileSync(join(profile, 'orca-data.json'))
await main(['agent', 'hooks', 'on', '--json'], root)
expect(process.exitCode).toBe(1)
expect(methods).toEqual(['status.get', 'settings.update'])
expect(readFileSync(join(profile, 'orca-data.json'))).toEqual(before)
expect(existsSync(join(profile, 'profile-state.db'))).toBe(false)
expect(existsSync(join(root, 'HOME', '.claude'))).toBe(false)
}
)
}
)
+65
View File
@@ -0,0 +1,65 @@
import type { CliStatusResult } from '../../shared/runtime-types'
import { launchOrcaApp } from './launch'
import { RuntimeClientError, RuntimeRpcFailureError, type RuntimeRpcSuccess } from './types'
type ReadStatus = () => Promise<RuntimeRpcSuccess<CliStatusResult>>
export async function openLocalOrca(
readStatus: ReadStatus,
timeoutMs: number
): Promise<RuntimeRpcSuccess<CliStatusResult>> {
const initial = await getStatusForOpen(readStatus)
// A blocked runtime cannot open a window; a second app would only hit its instance lock.
if (initial?.result.app.desktopWindowStatus === 'blocked') {
throwDesktopActivationBlocked()
}
launchOrcaApp()
if (initial?.result.app.desktopWindowStatus === 'available') {
return initial
}
const startedAt = Date.now()
while (Date.now() - startedAt < timeoutMs) {
const status = await getStatusForOpen(readStatus)
if (status?.result.app.desktopWindowStatus === 'blocked') {
throwDesktopActivationBlocked()
}
if (status?.result.app.desktopWindowStatus === 'available') {
return status
}
await delay(250)
}
throw new RuntimeClientError(
'runtime_open_timeout',
'Timed out waiting for an Orca desktop window. The runtime may still be running headlessly.'
)
}
async function getStatusForOpen(
readStatus: ReadStatus
): Promise<RuntimeRpcSuccess<CliStatusResult> | null> {
try {
return await readStatus()
} catch (error) {
// Explicit open may launch after an unavailable endpoint; a denied or rejected call cannot.
if (
error instanceof RuntimeClientError &&
!(error instanceof RuntimeRpcFailureError) &&
error.code === 'runtime_unavailable'
) {
return null
}
throw error
}
}
function throwDesktopActivationBlocked(): never {
throw new RuntimeClientError(
'desktop_activation_blocked',
'Orca is running headlessly, but it cannot open a desktop window safely because the persistent terminal provider is unavailable. Quit Orca normally and start the app again; do not use open -n.'
)
}
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
+247 -44
View File
@@ -1,10 +1,13 @@
import { EventEmitter } from 'node:events'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { RuntimeMetadata } from '../../shared/runtime-bootstrap'
import { formatCliError, reportCliError } from '../cli-error'
import { formatCliError } from '../cli-error'
import { main } from '../index'
import recordedCodexDenial from './fixtures/codex-0.165.0-linux-connect-denied.json'
import { RuntimeClient } from './client'
import { launchOrcaApp } from './launch'
import { getCliStatus } from './status'
import { getCliStatus, getOfflineRuntimeAdmission } from './status'
import { preflightElectronServe } from './serve-electron-preflight'
import { sendRequest } from './transport'
const { connect, tryReadMetadata } = vi.hoisted(() => ({
@@ -12,7 +15,11 @@ const { connect, tryReadMetadata } = vi.hoisted(() => ({
tryReadMetadata: vi.fn()
}))
vi.mock('node:net', () => ({ createConnection: connect }))
vi.mock('./metadata', () => ({ tryReadMetadata, readMetadata: tryReadMetadata }))
vi.mock('./metadata', () => ({
tryReadMetadata,
readMetadata: tryReadMetadata,
getDefaultUserDataPath: () => '/test'
}))
vi.mock('./launch', () => ({ launchOrcaApp: vi.fn() }))
vi.mock('./runtime-remote-pairing', () => ({ resolveRemoteConnection: () => null }))
@@ -41,15 +48,16 @@ beforeEach(() => {
socket = new TestSocket()
connect.mockReturnValue(socket)
tryReadMetadata.mockReturnValue(metadata)
mockKill()
mockKill('ESRCH')
})
afterEach(() => {
vi.unstubAllEnvs()
vi.restoreAllMocks()
vi.clearAllMocks()
vi.useRealTimers()
})
// Node emits 'error' then 'close' for a refused or denied connect.
// Synthetic Node error/events; recorded CLI output supplies only the errno for the replay below.
function failConnect(code: string): void {
socket.emit('error', Object.assign(new Error(`connect ${code} /private-runtime.sock`), { code }))
socket.emit('close')
@@ -85,12 +93,13 @@ describe('runtime access denied', () => {
expect(human).not.toContain('private-runtime')
})
it('names the Codex sandbox only when CODEX_SANDBOX is set', async () => {
it('names the Codex sandbox only when synthetic CODEX_SANDBOX is set', async () => {
vi.stubEnv('CODEX_SANDBOX', 'seatbelt')
const human = formatCliError(await deniedRequest('EPERM'))
expect(human).toContain('The Codex sandbox blocked this command from connecting to Orca')
expect(human).toContain('escalated permissions, outside the Codex sandbox')
expect(human).toContain("Codex's existing escalation when the session permits it")
expect(human).toContain('otherwise run the same command from a terminal outside the sandbox')
expect(human).not.toMatch(RESTART_OR_ABSENT_ADVICE)
})
@@ -98,54 +107,248 @@ describe('runtime access denied', () => {
expect(await deniedRequest('ECONNREFUSED')).toMatchObject({ code: 'runtime_unavailable' })
})
it.each([undefined, 'EPERM'])(
'fails status instead of guessing a state (kill %s)',
async (killCode) => {
mockKill(killCode)
const pending = getCliStatus('/test')
failConnect('EPERM')
it.each(['EPERM', 'EACCES'])(
'preserves %s with a hidden host PID through status and open without launching',
async (code) => {
const status = getCliStatus('/test')
failConnect(code)
await expect(status).rejects.toMatchObject({ code: 'runtime_access_denied' })
await expect(pending).rejects.toMatchObject({ code: 'runtime_access_denied' })
const open = new RuntimeClient('/test', 1000, null, null).openOrca()
failConnect(code)
await expect(open).rejects.toMatchObject({ code: 'runtime_access_denied' })
expect(launchOrcaApp).not.toHaveBeenCalled()
expect(connect).toHaveBeenCalledTimes(2)
expect(process.kill).not.toHaveBeenCalled()
}
)
// Sandbox denial can mask ECONNREFUSED on a dead runtime’s leftover socket.
it('gives not-running advice when the denied endpoint belongs to a dead pid', async () => {
mockKill('ESRCH')
const human = formatCliError(await deniedRequest('EPERM'))
expect(human).toContain("Orca is not running. Run 'orca open' first.")
expect(human).not.toContain('Do not restart')
const pending = getCliStatus('/test')
failConnect('EPERM')
await expect(pending).resolves.toMatchObject({
result: { app: { running: false }, runtime: { state: 'stale_bootstrap' } }
})
})
it('does not launch or poll Orca when the initial status is denied', async () => {
const pending = new RuntimeClient('/test', 1000, null, null).openOrca()
failConnect('EPERM')
await expect(pending).rejects.toMatchObject({ code: 'runtime_access_denied' })
expect(launchOrcaApp).not.toHaveBeenCalled()
expect(connect).toHaveBeenCalledTimes(1)
})
it('reports status --json as an ok:false envelope with the recovery data', async () => {
const pending = getCliStatus('/test')
failConnect('EPERM')
const error = await pending.catch((failure: unknown) => failure)
it('preserves recorded Codex Linux denial and recovery advice through status --json', async () => {
vi.stubEnv('ORCA_PAIRING_CODE', '')
vi.stubEnv('ORCA_ENVIRONMENT', '')
vi.stubEnv('ORCA_REMOTE_PAIRING', '')
mockKill(recordedCodexDenial.provenance.publishedPidProbeSystemCode)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
const priorExitCode = process.exitCode
try {
const pending = main(['status', '--json'], '/test')
await vi.waitFor(() => expect(connect).toHaveBeenCalledOnce())
failConnect(recordedCodexDenial.response.error.data.systemCode)
await pending
reportCliError(error, true, { commandPath: ['status'] })
expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toEqual(recordedCodexDenial.response)
expect(process.exitCode).toBe(recordedCodexDenial.exitCode)
expect(process.kill).not.toHaveBeenCalled()
expect(launchOrcaApp).not.toHaveBeenCalled()
} finally {
process.exitCode = priorExitCode
}
})
})
expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toMatchObject({
// Deterministic socket errno/events are synthetic, not a recorded sandbox transcript.
describe('private offline policy versus public connection truth', () => {
it.each([
['alive-to-absent', [true, false], 'runtime_access_denied', 1],
['absent-to-alive', [false, true], true, 2],
['absent-to-absent', [false, false], false, 2]
] as const)(
'samples denial at the original event and catch points: %s',
async (_name, samples, outcome, count) => {
const observations = [...samples]
vi.mocked(process.kill).mockImplementation(() => {
if (!observations.shift()) {
throw Object.assign(new Error('gone'), { code: 'ESRCH' })
}
return true
})
const pending = getOfflineRuntimeAdmission('/test').catch((error: unknown) => error)
failConnect('EPERM')
expect(process.kill).toHaveBeenCalledTimes(1)
// The transport's original await settles before the admission catch continuation.
await Promise.resolve()
expect(process.kill).toHaveBeenCalledTimes(1)
await Promise.resolve()
expect(process.kill).toHaveBeenCalledTimes(count)
expect(await pending).toMatchObject(
typeof outcome === 'string'
? { code: outcome }
: { reachable: false, legacyPidRunning: outcome, pid: outcome ? metadata.pid : null }
)
expect(tryReadMetadata).toHaveBeenCalledTimes(1)
expect(connect).toHaveBeenCalledTimes(1)
expect(vi.mocked(process.kill).mock.calls.every(([pid]) => pid === metadata.pid)).toBe(true)
}
)
function reply(value: unknown): void {
socket.emit('connect')
const request = JSON.parse(String(socket.write.mock.calls.at(-1)?.[0]))
socket.emit(
'data',
`${JSON.stringify({
id: request.id,
...Object(value),
_meta: { runtimeId: metadata.runtimeId }
})}\n`
)
}
it('never probes for server denial, even with socket-shaped data; independent late error still maps', async () => {
const pending = getOfflineRuntimeAdmission('/test').catch((error: unknown) => error)
reply({
ok: false,
error: {
code: 'runtime_access_denied',
data: { systemCode: 'EPERM', nextSteps: expect.any(Array) }
message: 'server denied',
data: { code: 'EPERM', systemCode: 'EPERM' }
}
})
await expect(pending).resolves.toMatchObject({ code: 'runtime_access_denied' })
expect(process.kill).not.toHaveBeenCalled()
socket.emit('error', Object.assign(new Error('late'), { code: 'EPERM' }))
expect(process.kill).toHaveBeenCalledTimes(1)
await expect(pending).resolves.toMatchObject({ code: 'runtime_access_denied' })
})
it.each([
'EMFILE',
'ECONNREFUSED',
'ECONNRESET',
'UNKNOWN',
'sync',
'close',
'protocol',
'request-id',
'replacement',
'projection',
'server',
'timeout'
])('retains the full outer catch domain and captured PID for %s', async (mode) => {
vi.useFakeTimers()
if (mode === 'sync') {
connect.mockImplementationOnce(() => {
throw new Error('construction failed')
})
}
const pending = getOfflineRuntimeAdmission('/test')
tryReadMetadata.mockReturnValue({ ...metadata, pid: 99999 })
if (mode === 'timeout') {
await vi.advanceTimersByTimeAsync(1000)
} else if (mode === 'close') {
socket.emit('close')
} else if (mode === 'protocol') {
socket.emit('data', 'invalid\n')
} else if (mode === 'request-id') {
socket.emit('data', '{"id":"wrong","ok":true,"result":{}}\n')
} else if (mode === 'replacement') {
socket.emit('connect')
const request = JSON.parse(String(socket.write.mock.calls.at(-1)?.[0]))
socket.emit(
'data',
`${JSON.stringify({
id: request.id,
ok: true,
result: {},
_meta: { runtimeId: 'replacement' }
})}\n`
)
} else if (mode === 'projection') {
reply({ ok: true, result: null })
} else if (mode === 'server') {
reply({ ok: false, error: { code: 'runtime_unavailable', message: 'refused' } })
} else if (mode !== 'sync') {
failConnect(mode)
}
expect(await pending).toEqual({ reachable: false, legacyPidRunning: false, pid: null })
expect(process.kill).toHaveBeenCalledExactlyOnceWith(metadata.pid, 0)
expect(connect).toHaveBeenCalledTimes(1)
expect(tryReadMetadata).toHaveBeenCalledTimes(1)
vi.useRealTimers()
})
it('does not probe incomplete metadata, and projects the entire success before admission', async () => {
tryReadMetadata.mockReturnValueOnce({ ...metadata, authToken: null })
expect(await getOfflineRuntimeAdmission('/test')).toEqual({
reachable: false,
legacyPidRunning: false,
pid: null
})
expect(connect).not.toHaveBeenCalled()
const pending = new RuntimeClient('/test', 1000, null, null).getOfflineRuntimeAdmission()
reply({
ok: true,
result: { runtimeId: metadata.runtimeId, graphStatus: 'ready', authoritativeWindowId: null }
})
expect(await pending).toEqual({ reachable: true, legacyPidRunning: true, pid: metadata.pid })
expect(process.kill).not.toHaveBeenCalled()
expect(connect).toHaveBeenCalledTimes(1)
})
it.each([0, -1, null])(
'retains nonpositive or missing legacy PID %s without a syscall',
async (pid) => {
tryReadMetadata.mockReturnValueOnce({ ...metadata, pid })
const pending = getOfflineRuntimeAdmission('/test')
failConnect('EPERM')
expect(await pending).toEqual({ reachable: false, legacyPidRunning: false, pid: null })
expect(process.kill).not.toHaveBeenCalled()
}
)
it.each(['EPERM', 'EINVAL', 'non-error'])(
'retains unverifiable legacy PID on %s probe exception',
async (code) => {
vi.mocked(process.kill).mockImplementation(() => {
throw code === 'non-error' ? code : Object.assign(new Error(code), { code })
})
const pending = getOfflineRuntimeAdmission('/test')
failConnect('ECONNREFUSED')
expect(await pending).toEqual({ reachable: false, legacyPidRunning: true, pid: metadata.pid })
expect(process.kill).toHaveBeenCalledExactlyOnceWith(metadata.pid, 0)
}
)
it('keeps malformed transport outside the private catch and public projection errors raw', async () => {
tryReadMetadata.mockReturnValueOnce({ ...metadata, transports: [null] })
await expect(getOfflineRuntimeAdmission('/test')).rejects.toBeInstanceOf(TypeError)
expect(process.kill).not.toHaveBeenCalled()
const pending = getCliStatus('/test').catch((error: unknown) => error)
reply({ ok: true, result: null })
const error = await pending
expect(error).toBeInstanceOf(TypeError)
expect(formatCliError(error)).toContain('null')
expect(process.kill).not.toHaveBeenCalled()
})
})
describe('serve default admission on synthetic socket denials', () => {
it.each([
[true, 'EPERM', 1, 'credentials'],
[false, 'EPERM', 2, 'cannot be shown to be free'],
[false, 'ENOENT', 2, null]
] as const)(
'preserves event-time alive=%s and listener errno=%s',
async (alive, listenerCode, connections, message) => {
if (alive) {
vi.mocked(process.kill).mockReturnValue(true)
}
const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true)
const pending = preflightElectronServe({}, '/test', { platform: 'linux' })
failConnect('EPERM')
await Promise.resolve()
await Promise.resolve()
await Promise.resolve()
await Promise.resolve()
if (!alive) {
socket.emit('error', Object.assign(new Error('listener'), { code: listenerCode }))
}
expect(await pending).toBe(message ? 3 : null)
expect(connect).toHaveBeenCalledTimes(connections)
expect(process.kill).toHaveBeenCalledTimes(alive ? 1 : 2)
if (message) {
expect(String(stderr.mock.calls.at(-1)?.[0])).toContain(message)
}
}
)
})
+3 -8
View File
@@ -1,14 +1,9 @@
import { RuntimeClientError } from './types'
import { isProcessRunning } from './runtime-pid-liveness'
// Errno determines denial; CODEX_SANDBOX only selects wording.
export function runtimeAccessDeniedError(
socketError: Error,
pid: number
): RuntimeClientError | null {
export function runtimeAccessDeniedError(socketError: Error): RuntimeClientError | null {
const systemCode = 'code' in socketError ? socketError.code : undefined
// ESRCH identifies a dead runtime even when its socket is access denied.
if ((systemCode !== 'EPERM' && systemCode !== 'EACCES') || !isProcessRunning(pid)) {
if (systemCode !== 'EPERM' && systemCode !== 'EACCES') {
return null
}
const codexSandbox = Boolean(process.env.CODEX_SANDBOX)
@@ -16,7 +11,7 @@ export function runtimeAccessDeniedError(
? `The Codex sandbox blocked this command from connecting to Orca (${systemCode}). Orca may be running normally.`
: `Permission denied connecting to Orca (${systemCode}). Orca may be running normally; this command's sandbox or OS permissions block the connection.`
const retryStep = codexSandbox
? 'Re-run this command with escalated permissions, outside the Codex sandbox.'
? "Retry through Codex's existing escalation when the session permits it; otherwise run the same command from a terminal outside the sandbox."
: 'Re-run this command outside its sandbox, or as a user allowed to reach the Orca runtime.'
return new RuntimeClientError('runtime_access_denied', message, {
systemCode,
+1 -1
View File
@@ -1,3 +1,4 @@
// Only the private offline compatibility policy uses this unqualified legacy PID observation.
export function isProcessRunning(pid: number | null | undefined): boolean {
if (!pid || pid <= 0) {
return false
@@ -6,7 +7,6 @@ export function isProcessRunning(pid: number | null | undefined): boolean {
process.kill(pid, 0)
return true
} catch (error) {
// Only ESRCH proves absence; EPERM indicates a live PID under another uid.
return !(error instanceof Error && 'code' in error && error.code === 'ESRCH')
}
}
@@ -1,12 +1,24 @@
import { mkdtempSync, rmSync } from 'node:fs'
import { mkdtempSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'
import { createServer, type Socket } from 'node:net'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import type { RuntimeMetadata } from '../../shared/runtime-bootstrap'
import { readServeAbortPark, writeServeAbortPark } from '../../shared/serve-abort-park'
import { preflightElectronServe, recordElectronServeStartupAbort } from './serve-electron-preflight'
import * as metadataReader from './metadata'
import { RuntimeClientError } from './types'
vi.mock('./metadata', async (original) => {
const actual = await original<typeof metadataReader>()
return { ...actual, tryReadMetadata: vi.fn(actual.tryReadMetadata) }
})
let actualMetadata: typeof metadataReader
beforeAll(async () => {
actualMetadata = await vi.importActual<typeof metadataReader>('./metadata')
})
const METADATA: RuntimeMetadata = {
runtimeId: 'runtime-1',
pid: 4242,
@@ -16,16 +28,7 @@ const METADATA: RuntimeMetadata = {
}
function status(reachable: boolean, pid: number | null = 4242) {
return {
id: 'local',
ok: true as const,
result: {
app: { running: reachable, pid },
runtime: { state: reachable ? 'ready' : 'stale_bootstrap', reachable, runtimeId: null },
graph: { state: 'not_running' }
},
_meta: { runtimeId: null }
}
return { reachable, legacyPidRunning: reachable, pid }
}
describe('preflightElectronServe', () => {
@@ -54,7 +57,7 @@ describe('preflightElectronServe', () => {
) {
return {
readMetadata: () => (overrides.metadata === undefined ? METADATA : overrides.metadata),
readStatus: vi.fn(async () => {
readAdmission: vi.fn(async () => {
if (overrides.statusError) {
throw overrides.statusError
}
@@ -176,3 +179,79 @@ describe('preflightElectronServe', () => {
})
})
})
// Real bootstrap/transport/listener coverage of the default admission dependency.
describe.skipIf(process.platform === 'win32')('serve private admission snapshots', () => {
let root: string
let server: ReturnType<typeof createServer> | undefined
const sockets = new Set<Socket>()
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'orca-serve-admission-'))
vi.spyOn(process.stderr, 'write').mockImplementation(() => true)
})
afterEach(async () => {
for (const socket of sockets) {
socket.destroy()
}
sockets.clear()
if (server) {
const listening = server
await new Promise<void>((resolve) => listening.close(() => resolve()))
}
server = undefined
vi.restoreAllMocks()
vi.mocked(metadataReader.tryReadMetadata).mockReset()
vi.mocked(metadataReader.tryReadMetadata).mockImplementation(actualMetadata.tryReadMetadata)
rmSync(root, { recursive: true, force: true })
})
function publish(pid = 2147483647, endpoint = join(root, 'runtime.sock')) {
const metadata = { ...METADATA, pid, transports: [{ kind: 'unix', endpoint }] }
writeFileSync(join(root, 'orca-runtime.json'), JSON.stringify(metadata))
return metadata
}
it('keeps the missing-M0 default path synchronous without a second read', () => {
expect(preflightElectronServe({}, root, { platform: 'linux' })).toBeNull()
expect(metadataReader.tryReadMetadata).toHaveBeenCalledTimes(1)
})
it.each([2147483647, process.pid])(
'retains launch permission for absent listener even with PID %s',
async (pid) => {
publish(pid)
await expect(preflightElectronServe({}, root, { platform: 'linux' })).resolves.toBeNull()
expect(metadataReader.tryReadMetadata).toHaveBeenCalledTimes(2)
}
)
it.each(['disappeared', 'replacement', 'incomplete'])(
'probes original M0 listener after M1 %s',
async (change) => {
const first = publish()
const listener = createServer((socket) => {
sockets.add(socket)
socket.once('close', () => sockets.delete(socket))
socket.resume()
})
server = listener
await new Promise<void>((resolve) => listener.listen(first.transports[0].endpoint, resolve))
vi.mocked(metadataReader.tryReadMetadata).mockImplementationOnce((path) => {
const m0 = actualMetadata.tryReadMetadata(path)
if (change === 'disappeared') {
unlinkSync(join(root, 'orca-runtime.json'))
} else {
publish(process.pid, join(root, 'new.sock'))
if (change === 'incomplete') {
writeFileSync(
join(root, 'orca-runtime.json'),
JSON.stringify({ ...first, authToken: null })
)
}
}
return m0
})
await expect(preflightElectronServe({}, root, { platform: 'linux' })).resolves.toBe(3)
expect(String(vi.mocked(process.stderr.write).mock.calls.at(-1)?.[0])).toContain(
change === 'replacement' ? `pid ${process.pid}` : 'pid 2147483647'
)
expect(metadataReader.tryReadMetadata).toHaveBeenCalledTimes(2)
}
)
})
+7 -11
View File
@@ -11,7 +11,7 @@ import {
} from '../../shared/serve-abort-park'
import { tryReadMetadata } from './metadata'
import { probeRuntimeListener, type RuntimeListenerProbe } from './runtime-listener-probe'
import { getCliStatus } from './status'
import { getOfflineRuntimeAdmission, type OfflineRuntimeAdmission } from './status'
import { RuntimeClientError } from './types'
/** Only an absent owner permits a new serve process. */
@@ -20,19 +20,15 @@ export type ServeProfileVerdict =
| { verdict: 'unverifiable'; reason: 'endpoint-held' | 'access-denied'; pid: number | null }
| { verdict: 'absent' }
type ServeProfileStatus = {
result: { app: { pid?: number | null }; runtime: { reachable: boolean } }
}
type PreflightDeps = {
readMetadata: (userDataPath: string) => RuntimeMetadata | null
readStatus: (userDataPath: string) => Promise<ServeProfileStatus>
readAdmission: (userDataPath: string) => Promise<OfflineRuntimeAdmission>
probeListener: (metadata: RuntimeMetadata) => Promise<RuntimeListenerProbe>
}
const DEFAULT_DEPS: PreflightDeps = {
readMetadata: tryReadMetadata,
readStatus: getCliStatus,
readAdmission: getOfflineRuntimeAdmission,
probeListener: probeRuntimeListener
}
@@ -42,17 +38,17 @@ export async function decideServeProfileVerdict(
metadata: RuntimeMetadata,
deps: PreflightDeps = DEFAULT_DEPS
): Promise<ServeProfileVerdict> {
let status: ServeProfileStatus
let status: OfflineRuntimeAdmission
try {
status = await deps.readStatus(userDataPath)
status = await deps.readAdmission(userDataPath)
} catch (error) {
if (error instanceof RuntimeClientError && error.code === 'runtime_access_denied') {
return { verdict: 'unverifiable', reason: 'access-denied', pid: metadata.pid ?? null }
}
throw error
}
const pid = status.result.app.pid ?? metadata.pid ?? null
if (status.result.runtime.reachable) {
const pid = status.pid ?? metadata.pid ?? null
if (status.reachable) {
return { verdict: 'live', evidence: 'rpc', pid }
}
const probe = await deps.probeListener(metadata)
+50 -34
View File
@@ -66,10 +66,10 @@ describe.skipIf(process.platform === 'win32')('CLI runtime status', () => {
getRuntimeMetadataPath(userDataPath),
JSON.stringify({
runtimeId: 'runtime-legacy',
pid: process.pid,
pid: 424242,
transport: { kind: 'unix', endpoint },
authToken: 'token',
startedAt: Date.now()
startedAt: 1
})
)
@@ -85,51 +85,67 @@ describe.skipIf(process.platform === 'win32')('CLI runtime status', () => {
})
})
// EPERM proves a PID exists under another uid; it must not mark a live runtime dead.
describe.skipIf(process.platform === 'win32')('CLI status pid fallback', () => {
async function statusWithUnreachableRuntime(
killError: NodeJS.ErrnoException
): Promise<Awaited<ReturnType<RuntimeClient['getCliStatus']>>> {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-status-probe-'))
// A persisted host PID cannot establish liveness in the caller's namespace.
describe.skipIf(process.platform === 'win32')('CLI status connection authority', () => {
it.each(['EPERM', 'ESRCH'])(
'reports an unavailable endpoint without using a PID probe (%s)',
async (code) => {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-status-probe-'))
writeFileSync(
getRuntimeMetadataPath(userDataPath),
JSON.stringify({
runtimeId: 'runtime-unreachable',
pid: 424242,
transport: { kind: 'unix', endpoint: join(userDataPath, 'absent.sock') },
authToken: 'token',
startedAt: 1
})
)
const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => {
throw Object.assign(new Error(`kill ${code}`), { code })
})
try {
await expect(new RuntimeClient(userDataPath).getCliStatus()).rejects.toMatchObject({
code: 'runtime_unavailable'
})
expect(killSpy).not.toHaveBeenCalled()
} finally {
killSpy.mockRestore()
}
}
)
it('reports a status timeout from a live listener without declaring its hidden PID dead', async () => {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-status-timeout-'))
const endpoint = join(userDataPath, 'runtime.sock')
const server = createServer((socket) => {
sockets.add(socket)
socket.once('close', () => sockets.delete(socket))
socket.resume()
})
servers.add(server)
await new Promise<void>((resolve) => server.listen(endpoint, resolve))
writeFileSync(
getRuntimeMetadataPath(userDataPath),
JSON.stringify({
runtimeId: 'runtime-unreachable',
runtimeId: 'runtime-timeout',
pid: 424242,
// Nothing is listening here, so `status.get` fails and the pid probe decides.
transport: { kind: 'unix', endpoint: join(userDataPath, 'absent.sock') },
transports: [{ kind: 'unix', endpoint }],
authToken: 'token',
startedAt: Date.now()
startedAt: 1
})
)
const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => {
throw killError
throw Object.assign(new Error('hidden PID'), { code: 'ESRCH' })
})
try {
return await new RuntimeClient(userDataPath).getCliStatus()
await expect(new RuntimeClient(userDataPath).getCliStatus()).rejects.toMatchObject({
code: 'runtime_timeout'
})
expect(killSpy).not.toHaveBeenCalled()
} finally {
killSpy.mockRestore()
}
}
it('keeps an unsignalable app running rather than calling the bootstrap stale', async () => {
const status = await statusWithUnreachableRuntime(
Object.assign(new Error('kill EPERM'), { code: 'EPERM' })
)
expect(status.result.app).toMatchObject({ running: true, pid: 424242 })
expect(status.result.runtime.state).toBe('starting')
expect(status.result.graph.state).toBe('starting')
})
it('still reports a stale bootstrap when the host proves the pid is gone', async () => {
const status = await statusWithUnreachableRuntime(
Object.assign(new Error('kill ESRCH'), { code: 'ESRCH' })
)
expect(status.result.app).toMatchObject({ running: false, pid: null })
expect(status.result.runtime.state).toBe('stale_bootstrap')
expect(status.result.graph.state).toBe('not_running')
})
})
+83 -48
View File
@@ -1,14 +1,20 @@
import type { CliStatusResult, RuntimeStatus } from '../../shared/runtime-types'
import { runtimeHostConnectionState } from '../../shared/runtime-host-connection-state'
import { findTransport } from '../../shared/runtime-bootstrap'
import { findTransport, type RuntimeMetadata } from '../../shared/runtime-bootstrap'
import { tryReadMetadata } from './metadata'
import { sendRequest } from './transport'
import {
projectRemoteAppStatus,
resolveDesktopWindowStatus
} from '../../shared/cli-app-status-projection'
import { RuntimeClientError, RuntimeRpcFailureError, type RuntimeRpcSuccess } from './types'
import {
RuntimeClientError,
RuntimeRpcFailureError,
type RuntimeRpcResponse,
type RuntimeRpcSuccess
} from './types'
import { isProcessRunning } from './runtime-pid-liveness'
import { runtimeAccessDeniedError } from './runtime-access-denied'
export { projectRemoteAppStatus, resolveDesktopWindowStatus }
@@ -35,62 +41,91 @@ export async function getCliStatus(
})
}
const response = await sendRequest<RuntimeStatus>(metadata, 'status.get', undefined, 1000)
return projectConnectedStatus(metadata, response)
}
export type OfflineRuntimeAdmission = {
reachable: boolean
legacyPidRunning: boolean
pid: number | null
}
// Shipped offline commands retain their original admission policy, separate from connection truth.
export async function getOfflineRuntimeAdmission(
userDataPath: string
): Promise<OfflineRuntimeAdmission> {
const metadata = tryReadMetadata(userDataPath)
const transport = metadata ? findTransport(metadata, 'unix', 'named-pipe') : null
if (!transport || !metadata?.authToken) {
return { reachable: false, legacyPidRunning: false, pid: null }
}
try {
const response = await sendRequest<RuntimeStatus>(metadata, 'status.get', undefined, 1000)
if (response.ok === false) {
throw new RuntimeRpcFailureError(response)
}
const graphState = response.result.graphStatus
const desktopWindowStatus = resolveDesktopWindowStatus(response.result)
return buildCliStatusResponse({
app: {
running: true,
pid: metadata.pid,
...(desktopWindowStatus ? { desktopWindowStatus } : {})
},
runtime: {
state: graphState === 'ready' ? 'ready' : 'graph_not_ready',
reachable: true,
connectionState: runtimeHostConnectionState({
hasStatusEntry: true,
status: response.result
}),
runtimeId: response.result.runtimeId,
...(response.result.appVersion ? { appVersion: response.result.appVersion } : {}),
...(response.result.remoteUpdateSupport
? { remoteUpdateSupport: response.result.remoteUpdateSupport }
: {}),
...(response.result.capabilities ? { capabilities: response.result.capabilities } : {}),
...(response.result.degradations ? { degradations: response.result.degradations } : {})
},
graph: {
state: graphState
const response = await sendRequest<RuntimeStatus>(
metadata,
'status.get',
undefined,
1000,
undefined,
undefined,
(error) => {
const code = 'code' in error ? error.code : undefined
return (code === 'EPERM' || code === 'EACCES') && isProcessRunning(metadata.pid)
? runtimeAccessDeniedError(error)
: null
}
})
)
const status = projectConnectedStatus(metadata, response)
return {
reachable: status.result.runtime.reachable,
legacyPidRunning: status.result.app.running,
pid: status.result.app.pid ?? null
}
} catch (error) {
// Access denial does not establish runtime liveness.
if (error instanceof RuntimeClientError && error.code === 'runtime_access_denied') {
throw error
}
const running = isProcessRunning(metadata.pid)
return buildCliStatusResponse({
app: {
running,
pid: running ? metadata.pid : null
},
runtime: {
state: running ? 'starting' : 'stale_bootstrap',
reachable: false,
connectionState: 'disconnected',
runtimeId: null
},
graph: {
state: running ? 'starting' : 'not_running'
}
})
return { reachable: false, legacyPidRunning: running, pid: running ? metadata.pid : null }
}
}
function projectConnectedStatus(
metadata: RuntimeMetadata,
response: RuntimeRpcResponse<RuntimeStatus>
): RuntimeRpcSuccess<CliStatusResult> {
if (response.ok === false) {
throw new RuntimeRpcFailureError(response)
}
const graphState = response.result.graphStatus
const desktopWindowStatus = resolveDesktopWindowStatus(response.result)
return buildCliStatusResponse({
app: {
running: true,
pid: metadata.pid,
...(desktopWindowStatus ? { desktopWindowStatus } : {})
},
runtime: {
state: graphState === 'ready' ? 'ready' : 'graph_not_ready',
reachable: true,
connectionState: runtimeHostConnectionState({
hasStatusEntry: true,
status: response.result
}),
runtimeId: response.result.runtimeId,
...(response.result.appVersion ? { appVersion: response.result.appVersion } : {}),
...(response.result.remoteUpdateSupport
? { remoteUpdateSupport: response.result.remoteUpdateSupport }
: {}),
...(response.result.capabilities ? { capabilities: response.result.capabilities } : {}),
...(response.result.degradations ? { degradations: response.result.degradations } : {})
},
graph: {
state: graphState
}
})
}
function buildCliStatusResponse(result: CliStatusResult): RuntimeRpcSuccess<CliStatusResult> {
return {
id: 'local-status',
+41 -2
View File
@@ -7,6 +7,7 @@ import type * as Net from 'node:net'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { RuntimeMetadata } from '../../shared/runtime-bootstrap'
import { MAX_TIMER_DELAY_MS } from '../../shared/timer-delay'
import { formatCliError } from '../cli-error'
import { sendRequest } from './transport'
vi.mock('node:net', async (importOriginal) => {
@@ -70,6 +71,45 @@ describe('runtime transport timeout validation', () => {
// These fixtures create Unix domain sockets rather than Windows named pipes.
describe.skipIf(process.platform === 'win32')('runtime transport', () => {
it('rejects a responding replacement runtime with retry advice in human output', async () => {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-transport-'))
roots.push(userDataPath)
const endpoint = join(userDataPath, 'runtime.sock')
const server = createServer((socket) => {
sockets.add(socket)
socket.once('close', () => sockets.delete(socket))
socket.once('data', (data) => {
const request: { id: string } = JSON.parse(String(data).trim())
socket.end(
`${JSON.stringify({
id: request.id,
ok: true,
result: {},
_meta: { runtimeId: 'runtime-replacement' }
})}\n`
)
})
})
servers.add(server)
await new Promise<void>((resolve) => server.listen(endpoint, resolve))
const metadata: RuntimeMetadata = {
runtimeId: 'runtime-original',
pid: 123,
transports: [{ kind: 'unix', endpoint }],
authToken: 'token',
startedAt: 1
}
const error: unknown = await sendRequest(metadata, 'status.get', undefined, 1000).catch(
(error: unknown) => error
)
expect(error).toMatchObject({ code: 'runtime_unavailable' })
expect(formatCliError(error)).toBe(
'The Orca runtime changed while the request was in flight.\nNext step: Retry the command.'
)
})
it('refreshes the per-call timeout when the runtime sends keepalive frames', async () => {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-runtime-transport-'))
roots.push(userDataPath)
@@ -151,8 +191,7 @@ describe.skipIf(process.platform === 'win32')('runtime transport', () => {
await vi.runAllTimersAsync()
expect(await response).toMatchObject({
code: 'runtime_unavailable',
message:
'The Orca runtime closed the connection before responding. Restart Orca and try again.'
message: 'Orca closed the connection before responding.'
})
expect(vi.getTimerCount()).toBe(0)
})
+9 -5
View File
@@ -17,7 +17,8 @@ export async function sendRequest<TResult>(
timeoutMs: number,
envelope?: RuntimeOrchestrationEnvelope,
// Federation forwards envelopes; the runtime fence must stay local.
expectedRuntimeSource?: ExpectedRuntimeSource
expectedRuntimeSource?: ExpectedRuntimeSource,
classifySocketError: (error: Error) => RuntimeClientError | null = runtimeAccessDeniedError
): Promise<RuntimeRpcResponse<TResult>> {
if (!isSafeTimerDelayMs(timeoutMs)) {
throw new RuntimeClientError(
@@ -79,10 +80,11 @@ export async function sendRequest<TResult>(
ok: false,
// Access denial does not establish that the app is dead.
error:
runtimeAccessDeniedError(error, metadata.pid) ??
classifySocketError(error) ??
new RuntimeClientError(
'runtime_unavailable',
'Could not connect to the running Orca app. Restart Orca and try again.'
'This terminal could not connect to Orca.',
{ nextSteps: ['Check that Orca is open and that this terminal can access it.'] }
)
})
})
@@ -92,7 +94,8 @@ export async function sendRequest<TResult>(
ok: false,
error: new RuntimeClientError(
'runtime_unavailable',
'The Orca runtime closed the connection before responding. Restart Orca and try again.'
'Orca closed the connection before responding.',
{ nextSteps: ['Check that Orca is open and that this terminal can access it.'] }
)
})
})
@@ -171,7 +174,8 @@ export async function sendRequest<TResult>(
ok: false,
error: new RuntimeClientError(
'runtime_unavailable',
'The Orca runtime changed while the request was in flight. Retry the command.'
'The Orca runtime changed while the request was in flight.',
{ nextSteps: ['Retry the command.'] }
)
})
return
@@ -74,6 +74,11 @@ describe('CLI remote WebSocket transport', () => {
})
const client = new RuntimeClient('/tmp/unused', 5_000, pairingUrl)
const response = await client.call<{ runtimeId: string }>('status.get')
expect(await client.getOfflineRuntimeAdmission()).toEqual({
reachable: true,
legacyPidRunning: false,
pid: null
})
expect(response.ok).toBe(true)
expect(response.result.runtimeId).toBe('runtime-ws-1')
@@ -13,7 +13,7 @@ import { resolveAppImageLauncherEndpointPath } from './appimage-stable-launcher'
import { ensureLinuxTerminalOrcaCliShimDir } from './linux-terminal-orca-cli-shim'
const created: string[] = []
const canFenceAppImageRuntime = process.platform === 'linux' && existsSync('/proc/self/stat')
const canFenceAppImageRuntime = process.platform === 'linux'
async function makeFixture(): Promise<{ userDataPath: string; resourcesPath: string }> {
const root = await mkdtemp(join(tmpdir(), 'orca-terminal-cli-shim-'))
@@ -82,8 +82,7 @@ describe('ensureLinuxTerminalOrcaCliShimDir', () => {
const shimPath = join(shimDir!, 'orca')
const content = readFileSync(shimPath, 'utf8')
expect(content).toContain(liveLauncherPath)
expect(content).toContain('runtime_pid=')
expect(content).toContain('/proc/$runtime_pid/stat')
expect(content).not.toContain('/proc/')
expect(existsSync(resolveAppImageLauncherEndpointPath(cacheRootPath, 'live'))).toBe(false)
await expect(
runProcess({ program: shimPath, args: [], timeoutMs: 3_000 })
@@ -158,32 +157,4 @@ describe('ensureLinuxTerminalOrcaCliShimDir', () => {
).resolves.toMatchObject({ code: 1, stdout: '' })
}
)
it.skipIf(!canFenceAppImageRuntime)(
'rejects a shim after its owning AppImage process generation changes',
async () => {
const { userDataPath, resourcesPath } = await makeFixture()
const appImagePath = join(userDataPath, 'Orca.AppImage')
await mkdir(userDataPath, { recursive: true })
await writeFile(appImagePath, '#!/usr/bin/env bash\n', { mode: 0o755 })
const liveLauncher = join(resourcesPath, 'bin', 'orca-ide')
writeFileSync(liveLauncher, '#!/usr/bin/env bash\nprintf original', { mode: 0o755 })
const shimDir = ensureLinuxTerminalOrcaCliShimDir({
userDataPath,
resourcesPath,
appImagePath,
appImageCacheRootPath: join(userDataPath, 'cache')
})
const shimPath = join(shimDir!, 'orca')
const staleContent = readFileSync(shimPath, 'utf8').replace(
/runtime_start_time='[^']*'/,
"runtime_start_time='stale-process'"
)
writeFileSync(shimPath, staleContent, { mode: 0o755 })
await expect(
runProcess({ program: shimPath, args: [], timeoutMs: 3_000 })
).resolves.toMatchObject({ code: 1, stdout: '' })
}
)
})
+1 -33
View File
@@ -100,8 +100,6 @@ function ensureAppImageShim(
}
type AppImageRuntimeFence = {
pid: number
startTime: string
runtimeRoot: string
runtimeIdentity: string
launcherIdentity: string
@@ -116,11 +114,8 @@ function captureAppImageRuntimeFence(launcherPath: string): AppImageRuntimeFence
const runtimeRoot = dirname(dirname(dirname(resolvedLauncherPath)))
const runtimeIdentity = readFileIdentity(runtimeRoot)
const launcherIdentity = readFileIdentity(resolvedLauncherPath)
const startTime = readLinuxProcessStartTime(process.pid)
return runtimeIdentity && launcherIdentity && startTime
return runtimeIdentity && launcherIdentity
? {
pid: process.pid,
startTime,
runtimeRoot,
runtimeIdentity,
launcherIdentity,
@@ -148,36 +143,15 @@ function formatFileIdentity(stats: Stats): string {
].join(':')
}
function readLinuxProcessStartTime(pid: number): string | null {
try {
const content = readFileSync(join('/proc', String(pid), 'stat'), 'utf8')
const commandEnd = content.lastIndexOf(') ')
if (commandEnd === -1) {
return null
}
const fields = content
.slice(commandEnd + 2)
.trim()
.split(/\s+/)
return fields[19] ?? null
} catch {
return null
}
}
function buildAppImageLiveLauncherScript(
launcherPath: string,
fence: AppImageRuntimeFence
): string {
const runtimePid = quotePosixShellDoubleQuotedApostrophe(String(fence.pid))
const runtimeStartTime = quotePosixShellDoubleQuotedApostrophe(fence.startTime)
const runtimeRoot = quotePosixShellDoubleQuotedApostrophe(fence.runtimeRoot)
const expectedRuntimeIdentity = quotePosixShellDoubleQuotedApostrophe(fence.runtimeIdentity)
const expectedLauncherIdentity = quotePosixShellDoubleQuotedApostrophe(fence.launcherIdentity)
const quotedLauncherPath = quotePosixShellDoubleQuotedApostrophe(launcherPath)
return `#!/usr/bin/env bash
runtime_pid=${runtimePid}
runtime_start_time=${runtimeStartTime}
runtime_root=${runtimeRoot}
launcher=${quotedLauncherPath}
expected_runtime_identity=${expectedRuntimeIdentity}
@@ -186,12 +160,6 @@ fail() {
printf 'Orca CLI is unavailable; reopen Orca or register the CLI again.\\n' >&2
exit 1
}
proc_stat_path="/proc/$runtime_pid/stat"
[[ -r "$proc_stat_path" ]] || fail
proc_stat="$(<"$proc_stat_path")" || fail
proc_fields=()
read -r -a proc_fields <<< "\${proc_stat##*) }" || fail
[[ "\${proc_fields[19]:-}" == "$runtime_start_time" ]] || fail
runtime_identity="$(stat -Lc '%d:%i:%s:%Y:%Z' -- "$runtime_root" 2>/dev/null)" || fail
[[ "$runtime_identity" == "$expected_runtime_identity" ]] || fail
launcher_identity="$(stat -Lc '%d:%i:%s:%Y:%Z' -- "$launcher" 2>/dev/null)" || fail
+18
View File
@@ -45,6 +45,24 @@ describe('prependOrcaCliDirToChildPath', () => {
expect(shim.ensureLinuxTerminalOrcaCliShimDir).not.toHaveBeenCalled()
}
)
it('exports the owning packaged Linux launcher and profile together with PATH', () => {
const env = {
PATH: `/usr/bin:${SHIM_DIR}::/bin`,
ORCA_CLI_COMMAND: '/other/orca',
ORCA_USER_DATA_PATH: '/other/profile'
}
const launcher = prependOrcaCliDirToChildPath(env, {
isPackaged: true,
userDataPath: USER_DATA,
platform: 'linux'
})
expect(launcher).toBe(join(SHIM_DIR, 'orca'))
expect(env.ORCA_CLI_COMMAND).toBe(launcher)
expect(env.ORCA_USER_DATA_PATH).toBe(USER_DATA)
expect(env.PATH).toBe(`${SHIM_DIR}:/usr/bin:/bin`)
})
it('puts a Windows execution host CLI first on Path without a POSIX restore dir', () => {
const binDir = join(USER_DATA, 'cli', 'bin')
const launcher = join(binDir, 'orca.exe')
+4 -1
View File
@@ -59,7 +59,10 @@ export function prependOrcaCliDirToChildPath(
.split(pathDelimiter)
.filter((entry) => entry.length > 0 && entry !== shimDir)
env.PATH = [shimDir, ...inheritedEntries].join(pathDelimiter)
return join(shimDir, 'orca')
const launcher = join(shimDir, 'orca')
env.ORCA_CLI_COMMAND = launcher
env.ORCA_USER_DATA_PATH = opts.userDataPath
return launcher
}
} else if (opts.resourcesPath && (platform === 'darwin' || platform === 'win32')) {
const bundledCliBin = join(opts.resourcesPath, 'bin')
@@ -448,7 +448,8 @@ describe('registerPtyHandlers', () => {
// Orca terminals must resolve this CLI before the GNOME screen reader (#7904).
expect(entries.indexOf(shimDir)).toBeGreaterThanOrEqual(0)
expect(entries.indexOf(shimDir)).toBeLessThan(entries.indexOf('/usr/bin'))
expect(env.ORCA_CLI_COMMAND).toBeUndefined()
expect(env.ORCA_CLI_COMMAND).toBe(join(shimDir, 'orca'))
expect(env.ORCA_USER_DATA_PATH).toBe('/tmp/orca-user-data')
} finally {
Object.defineProperty(process, 'platform', {
configurable: true,