diff --git a/.github/workflows/computer-e2e.yml b/.github/workflows/computer-e2e.yml
index 3023d414abe..784172d699e 100644
--- a/.github/workflows/computer-e2e.yml
+++ b/.github/workflows/computer-e2e.yml
@@ -117,6 +117,7 @@ jobs:
- run: pnpm verify:computer-native
- run: pnpm build:cli
- run: pnpm build:electron-vite
+ - run: pnpm verify:cli-runtime
# Why: boot the BUILT daemon-entry under plain Node the way production
# forks it. v1.4.129-rc.1 shipped a daemon that exited at module load
# (leaked electron require) while every other check passed; this fails
@@ -158,6 +159,7 @@ jobs:
- run: pnpm verify:computer-native
- run: pnpm build:cli
- run: pnpm build:electron-vite
+ - run: pnpm verify:cli-runtime
- run: pnpm test:e2e:computer --reporter=verbose tests/e2e/computer-mac.e2e.ts tests/e2e/computer-mac-safari.e2e.ts
linux:
@@ -185,6 +187,7 @@ jobs:
- run: pnpm verify:computer-native
- run: pnpm build:cli
- run: pnpm build:electron-vite
+ - run: pnpm verify:cli-runtime
- run: xvfb-run --auto-servernum dbus-run-session -- pnpm test:e2e:computer --reporter=verbose tests/e2e/computer-linux.e2e.ts
windows:
@@ -204,4 +207,5 @@ jobs:
- run: pnpm verify:computer-native
- run: pnpm build:cli
- run: pnpm build:electron-vite
+ - run: pnpm verify:cli-runtime
- run: pnpm test:e2e:computer --reporter=verbose tests/e2e/computer-windows.e2e.ts tests/e2e/computer-windows-store.e2e.ts
diff --git a/.github/workflows/mobile-ios-release.yml b/.github/workflows/mobile-ios-release.yml
index b38a79b9267..f759bd3efee 100644
--- a/.github/workflows/mobile-ios-release.yml
+++ b/.github/workflows/mobile-ios-release.yml
@@ -10,7 +10,7 @@ on:
workflow_dispatch:
inputs:
bump_patch_version:
- description: 'Bump the iOS marketing version patch number before release. Tick this after a version has shipped to the App Store (the release fails fast if the current version''s train is already closed).'
+ description: 'Use the first open iOS patch version after the checked-in version, skipping versions already closed on the App Store.'
required: false
default: false
type: boolean
diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml
index 2998d1dcce7..e9683904be2 100644
--- a/.github/workflows/mobile.yml
+++ b/.github/workflows/mobile.yml
@@ -30,6 +30,11 @@ jobs:
with:
node-version-file: package.json
+ - name: Setup Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: '3.3'
+
- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
@@ -56,6 +61,9 @@ jobs:
- name: Test
run: pnpm test
+ - name: Test iOS release version resolution
+ run: ruby fastlane/ios_release_version_test.rb
+
- name: Lint
run: pnpm lint
diff --git a/.github/workflows/release-cut.yml b/.github/workflows/release-cut.yml
index 2c48d466305..0142518c4cb 100644
--- a/.github/workflows/release-cut.yml
+++ b/.github/workflows/release-cut.yml
@@ -1330,7 +1330,8 @@ jobs:
}
Copy-Item -Path $signedInstaller.FullName -Destination 'dist/orca-windows-setup.exe' -Force
- & 'node_modules/app-builder-bin/win/x64/app-builder.exe' blockmap --input 'dist/orca-windows-setup.exe' --output 'dist/orca-windows-setup.exe.blockmap'
+ node config/scripts/generate-windows-blockmap.mjs 'dist/orca-windows-setup.exe' 'dist/orca-windows-setup.exe.blockmap'
+ if ($LASTEXITCODE -ne 0) { throw "blockmap generation failed with exit code $LASTEXITCODE" }
$installer = Get-Item 'dist/orca-windows-setup.exe'
$blockmap = Get-Item 'dist/orca-windows-setup.exe.blockmap'
diff --git a/.github/workflows/windows-signing-rehearsal.yml b/.github/workflows/windows-signing-rehearsal.yml
index 24b4307a83b..4ca080d803d 100644
--- a/.github/workflows/windows-signing-rehearsal.yml
+++ b/.github/workflows/windows-signing-rehearsal.yml
@@ -271,7 +271,7 @@ jobs:
throw 'Signed Windows installer was not returned by SignPath.'
}
Copy-Item -Path $signedInstaller.FullName -Destination 'dist/orca-windows-setup.exe' -Force
- & 'node_modules/app-builder-bin/win/x64/app-builder.exe' blockmap --input 'dist/orca-windows-setup.exe' --output 'dist/orca-windows-setup.exe.blockmap'
+ node config/scripts/generate-windows-blockmap.mjs 'dist/orca-windows-setup.exe' 'dist/orca-windows-setup.exe.blockmap'
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$installer = Get-Item 'dist/orca-windows-setup.exe'
diff --git a/.gitignore b/.gitignore
index 1b3f81e72c0..00b6dd02894 100644
--- a/.gitignore
+++ b/.gitignore
@@ -92,6 +92,7 @@ docs/**
!docs/reference/
!docs/reference/git-compatibility.md
!docs/reference/headless-linux-server.md
+!docs/reference/linux-glibc-compatibility.md
# Stably CLI (only docs/ are tracked)
.stably/*
diff --git a/AGENTS.md b/AGENTS.md
index eb43f911924..d4edf8dbaf1 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -27,6 +27,7 @@ Orca targets macOS, Linux, and Windows. Keep all platform-dependent behavior beh
- **Keyboard shortcuts**: Never hardcode `e.metaKey`. Use a platform check (`navigator.userAgent.includes('Mac')`) to pick `metaKey` on Mac and `ctrlKey` on Linux/Windows. Electron menu accelerators should use `CmdOrCtrl`.
- **Shortcut labels in UI**: Display `⌘` / `⇧` on Mac and `Ctrl+` / `Shift+` on other platforms.
- **File paths**: Use `path.join` or Electron/Node path utilities — never assume `/` or `\`.
+- **Linux native modules**: keep the glibc floor at Ubuntu 20.04 / glibc 2.31. A module compiled from source on a newer runner can reference symbol versions absent on the floor and crash the app on startup. See [`docs/reference/linux-glibc-compatibility.md`](./docs/reference/linux-glibc-compatibility.md); packaging fails if a bundled native binary needs newer glibc.
## SSH Use Case
diff --git a/README.md b/README.md
index 1a434350bd0..aa4ee905281 100644
--- a/README.md
+++ b/README.md
@@ -238,7 +238,7 @@ Pair with your desktop app to monitor and steer your agents from your phone.
- **Discord:** Join the community on **[Discord](https://discord.gg/fzjDKHxv8Q)**.
- **Twitter / X:** Follow **[@orca_build](https://x.com/orca_build)** for updates and announcements.
-- **WeChat:** Groups 1 and 2 are both full — now you can join the third one.
+- **WeChat:** All other groups are full, now we're on group 5.
diff --git a/config/electron-builder.config.cjs b/config/electron-builder.config.cjs
index 50cfe7691e8..6d9db80c558 100644
--- a/config/electron-builder.config.cjs
+++ b/config/electron-builder.config.cjs
@@ -11,6 +11,7 @@ const {
prunePackagedRuntimeNodeModules,
verifyPackagedMainRuntimeDeps
} = require('./packaged-runtime-node-modules.cjs')
+const { verifyLinuxGlibcFloor } = require('./scripts/verify-linux-glibc-floor.cjs')
const isMacRelease = process.env.ORCA_MAC_RELEASE === '1'
const isLinuxArm64Release = process.env.ORCA_LINUX_ARM64_RELEASE === '1'
@@ -133,6 +134,12 @@ module.exports = {
'node_modules/sherpa-onnx*/**'
],
afterPack: async (context) => {
+ // Why: a Linux runner-image glibc bump silently shipped a node-pty pty.node
+ // requiring GLIBC_2.34, crashing the app on startup on Ubuntu 20.04 (#9902).
+ // Fail packaging if any bundled native binary exceeds the supported floor.
+ if (context.electronPlatformName === 'linux') {
+ verifyLinuxGlibcFloor(context.appOutDir)
+ }
const resourcesDir =
context.electronPlatformName === 'darwin'
? join(
diff --git a/config/patches/node-pty@1.1.0.patch b/config/patches/node-pty@1.1.0.patch
index e100def9346..0b0038ed630 100644
--- a/config/patches/node-pty@1.1.0.patch
+++ b/config/patches/node-pty@1.1.0.patch
@@ -1,5 +1,5 @@
diff --git a/binding.gyp b/binding.gyp
-index 5f63978b07ab50aaf7523219a2170ec737a6b5db..b3309a07ef99dea7967d7bdd04b9fc3500acacae 100644
+index 5f63978b07ab50aaf7523219a2170ec737a6b5db..bbd9e06136e8922f40b5779e35d4fc835f1479ab 100644
--- a/binding.gyp
+++ b/binding.gyp
@@ -5,9 +5,6 @@
@@ -12,6 +12,23 @@ index 5f63978b07ab50aaf7523219a2170ec737a6b5db..b3309a07ef99dea7967d7bdd04b9fc35
'msvs_settings': {
'VCCLCompilerTool': {
'AdditionalOptions': [
+@@ -88,6 +85,16 @@
+ 'libraries!': [
+ '-lutil'
+ ]
++ }],
++ # Orca: pair with the .symver pins in pty.cc. Force the real
++ # libutil.so.1/libpthread.so.0 into DT_NEEDED (gcc's default
++ # --as-needed drops them because the pinned symbols resolve from
++ # libc's compat aliases at build time) so openpty/forkpty/
++ # pthread_sigmask still resolve on Ubuntu 20.04 (glibc 2.31).
++ ['OS=="linux"', {
++ 'ldflags': [
++ '-Wl,--no-as-needed,-l:libutil.so.1,-l:libpthread.so.0,--as-needed'
++ ]
+ }]
+ ]
+ }
diff --git a/deps/winpty/src/winpty.gyp b/deps/winpty/src/winpty.gyp
index 1ac5758bedd8cf54f32280dea4e4aeb5afdee30d..e619813759c6f14694838bdfbd0ea5f8360130ef 100644
--- a/deps/winpty/src/winpty.gyp
@@ -54,6 +71,27 @@ index 1ac5758bedd8cf54f32280dea4e4aeb5afdee30d..e619813759c6f14694838bdfbd0ea5f8
'msvs_settings': {
# Specify this setting here to override a setting from somewhere
# else, such as node's common.gypi.
+diff --git a/lib/conpty_console_list_agent.js b/lib/conpty_console_list_agent.js
+index 8c4fca9022a6d6f015bca87f61625cde2278f428..0a01730616488119aa21ef441cf3c441e02a974c 100644
+--- a/lib/conpty_console_list_agent.js
++++ b/lib/conpty_console_list_agent.js
+@@ -10,7 +10,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
+ var utils_1 = require("./utils");
+ var getConsoleProcessList = utils_1.loadNativeModule('conpty_console_list').module.getConsoleProcessList;
+ var shellPid = parseInt(process.argv[2], 10);
+-var consoleProcessList = getConsoleProcessList(shellPid);
++var consoleProcessList;
++try {
++ consoleProcessList = getConsoleProcessList(shellPid);
++}
++catch (_a) {
++ // Why: AttachConsole can fail after the shell exits; parent already has this fallback.
++ consoleProcessList = [shellPid];
++}
+ process.send({ consoleProcessList: consoleProcessList });
+ process.exit(0);
+ //# sourceMappingURL=conpty_console_list_agent.js.map
+\ No newline at end of file
diff --git a/lib/unixTerminal.js b/lib/unixTerminal.js
index 1ec12f796a822c78fba9ad7f6448c3987e325c23..cec8b67aef02f8199e5606a0d257088bf1865877 100644
--- a/lib/unixTerminal.js
@@ -73,31 +111,12 @@ index 1ec12f796a822c78fba9ad7f6448c3987e325c23..cec8b67aef02f8199e5606a0d257088b
var DEFAULT_FILE = 'sh';
var DEFAULT_NAME = 'xterm';
var DESTROY_SOCKET_TIMEOUT_MS = 200;
-diff --git a/lib/conpty_console_list_agent.js b/lib/conpty_console_list_agent.js
-index ccc111c9e03a4a661ccfd5d8e8f0ee699571b5dd..f92c6bef7d46dc35c941c87ef186aa46d8ed9c44 100644
---- a/lib/conpty_console_list_agent.js
-+++ b/lib/conpty_console_list_agent.js
-@@ -9,7 +9,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
- var utils_1 = require("./utils");
- var getConsoleProcessList = utils_1.loadNativeModule('conpty_console_list').module.getConsoleProcessList;
- var shellPid = parseInt(process.argv[2], 10);
--var consoleProcessList = getConsoleProcessList(shellPid);
-+var consoleProcessList;
-+try {
-+ consoleProcessList = getConsoleProcessList(shellPid);
-+}
-+catch (_a) {
-+ // Why: AttachConsole can fail after the shell exits; parent already has this fallback.
-+ consoleProcessList = [shellPid];
-+}
- process.send({ consoleProcessList: consoleProcessList });
- process.exit(0);
- //# sourceMappingURL=conpty_console_list_agent.js.map
diff --git a/src/conpty_console_list_agent.ts b/src/conpty_console_list_agent.ts
-index f6a653893e0b9b548c514db29d75599538ee1acb..1d5400489f200ef0161ca687e672e1cc02d29c95 100644
+index 181ccabbbe9c4948a9725fb1db907a68e9de01fc..67f31facf85562b67adbfbd04ce28ddd8eeb4a79 100644
--- a/src/conpty_console_list_agent.ts
+++ b/src/conpty_console_list_agent.ts
-@@ -11,5 +11,11 @@ import { loadNativeModule } from './utils';
+@@ -10,6 +10,12 @@ import { loadNativeModule } from './utils';
+
const getConsoleProcessList = loadNativeModule('conpty_console_list').module.getConsoleProcessList;
const shellPid = parseInt(process.argv[2], 10);
-const consoleProcessList = getConsoleProcessList(shellPid);
@@ -111,7 +130,7 @@ index f6a653893e0b9b548c514db29d75599538ee1acb..1d5400489f200ef0161ca687e672e1cc
process.send!({ consoleProcessList });
process.exit(0);
diff --git a/src/unix/pty.cc b/src/unix/pty.cc
-index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..61f39f0cbb91faa2c515f35d2ca850564e6368d9 100644
+index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..383df0c9c48355547c65e6c9bbba593d15c4dd44 100644
--- a/src/unix/pty.cc
+++ b/src/unix/pty.cc
@@ -23,7 +23,9 @@
@@ -124,7 +143,33 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..61f39f0cbb91faa2c515f35d2ca85056
#include
#include
-@@ -237,13 +239,23 @@ pty_getproc(int, char *);
+@@ -47,6 +49,25 @@
+ #include
+ #endif
+
++/* Orca: glibc 2.32-2.34 relocated pthread_sigmask/openpty/forkpty into libc
++ * under new symbol versions, so building on a newer glibc produces references
++ * (GLIBC_2.32/2.34) absent on Ubuntu 20.04 (glibc 2.31) and the app fails to
++ * launch. Pin these to the pre-merge version glibc still ships as a compat
++ * alias; the binding.gyp ldflags force libutil/libpthread into DT_NEEDED so
++ * those aliases are actually loaded on the target. */
++#if defined(__linux__)
++# if defined(__x86_64__)
++# define ORCA_GLIBC_COMPAT_VERSION "GLIBC_2.2.5"
++# elif defined(__aarch64__)
++# define ORCA_GLIBC_COMPAT_VERSION "GLIBC_2.17"
++# endif
++# ifdef ORCA_GLIBC_COMPAT_VERSION
++__asm__(".symver openpty,openpty@" ORCA_GLIBC_COMPAT_VERSION);
++__asm__(".symver forkpty,forkpty@" ORCA_GLIBC_COMPAT_VERSION);
++__asm__(".symver pthread_sigmask,pthread_sigmask@" ORCA_GLIBC_COMPAT_VERSION);
++# endif
++#endif
++
+ /* Some platforms name VWERASE and VDISCARD differently */
+ #if !defined(VWERASE) && defined(VWERSE)
+ #define VWERASE VWERSE
+@@ -237,13 +258,23 @@ pty_getproc(int, char *);
#endif
#if defined(__APPLE__) || defined(__OpenBSD__)
@@ -149,7 +194,7 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..61f39f0cbb91faa2c515f35d2ca85056
#endif
struct DelBuf {
-@@ -367,10 +379,11 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) {
+@@ -367,10 +398,11 @@ Napi::Value PtyFork(const Napi::CallbackInfo& info) {
argv[i + 3] = strdup(arg.c_str());
}
@@ -165,7 +210,7 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..61f39f0cbb91faa2c515f35d2ca85056
}
if (pty_nonblock(master) == -1) {
throw Napi::Error::New(napiEnv, "Could not set master fd to nonblocking.");
-@@ -684,15 +697,73 @@ pty_getproc(int fd, char *tty) {
+@@ -684,15 +716,73 @@ pty_getproc(int fd, char *tty) {
#endif
#if defined(__APPLE__)
@@ -241,25 +286,25 @@ index 7b4b9e1f990fbf95b51528bb56dc9717f5b87532..61f39f0cbb91faa2c515f35d2ca85056
for (; count < 3; count++) {
low_fds[count] = posix_openpt(O_RDWR);
-@@ -706,80 +777,118 @@ pty_posix_spawn(char** argv, char** env,
+@@ -706,80 +796,118 @@ pty_posix_spawn(char** argv, char** env,
POSIX_SPAWN_SETSID;
*master = posix_openpt(O_RDWR);
if (*master == -1) {
- return;
+ pty_set_spawn_error(err, "posix_openpt", errno);
-+ goto done;
-+ }
-+
-+ res = grantpt(*master);
-+ if (res == -1) {
-+ pty_set_spawn_error(err, "grantpt", errno);
+ goto done;
}
- int res = grantpt(*master) || unlockpt(*master);
-+ res = unlockpt(*master);
++ res = grantpt(*master);
if (res == -1) {
- return;
++ pty_set_spawn_error(err, "grantpt", errno);
++ goto done;
++ }
++
++ res = unlockpt(*master);
++ if (res == -1) {
+ pty_set_spawn_error(err, "unlockpt", errno);
+ goto done;
}
diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc
index 8c030818cbc..77b41ee22ff 100644
--- a/config/reliability-gates.jsonc
+++ b/config/reliability-gates.jsonc
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
- "updatedAt": "2026-07-20",
+ "updatedAt": "2026-07-22",
"policy": {
"maturityLevels": [
"experimental",
@@ -1387,7 +1387,10 @@
"surfaces": [
"remote agent launch and explicit resume",
"multi-client remote runtime sessions",
+ "headed desktop remote-server pairing",
+ "headless remote-server parity",
"daemon and relay reconnect",
+ "remote completion classification across disconnect and reconnect",
"terminal exit retirement and restart restore",
"mixed-version fallback"
],
@@ -1410,21 +1413,39 @@
"local",
"daemon",
"ssh",
+ "wsl",
"remote-runtime"
],
- "coverageNotes": "Deterministic macOS tests cover controller claims, daemon and SSH/relay operation replay, mixed-version selection, runtime ownership, exact provisional handoff, and durable terminal retirement. The real repro runs two independent clients against one headless remote Orca runtime over the encrypted pairing path and a real daemon-backed PTY. SSH coverage is contract/fault-injection coverage; WSL and live SSH hosts remain gaps.",
+ "coverageNotes": "Deterministic macOS tests cover controller claims, daemon and SSH/relay operation replay, mixed-version selection, runtime ownership, exact provisional handoff, durable terminal retirement, two independent viewer mirrors, guarded adoption of legacy live PTYs, and completion classification when either the outer remote transport or authoritative host/provider process inspection becomes unreachable. The adoption harness models v1.4.150 agent/setup/shell tabs, current-generation restart and reconnect, exact handle/incarnation/worktree/host checks, topology CAS, competing clients, split-pane/group restoration, WSL ownership, and SSH owner rejection. The secondary parity repro runs independent clients against one headless remote Orca runtime over encrypted pairing and a real daemon-backed PTY. The primary headed topology was exercised with an isolated macOS Orca desktop server, a separate paired Edge client, a real Codex process, injected WebSocket loss, reconnect, and explicit stop; host inspection and visible client state agreed throughout. SSH coverage is provider/relay contract and fault-injection coverage only; it does not substitute for paired-server coverage. Live Windows, Linux, WSL, and SSH hosts remain gaps.",
"motivatingLinks": [
"https://github.com/stablyai/orca/issues/8878",
+ "https://github.com/stablyai/orca/issues/9151",
"https://github.com/stablyai/orca/issues/9352",
"https://github.com/stablyai/orca/pull/9687"
],
- "invariant": "For every claim-capable execution route, one provider-session identity has at most one live PTY owner and one canonical host surface across concurrent clients, retries, reconnects, and stale publications. A physical exit retires that exact incarnation durably so stale client state and host restart cannot recreate it. Mixed-version routes select the unchanged legacy request before any authority side effect or execution-owner-local filesystem access.",
- "oracle": "Race independent clients and repeated operation IDs, then assert one physical spawn and one canonical PTY/surface; inject exit-before-reply, provider disconnect, conflicting claim scope, and old daemon/relay capabilities; assert safe adoption or explicit failure without a second spawn. After exact exit, assert terminal and tab listings omit the surface, a stale publication cannot restore it, restart cannot resurrect it, and an exact provisional handoff is consumed even when exit wins before the next snapshot.",
+ "invariant": "For every claim-capable execution route, one provider-session identity has at most one live PTY owner and one canonical host surface across concurrent clients, retries, reconnects, and stale publications. A live orphan may be adopted only when the controller proves its exact handle and incarnation, its worktree and host owner match, no competing visual owner exists, and a host topology CAS wins. A viewer may classify completion only from successful host/provider inspection or explicit lifecycle evidence; transport, handle, or provider unavailability remains unknown and breaks any consecutive-idle proof. A physical exit retires that exact incarnation durably so stale client state and host restart cannot recreate it. Mixed-version routes select the unchanged legacy request before any authority side effect or execution-owner-local filesystem access.",
+ "oracle": "Race independent clients and repeated operation IDs, then assert one physical spawn and one canonical PTY/surface; inject exit-before-reply, provider disconnect, conflicting claim scope, old daemon/relay capabilities, reused handles, stale incarnations, owner mismatch, and topology revision conflict; assert safe adoption or explicit failure without a second spawn or wrong-process attachment. Restore legacy split panes and groups beside a newer host-owned tab, preserving output, input, resize, titles, tab/leaf identity, active group, and multi-client convergence. For completion, drive a known running agent through outer transport loss, authoritative provider rejection, reconnect, explicit stop, real exit status, and successful hook completion; assert unavailable evidence never dispatches completion and two fresh authoritative idle samples are required after the gap. After exact exit, assert terminal and tab listings omit the surface, a stale publication cannot restore it, restart cannot resurrect it, and an exact provisional handoff is consumed even when exit wins before the next snapshot.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/shared/claimed-agent-pty-owner.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/providers/ssh-pty-provider-agent-session-create-operation.test.ts src/main/runtime/orca-runtime-agent-session-operation.test.ts src/main/runtime/remote-agent-session-host-authority.integration.test.ts src/main/runtime/orca-runtime-terminal-retirement.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.test.ts src/renderer/src/runtime/remote-runtime-session-tabs-inflight.test.ts src/renderer/src/runtime/web-runtime-session.test.ts src/renderer/src/runtime/web-session-tabs-sync.test.ts",
- "pnpm test:repro:remote-agent-session"
+ "pnpm exec vitest run --config config/vitest.config.ts tests/e2e/remote-terminal-tab-retirement.unit.test.ts",
+ "pnpm test:repro:remote-agent-session",
+ "Manual headed paired-server journey: isolated Orca desktop host + separate paired web client + real Codex process + 20-second WebSocket fault + reconnect + explicit stop",
+ "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/terminal-orphan-owner.test.ts src/main/runtime/terminal-orphan-topology.test.ts src/renderer/src/runtime/web-session-terminal-orphan-recovery.test.ts src/renderer/src/runtime/web-session-terminal-orphan-mixed-version.test.ts src/renderer/src/runtime/web-session-tabs-sync.test.ts --maxWorkers=1",
+ "pnpm exec vitest run --config config/vitest.config.ts src/main/providers/pty-process-inspection.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts src/relay/pty-handler.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/runtime/orca-runtime.test.ts tests/e2e/remote-agent-completion-authority.unit.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator.test.ts",
+ "pnpm exec vitest run --config config/vitest.config.ts tests/e2e/remote-agent-completion-authority.unit.test.ts src/main/providers/pty-process-inspection.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts src/relay/pty-handler.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator.test.ts src/renderer/src/components/terminal-pane/pty-connection.test.ts src/renderer/src/lib/codex-session-restart.test.ts"
],
"testFiles": [
+ "src/main/providers/pty-process-inspection.test.ts",
+ "src/main/daemon/terminal-host.test.ts",
+ "src/main/daemon/daemon-pty-router.test.ts",
+ "src/main/daemon/degraded-daemon-pty-provider.test.ts",
+ "src/relay/pty-handler.test.ts",
+ "src/main/runtime/orca-runtime.test.ts",
+ "tests/e2e/remote-agent-completion-authority.unit.test.ts",
+ "src/renderer/src/runtime/runtime-terminal-inspection.test.ts",
+ "src/renderer/src/components/terminal-pane/agent-completion-coordinator.test.ts",
+ "src/renderer/src/components/terminal-pane/pty-connection.test.ts",
+ "src/renderer/src/lib/codex-session-restart.test.ts",
"src/shared/claimed-agent-pty-owner.test.ts",
"src/main/daemon/daemon-pty-adapter.test.ts",
"src/main/providers/ssh-pty-provider-agent-session-create-operation.test.ts",
@@ -1434,9 +1455,71 @@
"src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.test.ts",
"src/renderer/src/runtime/remote-runtime-session-tabs-inflight.test.ts",
"src/renderer/src/runtime/web-runtime-session.test.ts",
- "src/renderer/src/runtime/web-session-tabs-sync.test.ts"
+ "src/renderer/src/runtime/web-session-tabs-sync.test.ts",
+ "tests/e2e/remote-terminal-tab-retirement.unit.test.ts",
+ "src/main/runtime/orca-runtime.test.ts",
+ "src/main/runtime/terminal-orphan-owner.test.ts",
+ "src/main/runtime/terminal-orphan-topology.test.ts",
+ "src/renderer/src/runtime/web-session-terminal-orphan-recovery.test.ts",
+ "src/renderer/src/runtime/web-session-terminal-orphan-mixed-version.test.ts"
],
"assertionRefs": [
+ {
+ "file": "src/main/runtime/orca-runtime.test.ts",
+ "assertions": [
+ "completion-sensitive process inspection preserves authoritative host/provider failures"
+ ]
+ },
+ {
+ "file": "src/main/providers/pty-process-inspection.test.ts",
+ "assertions": [
+ "dedicated provider inspection preserves failures and rejects missing PTYs instead of returning idle evidence"
+ ]
+ },
+ {
+ "file": "src/main/daemon/daemon-pty-router.test.ts",
+ "assertions": [
+ "completion inspection rejects an unmapped session instead of borrowing the current daemon"
+ ]
+ },
+ {
+ "file": "src/main/daemon/degraded-daemon-pty-provider.test.ts",
+ "assertions": [
+ "completion inspection rejects an unmapped session instead of borrowing the local fallback"
+ ]
+ },
+ {
+ "file": "src/relay/pty-handler.test.ts",
+ "assertions": [
+ "strict relay inspection rejects a missing PTY"
+ ]
+ },
+ {
+ "file": "tests/e2e/remote-agent-completion-authority.unit.test.ts",
+ "assertions": [
+ "transport loss remains unknown through reconnect and cannot dispatch completion",
+ "returned unavailability or a thrown transport failure interrupts consecutive-idle proof and requires two fresh authoritative idle samples",
+ "explicit stop, real exit status, and genuine successful completion remain distinct"
+ ]
+ },
+ {
+ "file": "src/renderer/src/runtime/runtime-terminal-inspection.test.ts",
+ "assertions": [
+ "direct SSH terminals use strict main-process inspection rather than lax split IPC evidence"
+ ]
+ },
+ {
+ "file": "src/renderer/src/components/terminal-pane/pty-connection.test.ts",
+ "assertions": [
+ "completion polling uses the atomic process-inspection boundary without regressing established lifecycle behavior"
+ ]
+ },
+ {
+ "file": "src/renderer/src/lib/codex-session-restart.test.ts",
+ "assertions": [
+ "one unreachable pane cannot suppress restart notices for another authoritatively confirmed Codex pane"
+ ]
+ },
{
"file": "src/shared/claimed-agent-pty-owner.test.ts",
"assertions": [
@@ -1485,9 +1568,60 @@
"only an exact structured-create handoff retires its provisional tab",
"an absent host tab retires its exact provisional handoff only after a causally post-create snapshot while unrelated tabs remain"
]
+ },
+ {
+ "file": "tests/e2e/remote-terminal-tab-retirement.unit.test.ts",
+ "assertions": [
+ "a durable host exit removes the terminal from two independent viewer mirrors instead of publishing a handle-less phantom",
+ "one exact exit produces one same-epoch higher-version host publication and one durable persistence flush",
+ "same-epoch stale publications cannot resurrect the retired surface after reconnect"
+ ]
+ },
+ {
+ "file": "src/main/runtime/orca-runtime.test.ts",
+ "assertions": [
+ "v1.4.150-shaped agent, setup, and shell PTYs adopt as one CAS transaction while stale incarnation and competing clients fail safely",
+ "current-generation restart and disconnect/reconnect preserve output, input, resize, title, tab, leaf, handle, and incarnation identity",
+ "split-pane and multi-group legacy topology merges beside a newer host-owned terminal without replacing it",
+ "equivalent Windows and separator-normalized persisted worktree keys canonicalize without duplicate terminal topology",
+ "connection mismatch, reused handles, SSH ownership mismatch, and stale topology revisions cannot claim a live PTY while WSL ownership succeeds"
+ ]
+ },
+ {
+ "file": "src/renderer/src/runtime/web-session-terminal-orphan-recovery.test.ts",
+ "assertions": [
+ "absence stays pending until an exact live orphan adoption settles",
+ "client pane and group topology is pruned to exact orphan claims and translated to host tab identities",
+ "a missing split leaf remains recoverable when another leaf in the same tab is already host-owned"
+ ]
+ },
+ {
+ "file": "src/renderer/src/runtime/web-session-terminal-orphan-mixed-version.test.ts",
+ "assertions": [
+ "mixed-version inventory without incarnation evidence remains visible but cannot adopt",
+ "a truncated legacy unfiltered inventory cannot hide a candidate whose liveness is unresolved"
+ ]
}
],
"evidenceRuns": [
+ {
+ "date": "2026-07-22",
+ "runner": "manual",
+ "platform": "macos",
+ "command": "Manual headed paired-server journey: isolated Orca desktop host + separate paired web client + real Codex process + 20-second WebSocket fault + reconnect + explicit stop",
+ "result": "passed",
+ "durationSeconds": 549,
+ "summary": "The primary user topology used an isolated headed Orca desktop as the owning server and a separate paired Edge client. Host inspection reported Codex alive before, during, and after a page-scoped WebSocket fault; the client showed no completion toast, reconnected to the same live Codex TUI, and explicit stop restored the shell prompt with no child process."
+ },
+ {
+ "date": "2026-07-23",
+ "runner": "local",
+ "platform": "macos",
+ "command": "pnpm exec vitest run --config config/vitest.config.ts tests/e2e/remote-agent-completion-authority.unit.test.ts src/main/providers/pty-process-inspection.test.ts src/main/daemon/terminal-host.test.ts src/main/daemon/daemon-pty-router.test.ts src/main/daemon/degraded-daemon-pty-provider.test.ts src/relay/pty-handler.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/renderer/src/runtime/runtime-terminal-inspection.test.ts src/renderer/src/components/terminal-pane/agent-completion-coordinator.test.ts src/renderer/src/components/terminal-pane/pty-connection.test.ts src/renderer/src/lib/codex-session-restart.test.ts",
+ "result": "passed",
+ "durationSeconds": 11.96,
+ "summary": "Eleven focused files and 870 tests passed on the current-main candidate. The cross-boundary harness fails with the implementation reverted by dispatching process-exit from unavailable remote evidence. Direct SSH uses strict main-process inspection, daemon and relay inspection reject missing or unmapped sessions, the terminal lifecycle suite uses the atomic inspection boundary, and one stale pane cannot suppress restart notices for a separately confirmed Codex pane."
+ },
{
"date": "2026-07-21",
"runner": "local",
@@ -1498,13 +1632,40 @@
"summary": "Ten focused files and 325 tests passed after the final review fixes, covering claim scope, mixed-version Pi/SSH fallback ordering, operation replay, terminal retirement, causal inventory fencing, exact concurrent handoff confirmation, daemon-generation integration, transport behavior, and remote host integration."
},
{
- "date": "2026-07-21",
+ "date": "2026-07-22",
"runner": "local",
"platform": "macos",
"command": "pnpm test:repro:remote-agent-session",
"result": "passed",
- "durationSeconds": 48.47,
- "summary": "The build-backed headless remote Orca harness passed over encrypted WebSocket pairing with two independent clients, proving one spawn, retry adoption, durable exit retirement, stale-publication rejection, and no restart resurrection."
+ "durationSeconds": 48.63,
+ "summary": "The secondary build-backed headless parity harness passed post-rebase on main@72a2d7bc7 over encrypted WebSocket pairing with independent clients, proving one spawn, retry adoption, durable exit retirement, stale-publication rejection, and no restart resurrection."
+ },
+ {
+ "date": "2026-07-22",
+ "runner": "local",
+ "platform": "macos",
+ "command": "pnpm exec vitest run --config config/vitest.config.ts tests/e2e/remote-terminal-tab-retirement.unit.test.ts",
+ "result": "failed",
+ "durationSeconds": 3.11,
+ "summary": "The exact cross-boundary oracle failed on pre-#9687 commit 2a32c5c9a because the retired publication still contained the pinned persisted terminal surface."
+ },
+ {
+ "date": "2026-07-22",
+ "runner": "local",
+ "platform": "macos",
+ "command": "pnpm exec vitest run --config config/vitest.config.ts tests/e2e/remote-terminal-tab-retirement.unit.test.ts",
+ "result": "failed",
+ "durationSeconds": 3.48,
+ "summary": "The exact strengthened oracle failed on PR #9053 head d3a1d3047 because its stale-headless pruning retained the pinned persisted terminal surface."
+ },
+ {
+ "date": "2026-07-22",
+ "runner": "local",
+ "platform": "macos",
+ "command": "pnpm exec vitest run --config config/vitest.config.ts tests/e2e/remote-terminal-tab-retirement.unit.test.ts",
+ "result": "passed",
+ "durationSeconds": 3.18,
+ "summary": "The same strengthened oracle passed on main@4fce2de49."
}
],
"runtimeBudget": {
@@ -1517,25 +1678,166 @@
},
"redGreenEvidence": {
"status": "partial",
- "evidence": "The motivating remote-client duplicate-resume and exited-surface repros are encoded in deterministic lower-layer tests and the real remote harness; saved CI red/green artifacts are still needed."
+ "evidence": "Issue #9151 has local red/green evidence: current main dispatches process-exit from a stale remote handle, while the fix preserves unknown liveness and requires fresh consecutive idle evidence after reconnect. The exact retirement oracle is red on pre-#9687 commit 2a32c5c9a and PR #9053 head d3a1d3047 because the pinned persisted surface remains in the host publication, and green on main@4fce2de49. Duplicate-resume red evidence remains encoded in lower-layer tests; saved CI artifacts are still needed."
},
"performanceBudget": {
"required": true,
- "evidence": "Agent-session reconciliation runs only at explicit claim admission, dedupes concurrent provider listing, and adds no polling or renderer output work. Create-operation ledgers are capped globally and per client, expire after 24 hours, and reject rather than evict live replay fences. Capability caches are bounded or connection-scoped, and exact handoffs are consumed by the next authoritative snapshot."
+ "evidence": "Agent-session reconciliation runs only at explicit claim admission, dedupes concurrent provider listing, and adds no polling or renderer output work. Completion inspection reuses the coordinator's per-pane in-flight guard, global concurrency/rate queue, and existing error backoff; the strict daemon path reduces two foreground RPCs to one. Create-operation ledgers are capped globally and per client, expire after 24 hours, and reject rather than evict live replay fences. Capability caches are bounded or connection-scoped, and exact handoffs are consumed by the next authoritative snapshot."
},
"promotionCriteria": [
"Run the focused gate and remote-server repro for at least 100 consecutive passes or 14 days across required CI platforms.",
"Attach saved red/green evidence for duplicate remote resume and exit-before-snapshot retirement.",
- "Add live Linux/Windows and SSH/WSL provider evidence before claiming full platform/provider coverage."
+ "Automate the headed Orca desktop-server journey with a separate paired client; add a physical host only when OS, ConPTY, update, sleep, firewall, or window lifecycle is causal.",
+ "Add live SSH/WSL provider evidence before claiming full provider coverage; Docker SSH proves only the SSH provider/relay path."
],
"knownGaps": [
- "The real remote-server harness currently runs on macOS and uses a local daemon-backed execution owner; Linux and Windows runs remain uncollected.",
- "SSH and relay failure ordering is deterministic contract coverage, not a live SSH-host journey; WSL has no provider-specific run.",
+ "The primary headed macOS desktop-server journey is collected but not automated in CI; Windows and Linux window, ConPTY, update, sleep/wake, and firewall behavior remain uncollected.",
+ "Mixed-version pairings remain conservative only when the completion-aware client and strict-inspection host changes are both present; older peers retain their legacy classification behavior.",
+ "The secondary headless parity harness runs on macOS with a local daemon-backed execution owner and independent short-lived encrypted RPC clients; two persistent viewer-store mirrors and reconnect ordering are joined deterministically in the cross-boundary unit test rather than mounted live.",
+ "SSH and relay failure ordering is deterministic provider-contract coverage, not a live SSH-host journey or paired-Orca-server proof; WSL has no provider-specific run, and Linux and Windows runs remain uncollected.",
"Fresh-launch operation replay is memory-backed and intentionally does not survive runtime restart; a durable operation journal is a documented future extension.",
"Automatic sleep checkpoints, verified nested-SSH execution namespaces, and multi-process profile coordination remain outside v1."
],
"demotionRule": "Keep experimental or demote if the focused gate flakes without a product or harness bug, if a retry can physically spawn twice, if a stale exit/publication can replace or resurrect a terminal, or if mixed-version fallback occurs after an authority side effect."
},
+ {
+ "id": "runtime-routing.active-server-preference",
+ "title": "Active Server changes only through its explicit Advanced control",
+ "maturity": "experimental",
+ "protection": "partial",
+ "owner": "runtime-routing",
+ "layer": "main-preload-renderer-persistence-contract",
+ "surfaces": [
+ "Advanced Active Server setting",
+ "saved server Connect and Disconnect",
+ "remote workspace navigation",
+ "terminal reveal and create",
+ "browser and mobile handoff",
+ "app restart"
+ ],
+ "platforms": [
+ "macos",
+ "linux",
+ "windows",
+ "mobile"
+ ],
+ "providers": [
+ "local",
+ "remote-runtime",
+ "ssh",
+ "wsl"
+ ],
+ "coveredPlatforms": [
+ "macos"
+ ],
+ "coveredProviders": [
+ "local",
+ "remote-runtime",
+ "ssh",
+ "wsl"
+ ],
+ "coverageNotes": "Platform-neutral deterministic tests separate the durable Active Server preference from per-client connection, selected-workspace, browser-session, and execution-host routing. The composed regression models Local desktop -> connect/navigate Windows 2 -> reveal a local terminal -> restart. Multi-client browser host overrides, multi-server profile caches, generic settings IPC rejection, local and remote workspace ownership, and restart reset of transient routing are covered. Live desktop UI runs remain uncollected.",
+ "motivatingLinks": [
+ "https://github.com/stablyai/orca/pull/9687"
+ ],
+ "invariant": "Only an explicit user change in Settings > Remote Orca Servers > Advanced > Active Server may mutate activeRuntimeEnvironmentId. Connecting, pairing, disconnecting, selecting or revealing a workspace or terminal, browser/mobile handoff, remote navigation, and reconnect must use transient or target-owner routing and must never rewrite the durable preference. Generic settings mutation cannot bypass the dedicated preference IPC.",
+ "oracle": "Start with Active Server=Local desktop, connect and navigate Windows 2, then reveal a local terminal and assert it succeeds while the persisted preference remains local before and after restart. Repeat with multiple clients and servers, browser host switches, remote-owned and local-owned workspaces, pairing/connect/disconnect, and generic settings writes. Assert only the dedicated validated preference method changes activeRuntimeEnvironmentId and stale host-operation completions cannot overwrite the newly selected transient host.",
+ "commands": [
+ "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/settings.test.ts src/main/ipc/runtime-environments.test.ts src/renderer/src/store/slices/settings.test.ts src/renderer/src/store/slices/browser.test.ts src/renderer/src/components/settings/browser-session-host-selection.test.ts src/renderer/src/components/settings/RuntimeEnvironmentsPane.test.ts src/renderer/src/components/status-bar/SshStatusSegment.test.ts src/renderer/src/components/sidebar/use-add-repo-host-selection.test.ts src/renderer/src/hooks/useIpcEvents.test.ts src/renderer/src/web/web-preload-api.test.ts --maxWorkers=1"
+ ],
+ "testFiles": [
+ "src/main/ipc/settings.test.ts",
+ "src/main/ipc/runtime-environments.test.ts",
+ "src/renderer/src/store/slices/settings.test.ts",
+ "src/renderer/src/store/slices/browser.test.ts",
+ "src/renderer/src/components/settings/browser-session-host-selection.test.ts",
+ "src/renderer/src/components/settings/RuntimeEnvironmentsPane.test.ts",
+ "src/renderer/src/components/status-bar/SshStatusSegment.test.ts",
+ "src/renderer/src/components/sidebar/use-add-repo-host-selection.test.ts",
+ "src/renderer/src/hooks/useIpcEvents.test.ts",
+ "src/renderer/src/web/web-preload-api.test.ts"
+ ],
+ "assertionRefs": [
+ {
+ "file": "src/main/ipc/settings.test.ts",
+ "assertions": [
+ "generic settings IPC strips activeRuntimeEnvironmentId while the dedicated validated IPC persists it",
+ "invalid preference types and unknown server identities cannot mutate the durable preference"
+ ]
+ },
+ {
+ "file": "src/renderer/src/hooks/useIpcEvents.test.ts",
+ "assertions": [
+ "Local desktop remains the durable default after transient Windows 2 navigation and a focused local terminal reveal succeeds",
+ "local and remote terminal create route by target workspace ownership instead of the durable preference"
+ ]
+ },
+ {
+ "file": "src/renderer/src/store/slices/browser.test.ts",
+ "assertions": [
+ "multiple clients select different transient browser hosts without changing Active Server",
+ "restart clears transient browser host override while retaining the durable local preference",
+ "late profile and import results update only their captured host and cannot overwrite a newer selection"
+ ]
+ },
+ {
+ "file": "src/renderer/src/components/settings/browser-session-host-selection.test.ts",
+ "assertions": [
+ "a removed transient server override falls back to an available host instead of leaving browser settings on an invalid option"
+ ]
+ },
+ {
+ "file": "src/renderer/src/components/settings/RuntimeEnvironmentsPane.test.ts",
+ "assertions": [
+ "connection status and the Advanced default-host selection are distinct concepts"
+ ]
+ },
+ {
+ "file": "src/renderer/src/web/web-preload-api.test.ts",
+ "assertions": [
+ "generic web settings writes cannot mutate Active Server",
+ "the dedicated web preference setter rejects unknown server identities without corrupting the saved choice"
+ ]
+ }
+ ],
+ "evidenceRuns": [
+ {
+ "date": "2026-07-22",
+ "runner": "local",
+ "platform": "macos",
+ "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/settings.test.ts src/main/ipc/runtime-environments.test.ts src/renderer/src/store/slices/settings.test.ts src/renderer/src/store/slices/browser.test.ts src/renderer/src/components/settings/browser-session-host-selection.test.ts src/renderer/src/components/settings/RuntimeEnvironmentsPane.test.ts src/renderer/src/components/status-bar/SshStatusSegment.test.ts src/renderer/src/components/sidebar/use-add-repo-host-selection.test.ts src/renderer/src/hooks/useIpcEvents.test.ts src/renderer/src/web/web-preload-api.test.ts --maxWorkers=1",
+ "result": "passed",
+ "durationSeconds": 6.55,
+ "summary": "Ten files and 282 tests passed, including the composed Local -> Windows 2 navigation -> local reveal -> restart regression, dedicated-only preference persistence, removed transient-host fallback, multi-client browser routing, late host-operation suppression, and web pairing/preference separation."
+ }
+ ],
+ "runtimeBudget": {
+ "p95Seconds": 20,
+ "scope": "focused persistence and routing contract tests"
+ },
+ "flakeHistory": {
+ "status": "unknown",
+ "evidence": "New deterministic gate with no soak history."
+ },
+ "redGreenEvidence": {
+ "status": "partial",
+ "evidence": "The user-observed local terminal reveal failure and unintended Active Server switch are encoded by deterministic routing and persistence tests; a saved intentional-break artifact is not yet attached."
+ },
+ "performanceBudget": {
+ "required": false,
+ "evidence": "Preference writes are explicit user actions; transient routing adds no polling and uses existing host/worktree indexes."
+ },
+ "promotionCriteria": [
+ "Run the focused gate in soak across macOS, Linux, and Windows.",
+ "Attach a live Windows Local -> Windows 2 -> local reveal -> restart artifact.",
+ "Attach saved red/green evidence for generic settings mutation and transient connection routing."
+ ],
+ "knownGaps": [
+ "The exact journey is deterministic contract coverage, not a packaged Windows UI automation run.",
+ "Browser/mobile handoff is covered through transient routing state and preload contracts, not a live phone browser session."
+ ],
+ "demotionRule": "Demote or block release if any non-Advanced path mutates Active Server, if local reveal depends on the durable default instead of workspace ownership, or if transient host state survives restart."
+ },
{
"id": "terminal-geometry.visible-convergence",
"title": "Visible desktop terminals converge across xterm, fit, PTY, shell, and runtime mirror size",
@@ -2350,7 +2652,7 @@
"id": "terminal-performance.no-hot-list-sessions",
"title": "Hot terminal interactions do not call global PTY session listing",
"maturity": "experimental",
- "protection": "none",
+ "protection": "partial",
"owner": "terminal-performance",
"layer": "ipc-count-contract",
"surfaces": [
@@ -2374,19 +2676,74 @@
"wsl",
"remote-runtime"
],
- "coveredPlatforms": [],
+ "coveredPlatforms": [
+ "macos"
+ ],
"coveredProviders": [],
- "coverageNotes": "Registered gap on main. The targeted-hasPty product hardening and no-hot count assertions exist only on the pending reliability stack. It registers here with its owning split PR.",
+ "coverageNotes": "Platform-neutral unit coverage proves the Resource Manager closed badge performs one readiness seed, coalesces unknown spawn signals, skips known-session reattach signals, and installs no interval. Broader terminal interaction coverage remains on the pending reliability stack.",
"motivatingLinks": [
"https://github.com/stablyai/orca/pull/7002",
- "https://github.com/stablyai/orca/pull/6858"
+ "https://github.com/stablyai/orca/pull/6858",
+ "https://github.com/stablyai/orca/issues/9386",
+ "https://github.com/stablyai/orca/pull/9387"
],
"invariant": "Typing, focus, terminal switch, workspace switch, visibility resume, resize, render, and per-pane liveness paths must not call global pty:listSessions; they must use targeted per-PTY APIs or cached provider-owned state.",
- "oracle": "The current executable slice asserts targeted visibility/first-input liveness, resize re-assertion after visibility resume, light tab/active-state resume, SSH/remote skip behavior, and closed Resource Manager status badges avoid pty:listSessions; targeted hasPty/getSize calls are allowed for liveness/resize slices and forbidden for light tab/active-state resume. The full hot-path oracle still needs instrumentation around raw focus, split focus, workspace switch, render ticks, and high-session local/daemon/SSH fixtures.",
- "commands": [],
- "testFiles": [],
- "assertionRefs": [],
- "evidenceRuns": [],
+ "oracle": "The current executable slice asserts targeted visibility/first-input liveness, resize re-assertion after visibility resume, light tab/active-state resume, SSH/remote skip behavior, and a closed Resource Manager budget of one readiness seed plus one coalesced inventory read only for unknown spawn IDs; known-session reattach signals and steady closed time perform zero reads. Targeted hasPty/getSize calls are allowed for liveness/resize slices and forbidden for light tab/active-state resume. The full hot-path oracle still needs instrumentation around raw focus, split focus, workspace switch, render ticks, and high-session local/daemon/SSH fixtures.",
+ "commands": [
+ "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/pty.test.ts src/renderer/src/components/status-bar/use-resource-session-inventory.test.tsx src/renderer/src/components/status-bar/resource-session-inventory.test.ts src/renderer/src/components/status-bar/ResourceUsageStatusSegment.session-polling.test.ts"
+ ],
+ "testFiles": [
+ "src/main/ipc/pty.test.ts",
+ "src/renderer/src/components/status-bar/use-resource-session-inventory.test.tsx",
+ "src/renderer/src/components/status-bar/resource-session-inventory.test.ts",
+ "src/renderer/src/components/status-bar/ResourceUsageStatusSegment.session-polling.test.ts"
+ ],
+ "assertionRefs": [
+ {
+ "file": "src/renderer/src/components/status-bar/use-resource-session-inventory.test.tsx",
+ "assertions": [
+ "the false-to-true workspace readiness transition performs one daemon inventory seed",
+ "a failed readiness seed surfaces an error and a later inventory refresh recovers",
+ "known-session reattach signals perform zero additional inventory reads",
+ "multiple unknown background spawn signals coalesce to one inventory read",
+ "spawn signals during a slow inventory read never overlap provider-wide scans and cause at most one required follow-up",
+ "unknown sessions that exit before reconciliation cancel their queued inventory read",
+ "unmount during a slow inventory read cannot schedule follow-up work",
+ "exit and out-of-order refresh races cannot resurrect stale sessions"
+ ]
+ },
+ {
+ "file": "src/main/ipc/pty.test.ts",
+ "assertions": [
+ "global inventory starts local and SSH provider listings concurrently"
+ ]
+ },
+ {
+ "file": "src/renderer/src/components/status-bar/resource-session-inventory.test.ts",
+ "assertions": [
+ "daemon inventory construction copies its source and preserves count parity",
+ "single and batch removals preserve unrelated sessions and no-op references"
+ ]
+ },
+ {
+ "file": "src/renderer/src/components/status-bar/ResourceUsageStatusSegment.session-polling.test.ts",
+ "assertions": [
+ "the closed inventory hook installs no interval",
+ "the badge count comes from cached daemon inventory rather than wake-hint bindings"
+ ]
+ }
+ ],
+ "evidenceRuns": [
+ {
+ "date": "2026-07-22",
+ "runner": "local",
+ "platform": "macos",
+ "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/pty.test.ts src/renderer/src/components/status-bar/use-resource-session-inventory.test.tsx src/renderer/src/components/status-bar/resource-session-inventory.test.ts src/renderer/src/components/status-bar/ResourceUsageStatusSegment.session-polling.test.ts",
+ "result": "passed",
+ "durationSeconds": 4.3,
+ "summary": "4 files and 358 tests passed, covering readiness seed/recovery, zero interval polling, bounded unknown-spawn reconciliation, concurrent provider starts, exit fencing, cleanup, and out-of-order refresh fencing."
+ }
+ ],
"runtimeBudget": {
"p95Seconds": 20,
"scope": "unit or focused Electron count gate"
@@ -2397,7 +2754,7 @@
},
"redGreenEvidence": {
"status": "partial",
- "evidence": "Tests assert visibility resume prefers targeted hasPty over listSessions, first input after visibility resume calls targeted hasPty once, resize re-assertion after visibility resume uses getSize/resize without listSessions, light tab switches and visible active-state resume avoid listSessions/hasPty/getSize fanout while still allowing the active PTY scheduler hint, SSH/remote broad listing is skipped, Resource Manager broad session inventory polling is scoped to the open popover rather than its closed badge, and panes close only on authoritative false. Needs broader raw focus/workspace-switch/render/high-session count coverage before promotion."
+ "evidence": "Tests assert visibility resume prefers targeted hasPty over listSessions, first input after visibility resume calls targeted hasPty once, resize re-assertion after visibility resume uses getSize/resize without listSessions, light tab switches and visible active-state resume avoid listSessions/hasPty/getSize fanout while still allowing the active PTY scheduler hint, SSH/remote broad listing is skipped, and the closed Resource Manager performs one readiness seed while known reattach signals and steady time perform no additional reads. Needs broader raw focus/workspace-switch/render/high-session count coverage before promotion."
},
"performanceBudget": {
"required": true,
@@ -2409,8 +2766,8 @@
"Run with enough preserved sessions/providers to make a broad listing observable."
],
"knownGaps": [
- "No executable coverage on main yet; the slice lives on the pending fix-terminal-reliability stack.",
- "Current command covers targeted visibility/first-input liveness, resize re-assertion on visibility resume, light tab/active-state resume, SSH/remote skip behavior, and closed Resource Manager session-poll avoidance, but not every hot interaction listed in the invariant.",
+ "Current commands cover Resource Manager readiness/lifecycle inventory counts; the broader targeted-liveness slice still lives on the pending fix-terminal-reliability stack.",
+ "Current coverage includes the closed Resource Manager's no-interval and known-reattach budgets, but not every hot interaction listed in the invariant.",
"No Electron or IPC-level high-session counter gate yet proves raw focus, workspace switch, render, or high-session typing stay at zero global listSessions calls."
],
"demotionRule": "Cannot promote if the test allows broad listing in any hot interaction path."
@@ -4663,16 +5020,18 @@
"macos"
],
"coveredProviders": [],
- "coverageNotes": "Local macOS evidence over the runtime-RPC stream budgets on main@1282f5c2d. PR #5824 adds a platform-neutral mobile decision gate proving chat-covered terminal streams pause and resume only after the mounted WebView is ready; live Android restore evidence remains required. The pending stack adds byte-exact 512KB/2MB/256KB/48KB budget assertions; legacy JSON subscribe parity remains undecided.",
+ "coverageNotes": "Local macOS evidence covers runtime-RPC stream budgets plus paired-renderer parse/discard credit. Deferred credit is shared by local and remote transports, batches ACKs at 192 KiB or 4 ms, grows per-stream windows from 512 KiB to 2 MiB and aggregate windows from 2 MiB to 8 MiB, bounds queued output to 256 KiB per stream, and caps each multiplex connection at 32 active or pending streams for an 8 MiB aggregate pending-output ceiling. Deterministic tests cover replay ordering, stale generations, malformed frames, hidden panes, queue eviction, disposal, send/recovery failure, repeated pending-slot replacement, reconnect, and round-robin fairness. The opt-in benchmark covers 1/20/100 ms RTT and 1/4/8 viewers, exact protocol-frame allocations, scheduler CPU, and measured @xterm/headless parser CPU/retained heap. Live Android restore evidence, browser/WebGL parser measurements, and legacy JSON subscribe parity remain required.",
"motivatingLinks": [
"https://github.com/stablyai/orca/pull/6951",
"https://github.com/stablyai/orca/pull/6955",
"https://github.com/stablyai/orca/pull/7009"
],
- "invariant": "Runtime and mobile terminal subscriptions must cap initial snapshots, live output buffered while snapshots load, chunk sizes, and batch sizes; a terminal covered by native chat must have no live output subscription and must restore from fresh scrollback when revealed, while preserving output order, input locks, resize/driver events, and fallback parity or explicit fallback deprecation.",
- "oracle": "The current executable slice asserts mobile initial snapshots downgrade until they fit <=512KB, requested binary snapshots downgrade until they fit <=2MB, binary live output queued while the initial snapshot loads stays <=256KB while preserving the newest tail, large binary output is split into <=48KB frames, output bursts are coalesced before emit, aborts do not register stale listeners, and stale mobile resize re-stream completions are dropped. The mobile native-chat decision test asserts an active stream pauses while covered and resumes only for a ready active terminal. JSON fallback parity and live Android scrollback restoration remain explicit gaps.",
+ "invariant": "Runtime and mobile terminal subscriptions must cap initial snapshots, live output buffered while snapshots load, chunk sizes, batches, and aggregate in-flight credit. ACK means the renderer parsed the bytes or intentionally discarded them; receipt-time ACK is forbidden. Every replay, stale-generation, malformed-frame, hidden-pane, eviction, disposal, error, and reconnect path must settle credit exactly once so streams neither leak memory nor stall. A terminal covered by native chat must restore from fresh scrollback when revealed, while preserving output order, input locks, resize/driver events, fairness, and fallback parity or explicit fallback deprecation.",
+ "oracle": "Assert mobile initial snapshots downgrade until they fit <=512KB, requested binary snapshots downgrade until they fit <=2MB, live output queued while snapshots load stays <=256KB per stream, large output splits into <=48KB frames, and output bursts coalesce. Feed paired output through the xterm parse callback and prove ACK is deferred until parse or intentional discard, then inject stale generation, malformed/transformed frames, replay failure, queue eviction, hidden panes, pane disposal, ACK send failure, recovery serialization failure, and reconnect races; assert ordered replay and exactly-once credit settlement. Fill the aggregate window across bulk and interactive streams, ACK once, and prove round-robin progress. Run the opt-in 64 MiB/viewer RTT matrix and enforce bounded 8 MiB aggregate in-flight memory, >7 MiB/s/viewer at 100 ms RTT, and <200 ms completion spread. JSON fallback parity and live Android scrollback restoration remain explicit gaps.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/rpc/terminal-subscribe-buffer.test.ts src/main/runtime/rpc/terminal-output-batching.test.ts src/main/runtime/rpc/terminal-multiplex.test.ts",
+ "pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/rpc/terminal-subscribe-buffer.test.ts src/main/runtime/rpc/terminal-output-batching.test.ts src/main/runtime/rpc/terminal-multiplex.test.ts src/renderer/src/components/terminal-pane/pty-connection.test.ts src/renderer/src/components/terminal-pane/terminal-pty-ack-gate.test.ts src/renderer/src/lib/pane-manager/terminal-delivery-credit.test.ts src/renderer/src/runtime/remote-runtime-terminal-parse-backpressure.test.ts src/renderer/src/runtime/runtime-terminal-stream.test.ts --maxWorkers=1",
+ "ORCA_TERMINAL_PERF_BENCH=1 pnpm exec vitest run --config config/vitest.config.ts --disableConsoleIntercept src/main/runtime/rpc/terminal-multiplex-flow-control.bench.test.ts",
"pnpm --dir mobile exec vitest run --root .. mobile/src/session/mobile-native-chat-terminal-stream.test.ts",
"pnpm --dir mobile exec vitest run --root .. mobile/src/session/use-mobile-native-chat-terminal-stream.test.ts"
],
@@ -4680,6 +5039,12 @@
"src/main/runtime/rpc/terminal-subscribe-buffer.test.ts",
"src/main/runtime/rpc/terminal-output-batching.test.ts",
"src/main/runtime/rpc/terminal-multiplex.test.ts",
+ "src/main/runtime/rpc/terminal-multiplex-flow-control.bench.test.ts",
+ "src/renderer/src/components/terminal-pane/pty-connection.test.ts",
+ "src/renderer/src/components/terminal-pane/terminal-pty-ack-gate.test.ts",
+ "src/renderer/src/lib/pane-manager/terminal-delivery-credit.test.ts",
+ "src/renderer/src/runtime/remote-runtime-terminal-parse-backpressure.test.ts",
+ "src/renderer/src/runtime/runtime-terminal-stream.test.ts",
"mobile/src/session/mobile-native-chat-terminal-stream.test.ts",
"mobile/src/session/use-mobile-native-chat-terminal-stream.test.ts"
],
@@ -4704,7 +5069,33 @@
"assertions": [
"requested snapshots fall back smaller when serialized data exceeds the send budget",
"oversized live output frames are bounded for subscribed binary streams",
- "multibyte live output flushes when encoded bytes reach the batch budget"
+ "multibyte live output flushes when encoded bytes reach the batch budget",
+ "adaptive credit grows only after ACK, stays globally bounded, and drains pending streams round-robin",
+ "send and recovery serialization failures detach once instead of leaking credit or retrying forever",
+ "32 active or pending slots cap aggregate queued output and repeated pending-slot subscribe cancels its older waiter"
+ ]
+ },
+ {
+ "file": "src/renderer/src/lib/pane-manager/terminal-delivery-credit.test.ts",
+ "assertions": [
+ "nested synchronous deliveries restore the outer credit owner",
+ "unclaimed intentional discards settle automatically while every claimed scheduler child must settle before the parent credits"
+ ]
+ },
+ {
+ "file": "src/renderer/src/runtime/remote-runtime-terminal-parse-backpressure.test.ts",
+ "assertions": [
+ "paired renderer ACK waits for xterm parse completion or explicit discard",
+ "192 KiB parsed output batches into one ACK while the 4 ms timer releases interactive output",
+ "malformed frames, malformed transformed output, disposal, late parse, renderer delivery failure, and ACK transport failure release credit or close the owning stream without reordering output"
+ ]
+ },
+ {
+ "file": "src/main/runtime/rpc/terminal-multiplex-flow-control.bench.test.ts",
+ "assertions": [
+ "one through eight viewers stay within the 8 MiB aggregate adaptive window",
+ "the 100 ms RTT model sustains more than 7 MiB/s per viewer with less than 200 ms fairness spread",
+ "the opt-in benchmark reports RTT throughput, scheduler CPU time, exact protocol frame allocations, completion spread, and measured @xterm/headless parser CPU and retained heap"
]
},
{
@@ -4741,6 +5132,15 @@
"result": "passed",
"durationSeconds": 0.2,
"summary": "The focused mobile native-chat suite passed with 3 terminal-stream lifecycle assertions in the staged PR #5824 worktree."
+ },
+ {
+ "date": "2026-07-22",
+ "runner": "local",
+ "platform": "macos",
+ "command": "ORCA_TERMINAL_PERF_BENCH=1 pnpm exec vitest run --config config/vitest.config.ts --disableConsoleIntercept src/main/runtime/rpc/terminal-multiplex-flow-control.bench.test.ts",
+ "result": "passed",
+ "durationSeconds": 0.91,
+ "summary": "The 1/20/100 ms RTT x 1/4/8 viewer matrix stayed at or below 8 MiB in flight with zero completion spread. At 100 ms it modeled 18.8 MiB/s per viewer for 1-4 viewers and 9.7 MiB/s for 8 viewers. Measured @xterm/headless parsing was 26.7/63.6/95.3 aggregate MiB/s for 1/4/8 viewers, with 84.4/236.5/336.0 ms CPU and 2893/13409/28991 KiB retained heap for 4 MiB per viewer."
}
],
"runtimeBudget": {
@@ -4757,7 +5157,7 @@
},
"performanceBudget": {
"required": true,
- "evidence": "This gate is the byte and batching budget for runtime/mobile terminal streaming."
+ "evidence": "Parsed/discarded credit uses 192 KiB/4 ms ACK batching, 512 KiB-to-2 MiB adaptive per-stream windows, a 2 MiB-to-8 MiB aggregate window, <=48 KiB output frames, <=256 KiB queued output per stream, and <=32 streams per connection (8 MiB aggregate pending output). The 64 MiB/viewer model gate requires >7 MiB/s/viewer at 100 ms RTT, <200 ms completion spread, and aggregate in-flight bytes <=8 MiB. The 2026-07-22 run modeled 9.7 MiB/s/viewer at 100 ms with eight viewers and measured @xterm/headless at 95.3 aggregate MiB/s, 336.0 ms parser CPU, and 28991 KiB retained heap for eight 4 MiB viewers."
},
"promotionCriteria": [
"Gate binary multiplex first.",
@@ -4766,7 +5166,8 @@
],
"knownGaps": [
"The pure mobile decision gate does not yet prove live Android WebView scrollback restore after a chat toggle.",
- "Legacy JSON subscribe parity is undecided."
+ "Legacy JSON subscribe parity is undecided.",
+ "The parser measurement uses @xterm/headless; browser renderer/WebGL CPU, GPU, and allocation behavior still need packaged-app performance evidence."
],
"demotionRule": "Cannot promote while a supported stream path has uncapped snapshot or live-output buffering."
},
diff --git a/config/scripts/computer-e2e-workflow.test.mjs b/config/scripts/computer-e2e-workflow.test.mjs
index 3ef41583217..560859c5264 100644
--- a/config/scripts/computer-e2e-workflow.test.mjs
+++ b/config/scripts/computer-e2e-workflow.test.mjs
@@ -177,7 +177,7 @@ describe('computer-use e2e workflow', () => {
)
})
- it('builds Electron main output before every computer-use e2e run', () => {
+ it('builds Electron main output and verifies the CLI before every computer-use e2e run', () => {
const workflow = parse(
readFileSync(join(projectDir, '.github/workflows/computer-e2e.yml'), 'utf8')
)
@@ -187,6 +187,7 @@ describe('computer-use e2e workflow', () => {
.map((step) => step.run)
.filter((run) => typeof run === 'string')
const buildIndex = runs.indexOf('pnpm build:electron-vite')
+ const cliVerifyIndex = runs.indexOf('pnpm verify:cli-runtime')
const e2eIndexes = runs
.map((run, index) => (run.includes('test:e2e:computer') ? index : -1))
.filter((index) => index >= 0)
@@ -195,10 +196,17 @@ describe('computer-use e2e workflow', () => {
buildIndex,
`${jobName} should build out/main before computer e2e`
).toBeGreaterThanOrEqual(0)
+ expect(
+ cliVerifyIndex,
+ `${jobName} should verify the CLI after the main build`
+ ).toBeGreaterThan(buildIndex)
for (const e2eIndex of e2eIndexes) {
expect(buildIndex, `${jobName} should build out/main before computer e2e`).toBeLessThan(
e2eIndex
)
+ expect(cliVerifyIndex, `${jobName} should verify the CLI before computer e2e`).toBeLessThan(
+ e2eIndex
+ )
}
}
})
diff --git a/config/scripts/generate-windows-blockmap.mjs b/config/scripts/generate-windows-blockmap.mjs
new file mode 100644
index 00000000000..7444a7b43dc
--- /dev/null
+++ b/config/scripts/generate-windows-blockmap.mjs
@@ -0,0 +1,18 @@
+// Regenerate the electron-updater `.blockmap` for a (re-signed) Windows installer.
+// electron-builder 26 dropped the app-builder-bin Go binary; blockmap generation
+// now lives in app-builder-lib's pure-JS `buildBlockMap`. Mirrors createBlockmap's
+// "gzip" format for the standalone NSIS installer blockmap.
+import { createRequire } from 'node:module'
+
+const require = createRequire(import.meta.url)
+
+const [input, output] = process.argv.slice(2)
+if (!input || !output) {
+ console.error('usage: generate-windows-blockmap.mjs ')
+ process.exit(1)
+}
+
+const { buildBlockMap } = require('app-builder-lib/out/targets/blockmap/blockmap')
+
+const info = await buildBlockMap(input, 'gzip', output)
+console.log(`blockmap written: ${output} (installer sha512=${info.sha512}, size=${info.size})`)
diff --git a/config/scripts/run-headless-linux-pairing-docker.mjs b/config/scripts/run-headless-linux-pairing-docker.mjs
index 15f1135bb53..635c66348cc 100644
--- a/config/scripts/run-headless-linux-pairing-docker.mjs
+++ b/config/scripts/run-headless-linux-pairing-docker.mjs
@@ -194,8 +194,21 @@ async function validateAuthenticatedPairing() {
status?._meta?.runtimeId === payload.runtimeId,
'paired client runtime ID does not match ready contract'
)
+ assert(
+ typeof statusResult?.runtime?.appVersion === 'string',
+ 'paired server did not report its Orca app version'
+ )
+ assert(
+ statusResult?.runtime?.capabilities?.includes('updater.remote-control.v1'),
+ 'paired server did not advertise remote updater capability'
+ )
+ assert(
+ statusResult?.runtime?.remoteUpdateSupport?.automatic === false &&
+ statusResult.runtime.remoteUpdateSupport.reason === 'manual-service-update-required',
+ 'direct headless server did not require a safe manual service update'
+ )
stopContainer(server.name)
- console.log('PASS authenticated E2EE pairing from an independent Debian container')
+ console.log('PASS paired E2EE updater inventory and manual-service fallback')
}
async function validateUnreachableOffer() {
diff --git a/config/scripts/run-nested-runtime-ssh-e2e.mjs b/config/scripts/run-nested-runtime-ssh-e2e.mjs
new file mode 100644
index 00000000000..577fd40c6bb
--- /dev/null
+++ b/config/scripts/run-nested-runtime-ssh-e2e.mjs
@@ -0,0 +1,32 @@
+import { spawnSync } from 'node:child_process'
+
+const result = spawnSync(
+ process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm',
+ [
+ 'exec',
+ 'playwright',
+ 'test',
+ 'tests/e2e/nested-runtime-ssh-routing.spec.ts',
+ 'tests/e2e/nested-runtime-ssh-lifecycle.spec.ts',
+ '--config',
+ 'tests/playwright.config.ts',
+ '--project',
+ 'electron-headless',
+ '--workers=1'
+ ],
+ {
+ cwd: process.cwd(),
+ env: {
+ ...process.env,
+ ORCA_E2E_NESTED_RUNTIME_SSH: '1',
+ ORCA_E2E_SSH_DOCKER: '1',
+ ORCA_E2E_WEB_CLIENT: '1'
+ },
+ stdio: 'inherit'
+ }
+)
+
+if (result.error) {
+ throw result.error
+}
+process.exit(result.status ?? 1)
diff --git a/config/scripts/verify-cli-bin.test.mjs b/config/scripts/verify-cli-bin.test.mjs
index da988064d05..d26a603a148 100644
--- a/config/scripts/verify-cli-bin.test.mjs
+++ b/config/scripts/verify-cli-bin.test.mjs
@@ -47,6 +47,14 @@ describe('verifyPackageCliBin', () => {
})
})
+ it('rejects a compiled CLI with a missing runtime dependency', () => {
+ const { projectDir } = makeProjectWithCli(
+ '#!/usr/bin/env node\nrequire("./missing-runtime-dependency")\n'
+ )
+
+ expect(() => verifyPackageCliBin({ projectDir, runHelp: true })).toThrow()
+ })
+
it('rejects an empty package bin target', () => {
const { projectDir } = makeProjectWithCli('')
diff --git a/config/scripts/verify-linux-glibc-floor.cjs b/config/scripts/verify-linux-glibc-floor.cjs
new file mode 100644
index 00000000000..55ec8ca7724
--- /dev/null
+++ b/config/scripts/verify-linux-glibc-floor.cjs
@@ -0,0 +1,394 @@
+const { readdirSync, openSync, readSync, closeSync } = require('node:fs')
+const { spawnSync } = require('node:child_process')
+const { join, relative } = require('node:path')
+
+// Why: v1.4.150 shipped a Linux build whose node-pty pty.node required
+// GLIBC_2.34 (openpty/forkpty were relocated into libc by glibc's
+// libutil/libpthread merge), so the app crashed on startup on Ubuntu 20.04
+// (glibc 2.31) — the runner image silently bumped the build-host glibc. This
+// gate fails Linux packaging if any bundled native binary requires a glibc (or
+// libstdc++) symbol version newer than stock Ubuntu 20.04 ships, so a future
+// runner bump or dependency change cannot reintroduce the regression unnoticed.
+// See docs/reference/linux-glibc-compatibility.md.
+const MIN_GLIBC = Object.freeze([2, 31])
+
+// The symbol-version families this gate checks, each with the highest version
+// node stock Ubuntu 20.04 provides. glibc is the #9902 launch-crash axis;
+// libstdc++ (GLIBCXX_/CXXABI_) is the same crash class for C++ native modules
+// against the system libstdc++ (Orca does not bundle one).
+const VERSION_FLOORS = Object.freeze([
+ Object.freeze({ prefix: 'GLIBC_', floor: MIN_GLIBC }),
+ Object.freeze({ prefix: 'GLIBCXX_', floor: Object.freeze([3, 4, 28]) }),
+ Object.freeze({ prefix: 'CXXABI_', floor: Object.freeze([1, 3, 12]) })
+])
+const FLOOR_LABEL = 'Ubuntu 20.04 (glibc 2.31 / libstdc++ GLIBCXX_3.4.28)'
+
+// Why: the sherpa-onnx speech prebuilt is a third-party manylinux binary that
+// already requires GLIBCXX_3.4.29 (GCC 11 / Ubuntu 21.10+, 22.04 LTS). It loads
+// lazily in the speech worker (src/main/speech/stt-worker.ts), never at app
+// launch, so it cannot cause the #9902 startup crash. Exempt it from the
+// libstdc++ floor (its glibc is still gated) rather than fail the release on a
+// pre-existing, non-launch condition — speech needs libstdc++ >= GCC 11.
+const LIBSTDCXX_FLOOR_EXEMPT = /(?:^|[/\\])sherpa-onnx/
+
+// VER_FLG_WEAK: a version need whose references are all weak. The loader
+// tolerates its absence (resolves to null and the caller's fallback runs)
+// instead of refusing to load, so a weak need must not count as a requirement.
+const VER_FLG_WEAK = 0x2
+
+/** Parse a "2.34" / "3.4.28" version string into a numeric tuple. */
+function parseGlibcVersion(versionStr) {
+ return versionStr.split('.').map((part) => Number.parseInt(part, 10))
+}
+
+/** Compare two numeric version tuples; missing trailing parts are 0. */
+function compareGlibcVersions(a, b) {
+ const length = Math.max(a.length, b.length)
+ for (let i = 0; i < length; i += 1) {
+ const diff = (a[i] ?? 0) - (b[i] ?? 0)
+ if (diff !== 0) {
+ return diff < 0 ? -1 : 1
+ }
+ }
+ return 0
+}
+
+/**
+ * Parse `objdump -p` "Version References" (the ELF `.gnu.version_r` section)
+ * into the version nodes this binary requires from each shared library. This is
+ * the authoritative load-time requirement list: unlike the dynamic symbol table
+ * (`objdump -T`), it also captures symbol-less ABI markers such as
+ * `GLIBC_ABI_DT_RELR` (packed relative relocations, glibc 2.36+) that still
+ * block loading on an older glibc. Each entry: `0xHASH 0xFLAGS `.
+ */
+function parseVersionNeeds(objdumpOutput) {
+ const needs = []
+ let library = null
+ let inSection = false
+ for (const line of objdumpOutput.split('\n')) {
+ if (line.startsWith('Version References:')) {
+ inSection = true
+ continue
+ }
+ if (!inSection) {
+ continue
+ }
+ // Any new non-indented line ends the Version References block.
+ if (!/^\s/.test(line)) {
+ inSection = false
+ continue
+ }
+ const libraryMatch = line.match(/^\s+required from (\S+):/)
+ if (libraryMatch) {
+ library = libraryMatch[1]
+ continue
+ }
+ const entryMatch = line.match(/^\s+0x[0-9a-fA-F]+\s+0x([0-9a-fA-F]+)\s+\d+\s+(\S+)/)
+ if (entryMatch) {
+ const flags = Number.parseInt(entryMatch[1], 16)
+ needs.push({ library, name: entryMatch[2], weak: (flags & VER_FLG_WEAK) !== 0 })
+ }
+ }
+ return needs
+}
+
+/**
+ * Whether a version node is newer than the floor Ubuntu 20.04 provides. Numeric
+ * nodes (`GLIBC_2.34`, `GLIBCXX_3.4.29`) compare by version. Any non-numeric
+ * glibc node is rejected: `GLIBC_ABI_DT_RELR` is a 2.36+ marker, and
+ * `GLIBC_PRIVATE` is not a stable ABI contract — its symbols differ across
+ * glibc releases, so a binary needing one can fail to load on the floor even
+ * though the version node itself exists (a well-formed addon needs neither).
+ * Named libstdc++ nodes (`CXXABI_TM_1`, `GLIBCXX_LDBL_*`) ship on 20.04.
+ * Families we do not gate (`GCC_`, `NSS_`) return false.
+ */
+function isVersionNodeAboveFloor(name) {
+ for (const { prefix, floor } of VERSION_FLOORS) {
+ if (!name.startsWith(prefix)) {
+ continue
+ }
+ const rest = name.slice(prefix.length)
+ if (/^[0-9]+(?:\.[0-9]+)*$/.test(rest)) {
+ return compareGlibcVersions(parseGlibcVersion(rest), floor) > 0
+ }
+ // Non-numeric suffix: reject every glibc node (ABI markers and PRIVATE).
+ return prefix === 'GLIBC_'
+ }
+ return false
+}
+
+function isLibstdcxxNode(name) {
+ return name.startsWith('GLIBCXX_') || name.startsWith('CXXABI_')
+}
+
+/**
+ * Version needs from `filePath` that would prevent loading on the floor OS.
+ * `sherpa-onnx` is exempt from the libstdc++ floor (see LIBSTDCXX_FLOOR_EXEMPT)
+ * but its glibc needs are still checked.
+ */
+function findFloorViolations(needs, filePath = '') {
+ const exemptLibstdcxx = LIBSTDCXX_FLOOR_EXEMPT.test(filePath)
+ return needs.filter(
+ (need) =>
+ !need.weak &&
+ isVersionNodeAboveFloor(need.name) &&
+ !(exemptLibstdcxx && isLibstdcxxNode(need.name))
+ )
+}
+
+// On stock Ubuntu 20.04 (glibc 2.31) these symbols live ONLY in these DSOs —
+// glibc kept openpty/forkpty in libutil until the 2.34 merge. A binary that
+// imports them must keep the DSO in DT_NEEDED or they will not resolve on the
+// floor. This guards config/patches/node-pty@1.1.0.patch's forced
+// `-l:libutil.so.1`: if a toolchain change ever dropped that ldflag, the pinned
+// openpty@GLIBC_2.2.5 would still resolve from libc's compat alias at build time
+// (so the version-floor check passes) yet fail to load on 20.04. libpthread
+// (pthread_sigmask) is intentionally omitted — the Node/Electron host always
+// loads it, so it resolves regardless of this addon's DT_NEEDED.
+const RELOCATED_SYMBOL_PROVIDERS = Object.freeze({
+ openpty: 'libutil.so.1',
+ forkpty: 'libutil.so.1'
+})
+
+/**
+ * Relocated symbols the binary imports whose providing DSO is absent from
+ * DT_NEEDED — meaning they resolve at build time but not on the floor OS.
+ */
+function findMissingProviderDeps(importedSymbols, neededLibraries) {
+ const missing = []
+ for (const [symbol, library] of Object.entries(RELOCATED_SYMBOL_PROVIDERS)) {
+ if (importedSymbols.has(symbol) && !neededLibraries.has(library)) {
+ missing.push({ symbol, library })
+ }
+ }
+ return missing
+}
+
+function isElfFile(filePath) {
+ let fd
+ try {
+ fd = openSync(filePath, 'r')
+ const header = Buffer.alloc(4)
+ const bytesRead = readSync(fd, header, 0, 4, 0)
+ return bytesRead === 4 && header[0] === 0x7f && header.toString('latin1', 1, 4) === 'ELF'
+ } catch {
+ return false
+ } finally {
+ if (fd !== undefined) {
+ closeSync(fd)
+ }
+ }
+}
+
+/** Recursively collect ELF native binaries (`.node`, `.so[.N]`, executables). */
+function collectNativeBinaries(rootDir) {
+ const binaries = []
+ const walk = (dir) => {
+ let entries
+ try {
+ entries = readdirSync(dir, { withFileTypes: true })
+ } catch {
+ return
+ }
+ for (const entry of entries) {
+ const fullPath = join(dir, entry.name)
+ if (entry.isSymbolicLink()) {
+ continue
+ }
+ if (entry.isDirectory()) {
+ walk(fullPath)
+ continue
+ }
+ if (!entry.isFile()) {
+ continue
+ }
+ // Why: .node/.so are always native; extensionless files (the Electron
+ // executable, chrome-sandbox) are checked via the ELF magic so we cover
+ // every launch-critical binary without objdump-ing app.asar or assets.
+ const looksNative = entry.name.endsWith('.node') || /\.so(\.\d+)*$/.test(entry.name)
+ if (looksNative || !entry.name.includes('.')) {
+ if (isElfFile(fullPath)) {
+ binaries.push(fullPath)
+ }
+ }
+ }
+ }
+ walk(rootDir)
+ return binaries.sort()
+}
+
+function resolveObjdump(explicitPath) {
+ const candidates = [explicitPath, 'objdump', 'llvm-objdump'].filter(Boolean)
+ for (const candidate of candidates) {
+ const probe = spawnSync(candidate, ['--version'], { encoding: 'utf8', env: cLocaleEnv() })
+ if (!probe.error && probe.status === 0) {
+ return candidate
+ }
+ }
+ return null
+}
+
+// Why: GNU objdump localizes its section headers ("Version References:") via
+// gettext, and the parser anchors on the English text. Force the C locale so
+// output stays deterministic on non-English packaging hosts (LC_ALL=C also
+// disables LANGUAGE-based message translation).
+function cLocaleEnv() {
+ return { ...process.env, LC_ALL: 'C', LANG: 'C' }
+}
+
+/**
+ * Run objdump with one flag on `filePath`. Fail-closed: a spawn error, non-zero
+ * exit, or signal throws, because a silently-unreadable binary (truncated,
+ * corrupt, or an objdump that cannot decode its format) would let a too-new
+ * binary slip past the gate.
+ */
+function runObjdump(objdumpPath, flag, filePath) {
+ const result = spawnSync(objdumpPath, [flag, filePath], {
+ encoding: 'utf8',
+ maxBuffer: 64 * 1024 * 1024,
+ env: cLocaleEnv()
+ })
+ if (result.error) {
+ throw new Error(
+ `[verify-linux-glibc-floor] could not run objdump on ${filePath}: ${result.error.message}`
+ )
+ }
+ if (result.signal || result.status !== 0) {
+ throw new Error(
+ `[verify-linux-glibc-floor] objdump ${flag} failed for ${filePath} ` +
+ `(status ${result.status}, signal ${result.signal ?? 'none'}): ${(result.stderr || '').trim()}`
+ )
+ }
+ return result.stdout || ''
+}
+
+/** DT_NEEDED shared-library names from `objdump -p` (` NEEDED `). */
+function parseNeededLibraries(objdumpOutput) {
+ const needed = new Set()
+ for (const line of objdumpOutput.split('\n')) {
+ const match = line.match(/^\s+NEEDED\s+(\S+)/)
+ if (match) {
+ needed.add(match[1])
+ }
+ }
+ return needed
+}
+
+/** Undefined (imported) dynamic symbol base names from `objdump -T` (`*UND*`). */
+function parseImportedSymbols(objdumpOutput) {
+ const imported = new Set()
+ for (const line of objdumpOutput.split('\n')) {
+ if (!line.includes('*UND*')) {
+ continue
+ }
+ // The symbol name is the final token; strip any @VERSION suffix.
+ const token = line.trim().split(/\s+/).pop()
+ if (token) {
+ imported.add(token.split('@')[0])
+ }
+ }
+ return imported
+}
+
+/** Version needs + DT_NEEDED from a single `objdump -p` (fail-closed). */
+function readDynamicInfo(filePath, objdumpPath) {
+ const output = runObjdump(objdumpPath, '-p', filePath)
+ return {
+ versionNeeds: parseVersionNeeds(output),
+ neededLibraries: parseNeededLibraries(output)
+ }
+}
+
+/** Imported (undefined) dynamic symbols from `objdump -T` (fail-closed). */
+function readImportedSymbols(filePath, objdumpPath) {
+ return parseImportedSymbols(runObjdump(objdumpPath, '-T', filePath))
+}
+
+/**
+ * Fail Linux packaging if any bundled native binary under `rootDir` requires a
+ * glibc/libstdc++ symbol version newer than the floor OS. No-op is not allowed
+ * on Linux: a missing objdump throws, because a silent skip would defeat the
+ * regression gate on exactly the host where it matters.
+ */
+function verifyLinuxGlibcFloor(rootDir, options = {}) {
+ const binaries = collectNativeBinaries(rootDir)
+ if (binaries.length === 0) {
+ console.log(`[verify-linux-glibc-floor] OK — no bundled native binaries under ${rootDir}`)
+ return
+ }
+
+ // Why: resolve objdump only once there is something to inspect, so a fixture
+ // with no ELF binaries does not fail on a host that lacks binutils.
+ const objdumpPath = resolveObjdump(options.objdumpPath)
+ if (!objdumpPath) {
+ throw new Error(
+ '[verify-linux-glibc-floor] objdump not found. Install binutils on the Linux ' +
+ 'packaging host so the glibc-floor gate can inspect bundled native binaries.'
+ )
+ }
+
+ const offenders = []
+ for (const filePath of binaries) {
+ const { versionNeeds, neededLibraries } = readDynamicInfo(filePath, objdumpPath)
+ const floorViolations = findFloorViolations(versionNeeds, filePath)
+ // Only pay for `objdump -T` when a relocated-symbol provider is not already
+ // in DT_NEEDED (the common, healthy case short-circuits without it).
+ const providerViolations = Object.values(RELOCATED_SYMBOL_PROVIDERS).some(
+ (library) => !neededLibraries.has(library)
+ )
+ ? findMissingProviderDeps(readImportedSymbols(filePath, objdumpPath), neededLibraries)
+ : []
+ if (floorViolations.length > 0 || providerViolations.length > 0) {
+ offenders.push({ filePath, floorViolations, providerViolations })
+ }
+ }
+
+ if (offenders.length > 0) {
+ const detail = offenders
+ .map(({ filePath, floorViolations, providerViolations }) => {
+ const reasons = []
+ if (floorViolations.length > 0) {
+ const nodes = [...new Set(floorViolations.map((v) => v.name))].sort()
+ const libraries = [...new Set(floorViolations.map((v) => v.library).filter(Boolean))]
+ reasons.push(
+ `needs ${nodes.join(', ')}${libraries.length > 0 ? ` (from ${libraries.join(', ')})` : ''}`
+ )
+ }
+ for (const { symbol, library } of providerViolations) {
+ reasons.push(`imports ${symbol} but ${library} is not in DT_NEEDED`)
+ }
+ return ` ${relative(rootDir, filePath) || filePath} ${reasons.join('; ')}`
+ })
+ .join('\n')
+ throw new Error(
+ `[verify-linux-glibc-floor] ${offenders.length} bundled native binar${offenders.length === 1 ? 'y' : 'ies'} ` +
+ `will not load on ${FLOOR_LABEL}, so the app will crash on startup there:\n${detail}\n` +
+ 'See docs/reference/linux-glibc-compatibility.md — rebuild the offending module against an older ' +
+ 'toolchain or pin the relocated symbols (as config/patches/node-pty@1.1.0.patch does).'
+ )
+ }
+
+ console.log(
+ `[verify-linux-glibc-floor] OK — ${binaries.length} bundled native binaries all load on ${FLOOR_LABEL}`
+ )
+}
+
+module.exports = {
+ MIN_GLIBC,
+ VERSION_FLOORS,
+ FLOOR_LABEL,
+ RELOCATED_SYMBOL_PROVIDERS,
+ parseGlibcVersion,
+ compareGlibcVersions,
+ parseVersionNeeds,
+ parseNeededLibraries,
+ parseImportedSymbols,
+ isVersionNodeAboveFloor,
+ isLibstdcxxNode,
+ findFloorViolations,
+ findMissingProviderDeps,
+ collectNativeBinaries,
+ readDynamicInfo,
+ readImportedSymbols,
+ verifyLinuxGlibcFloor
+}
diff --git a/config/scripts/verify-linux-glibc-floor.test.mjs b/config/scripts/verify-linux-glibc-floor.test.mjs
new file mode 100644
index 00000000000..603e4e85c00
--- /dev/null
+++ b/config/scripts/verify-linux-glibc-floor.test.mjs
@@ -0,0 +1,323 @@
+import { mkdtemp, mkdir, writeFile, symlink, rm } from 'node:fs/promises'
+import { createRequire } from 'node:module'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { describe, expect, it } from 'vitest'
+
+const require = createRequire(import.meta.url)
+const {
+ parseGlibcVersion,
+ compareGlibcVersions,
+ parseVersionNeeds,
+ parseNeededLibraries,
+ parseImportedSymbols,
+ isVersionNodeAboveFloor,
+ findFloorViolations,
+ findMissingProviderDeps,
+ collectNativeBinaries,
+ verifyLinuxGlibcFloor
+} = require('./verify-linux-glibc-floor.cjs')
+
+// 0x7f 'E' 'L' 'F' + class/data/version padding — enough for the magic check.
+const ELF_HEADER = Buffer.from([0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00])
+
+// Real `objdump -p` "Version References" shape (entry: 0xHASH 0xFLAGS NAME;
+// flags 0x02 = VER_FLG_WEAK). Includes a symbol-less ABI marker, a weak need,
+// and a libstdc++ need.
+const OBJDUMP_P = [
+ 'Dynamic Section:',
+ ' NEEDED libc.so.6',
+ '',
+ 'Version References:',
+ ' required from libc.so.6:',
+ ' 0x09691a75 0x00 06 GLIBC_2.2.5',
+ ' 0x069691b4 0x00 05 GLIBC_2.34',
+ ' 0x0d696914 0x02 04 GLIBC_2.18',
+ ' 0x00fd0e42 0x00 03 GLIBC_ABI_DT_RELR',
+ ' required from libstdc++.so.6:',
+ ' 0x0b481abc 0x00 07 GLIBCXX_3.4.29',
+ ''
+].join('\n')
+
+describe('verify-linux-glibc-floor parsing', () => {
+ it('parses and compares numeric version tuples', () => {
+ expect(parseGlibcVersion('2.34')).toEqual([2, 34])
+ expect(parseGlibcVersion('3.4.28')).toEqual([3, 4, 28])
+ expect(compareGlibcVersions([2, 2, 5], [2, 14])).toBe(-1)
+ expect(compareGlibcVersions([2, 31], [2, 32])).toBe(-1)
+ expect(compareGlibcVersions([2, 34], [2, 31])).toBe(1)
+ expect(compareGlibcVersions([2, 31], [2, 31])).toBe(0)
+ expect(compareGlibcVersions([2, 31], [2, 31, 0])).toBe(0)
+ expect(compareGlibcVersions([3, 4, 29], [3, 4, 28])).toBe(1)
+ })
+
+ it('parses objdump -p Version References into per-library version needs', () => {
+ const needs = parseVersionNeeds(OBJDUMP_P)
+ expect(needs).toContainEqual({ library: 'libc.so.6', name: 'GLIBC_2.34', weak: false })
+ expect(needs).toContainEqual({ library: 'libc.so.6', name: 'GLIBC_ABI_DT_RELR', weak: false })
+ expect(needs).toContainEqual({ library: 'libc.so.6', name: 'GLIBC_2.18', weak: true })
+ expect(needs).toContainEqual({ library: 'libstdc++.so.6', name: 'GLIBCXX_3.4.29', weak: false })
+ })
+
+ it('classifies version nodes across glibc and libstdc++ families', () => {
+ expect(isVersionNodeAboveFloor('GLIBC_2.34')).toBe(true)
+ expect(isVersionNodeAboveFloor('GLIBC_2.31')).toBe(false)
+ expect(isVersionNodeAboveFloor('GLIBC_ABI_DT_RELR')).toBe(true) // symbol-less marker (2.36+)
+ // GLIBC_PRIVATE is not a stable ABI contract; a needed private symbol can be
+ // absent on the floor even though the version node exists — reject it.
+ expect(isVersionNodeAboveFloor('GLIBC_PRIVATE')).toBe(true)
+ expect(isVersionNodeAboveFloor('CXXABI_TM_1')).toBe(false) // named libstdc++ node on 20.04
+ expect(isVersionNodeAboveFloor('GLIBCXX_3.4.29')).toBe(true) // GCC 11, above 20.04's 3.4.28
+ expect(isVersionNodeAboveFloor('GLIBCXX_3.4.28')).toBe(false)
+ expect(isVersionNodeAboveFloor('CXXABI_1.3.13')).toBe(true)
+ expect(isVersionNodeAboveFloor('CXXABI_1.3.12')).toBe(false)
+ expect(isVersionNodeAboveFloor('GCC_3.0')).toBe(false) // family not gated
+ })
+
+ it('flags strong too-new glibc + libstdc++ needs, skipping weak and ungated families', () => {
+ const violations = findFloorViolations(parseVersionNeeds(OBJDUMP_P), '/opt/app/pty.node')
+ const names = violations.map((v) => v.name).sort()
+ // GLIBC_2.34, GLIBC_ABI_DT_RELR, GLIBCXX_3.4.29 fail; weak GLIBC_2.18 and
+ // GLIBC_2.2.5 are excluded.
+ expect(names).toEqual(['GLIBCXX_3.4.29', 'GLIBC_2.34', 'GLIBC_ABI_DT_RELR'].sort())
+ })
+
+ it('exempts sherpa-onnx from the libstdc++ floor but still gates its glibc', () => {
+ const needs = [
+ { library: 'libstdc++.so.6', name: 'GLIBCXX_3.4.29', weak: false },
+ { library: 'libc.so.6', name: 'GLIBC_2.34', weak: false }
+ ]
+ // A launch-critical module: both are violations.
+ expect(
+ findFloorViolations(needs, '/opt/app/node_modules/node-pty/pty.node').map((v) => v.name)
+ ).toEqual(['GLIBCXX_3.4.29', 'GLIBC_2.34'])
+ // sherpa: GLIBCXX exempt (lazy speech prebuilt), glibc still enforced.
+ expect(
+ findFloorViolations(
+ needs,
+ '/opt/app/node_modules/sherpa-onnx-linux-x64/sherpa-onnx.node'
+ ).map((v) => v.name)
+ ).toEqual(['GLIBC_2.34'])
+ })
+
+ it('reports no violations when every strong need is at or below the floor', () => {
+ const needs = parseVersionNeeds(
+ [
+ 'Version References:',
+ ' required from libc.so.6:',
+ ' 0x00 0x00 02 GLIBC_2.2.5',
+ ' 0x00 0x00 03 GLIBC_2.28',
+ ' required from libstdc++.so.6:',
+ ' 0x00 0x00 04 GLIBCXX_3.4.22'
+ ].join('\n')
+ )
+ expect(findFloorViolations(needs, '/opt/app/pty.node')).toEqual([])
+ })
+})
+
+describe('DT_NEEDED provider check', () => {
+ const OBJDUMP_P_DYNAMIC = [
+ 'Dynamic Section:',
+ ' NEEDED libutil.so.1',
+ ' NEEDED libpthread.so.0',
+ ' NEEDED libc.so.6',
+ '',
+ 'Version References:',
+ ' required from libc.so.6:',
+ ' 0x0 0x00 02 GLIBC_2.2.5'
+ ].join('\n')
+
+ it('parses DT_NEEDED shared libraries from objdump -p', () => {
+ const needed = parseNeededLibraries(OBJDUMP_P_DYNAMIC)
+ expect([...needed].sort()).toEqual(['libc.so.6', 'libpthread.so.0', 'libutil.so.1'])
+ })
+
+ it('parses undefined imported symbols from objdump -T, stripping @VERSION', () => {
+ const output = [
+ '0000000000000000 DF *UND*\t0000000000000000 (GLIBC_2.2.5) openpty',
+ '0000000000000000 w DF *UND*\t0000000000000000 __cxa_finalize@GLIBC_2.2.5',
+ '0000000000000000 DF .text\t0000000000000000 defined_symbol'
+ ].join('\n')
+ const imported = parseImportedSymbols(output)
+ expect(imported.has('openpty')).toBe(true)
+ expect(imported.has('__cxa_finalize')).toBe(true)
+ expect(imported.has('defined_symbol')).toBe(false) // not *UND*
+ })
+
+ it('flags a binary that imports openpty/forkpty without libutil.so.1 in DT_NEEDED', () => {
+ const importsPty = new Set(['openpty', 'forkpty', 'free'])
+ // Missing libutil.so.1 -> the pinned symbols would not resolve on the floor.
+ expect(
+ findMissingProviderDeps(importsPty, new Set(['libc.so.6'])).map((m) => m.symbol)
+ ).toEqual(['openpty', 'forkpty'])
+ // With libutil.so.1 present, no violation.
+ expect(findMissingProviderDeps(importsPty, new Set(['libc.so.6', 'libutil.so.1']))).toEqual([])
+ // A binary that doesn't import the relocated symbols is never flagged.
+ expect(findMissingProviderDeps(new Set(['free']), new Set(['libc.so.6']))).toEqual([])
+ })
+})
+
+describe('collectNativeBinaries', () => {
+ it('collects only ELF .node/.so/executable files, skipping non-ELF and symlinks', async () => {
+ const root = await mkdtemp(join(tmpdir(), 'orca-glibc-collect-'))
+ try {
+ await mkdir(join(root, 'nested'), { recursive: true })
+ await writeFile(join(root, 'addon.node'), ELF_HEADER)
+ await writeFile(join(root, 'nested', 'lib.so'), ELF_HEADER)
+ await writeFile(join(root, 'nested', 'lib.so.1'), ELF_HEADER)
+ await writeFile(join(root, 'orca-ide'), ELF_HEADER) // extensionless executable
+ await writeFile(join(root, 'script.js'), ELF_HEADER) // has extension, not native
+ await writeFile(join(root, 'text.node'), 'not an elf file') // native name, non-ELF
+ await writeFile(join(root, 'notes.md'), ELF_HEADER)
+ try {
+ await symlink(join(root, 'addon.node'), join(root, 'alias.node'))
+ } catch {
+ // Symlink creation can be restricted; the rest of the assertions still hold.
+ }
+
+ const found = collectNativeBinaries(root).map((p) => p.slice(root.length + 1))
+ expect(found).toContain('addon.node')
+ expect(found).toContain(join('nested', 'lib.so'))
+ expect(found).toContain(join('nested', 'lib.so.1'))
+ expect(found).toContain('orca-ide')
+ expect(found).not.toContain('script.js')
+ expect(found).not.toContain('text.node')
+ expect(found).not.toContain('notes.md')
+ expect(found).not.toContain('alias.node')
+ } finally {
+ await rm(root, { recursive: true, force: true })
+ }
+ })
+})
+
+describe.skipIf(process.platform === 'win32')('verifyLinuxGlibcFloor', () => {
+ // A stub objdump keyed on the inspected file's basename. Handles `-p` (Dynamic
+ // Section DT_NEEDED + Version References) and `-T` (undefined symbols).
+ // `*fail*` exits non-zero (fail-closed branch); `*noutil*` omits libutil.so.1
+ // from DT_NEEDED; `*pty*` imports openpty. Match on basename only so the
+ // (random) temp-dir path cannot collide.
+ async function writeStubObjdump(dir) {
+ const stubPath = join(dir, 'objdump-stub.sh')
+ await writeFile(
+ stubPath,
+ [
+ '#!/bin/sh',
+ 'if [ "$1" = "--version" ]; then echo "GNU objdump (stub)"; exit 0; fi',
+ 'f=$(basename "$2")',
+ 'case "$f" in',
+ ' *fail*) echo "objdump: $f: File format not recognized" >&2; exit 1 ;;',
+ 'esac',
+ 'if [ "$1" = "-T" ]; then',
+ ' case "$f" in',
+ ' *pty*) printf "0000 DF *UND* 0000 (GLIBC_2.2.5) openpty\\n" ;;',
+ ' esac',
+ ' exit 0',
+ 'fi',
+ 'printf "Dynamic Section:\\n NEEDED libc.so.6\\n"',
+ 'case "$f" in',
+ ' *noutil*) : ;;',
+ ' *) printf " NEEDED libutil.so.1\\n NEEDED libpthread.so.0\\n" ;;',
+ 'esac',
+ 'printf "\\nVersion References:\\n required from libc.so.6:\\n"',
+ 'case "$f" in',
+ ' *bad*) printf " 0x0 0x00 03 GLIBC_2.34\\n 0x0 0x00 04 GLIBC_2.2.5\\n" ;;',
+ ' *relr*) printf " 0x0 0x00 05 GLIBC_ABI_DT_RELR\\n 0x0 0x00 04 GLIBC_2.2.5\\n" ;;',
+ ' *weakonly*) printf " 0x0 0x02 06 GLIBC_2.32\\n 0x0 0x00 04 GLIBC_2.2.5\\n" ;;',
+ ' *cxx*|*sherpa*)',
+ ' printf " required from libstdc++.so.6:\\n 0x0 0x00 07 GLIBCXX_3.4.29\\n" ;;',
+ ' *) printf " 0x0 0x00 08 GLIBC_2.28\\n 0x0 0x00 04 GLIBC_2.2.5\\n" ;;',
+ 'esac',
+ 'exit 0'
+ ].join('\n'),
+ { mode: 0o755 }
+ )
+ return stubPath
+ }
+
+ it('throws listing binaries over the floor (glibc, DT_RELR marker, and libstdc++)', async () => {
+ const root = await mkdtemp(join(tmpdir(), 'orca-glibc-over-'))
+ try {
+ const objdumpPath = await writeStubObjdump(root)
+ await mkdir(join(root, 'app', 'resources'), { recursive: true })
+ await writeFile(join(root, 'app', 'resources', 'bad-pty.node'), ELF_HEADER)
+ await writeFile(join(root, 'app', 'relr-exe.node'), ELF_HEADER)
+ await writeFile(join(root, 'app', 'cxx-addon.node'), ELF_HEADER) // launch-critical GLIBCXX_3.4.29
+ await writeFile(join(root, 'app', 'good.so'), ELF_HEADER)
+
+ let error
+ try {
+ verifyLinuxGlibcFloor(join(root, 'app'), { objdumpPath })
+ } catch (e) {
+ error = e
+ }
+ expect(error).toBeDefined()
+ expect(error.message).toMatch(/bad-pty\.node needs GLIBC_2\.34/)
+ expect(error.message).toMatch(/relr-exe\.node needs GLIBC_ABI_DT_RELR/)
+ expect(error.message).toMatch(/cxx-addon\.node needs GLIBCXX_3\.4\.29/)
+ } finally {
+ await rm(root, { recursive: true, force: true })
+ }
+ })
+
+ it('throws when a pinned binary imports openpty without libutil.so.1 in DT_NEEDED', async () => {
+ const root = await mkdtemp(join(tmpdir(), 'orca-glibc-noutil-'))
+ try {
+ const objdumpPath = await writeStubObjdump(root)
+ await mkdir(join(root, 'app'), { recursive: true })
+ // Below the version floor (so the version check passes) but libutil.so.1
+ // is missing from DT_NEEDED — openpty would not resolve on Ubuntu 20.04.
+ await writeFile(join(root, 'app', 'noutil-pty.node'), ELF_HEADER)
+
+ expect(() => verifyLinuxGlibcFloor(join(root, 'app'), { objdumpPath })).toThrow(
+ /noutil-pty\.node imports openpty but libutil\.so\.1 is not in DT_NEEDED/
+ )
+ } finally {
+ await rm(root, { recursive: true, force: true })
+ }
+ })
+
+ it('passes weak/at-floor needs and the exempt sherpa-onnx libstdc++ prebuilt', async () => {
+ const root = await mkdtemp(join(tmpdir(), 'orca-glibc-under-'))
+ try {
+ const objdumpPath = await writeStubObjdump(root)
+ const sherpaDir = join(root, 'app', 'node_modules', 'sherpa-onnx-linux-x64')
+ await mkdir(sherpaDir, { recursive: true })
+ await writeFile(join(root, 'app', 'good-pty.node'), ELF_HEADER)
+ await writeFile(join(root, 'app', 'weakonly-lib.so'), ELF_HEADER) // weak GLIBC_2.32 → OK
+ await writeFile(join(root, 'app', 'orca-ide'), ELF_HEADER)
+ await writeFile(join(sherpaDir, 'sherpa-onnx.node'), ELF_HEADER) // GLIBCXX_3.4.29, exempt
+
+ expect(() => verifyLinuxGlibcFloor(join(root, 'app'), { objdumpPath })).not.toThrow()
+ } finally {
+ await rm(root, { recursive: true, force: true })
+ }
+ })
+
+ it('fails closed when objdump cannot read a binary (non-zero exit)', async () => {
+ const root = await mkdtemp(join(tmpdir(), 'orca-glibc-closed-'))
+ try {
+ const objdumpPath = await writeStubObjdump(root)
+ await mkdir(join(root, 'app'), { recursive: true })
+ await writeFile(join(root, 'app', 'unreadable-fail.node'), ELF_HEADER)
+
+ expect(() => verifyLinuxGlibcFloor(join(root, 'app'), { objdumpPath })).toThrow(
+ /objdump -p failed/
+ )
+ } finally {
+ await rm(root, { recursive: true, force: true })
+ }
+ })
+
+ it('is a no-op (no objdump needed) when there are no native binaries', async () => {
+ const root = await mkdtemp(join(tmpdir(), 'orca-glibc-empty-'))
+ try {
+ await mkdir(join(root, 'app'), { recursive: true })
+ await writeFile(join(root, 'app', 'readme.txt'), 'no binaries here')
+ expect(() =>
+ verifyLinuxGlibcFloor(join(root, 'app'), { objdumpPath: '/nonexistent/objdump' })
+ ).not.toThrow()
+ } finally {
+ await rm(root, { recursive: true, force: true })
+ }
+ })
+})
diff --git a/config/tsconfig.cli.json b/config/tsconfig.cli.json
index e8a1f036c10..6b31708de90 100644
--- a/config/tsconfig.cli.json
+++ b/config/tsconfig.cli.json
@@ -3,6 +3,10 @@
"include": [
"../src/cli/**/*",
"../src/shared/**/*",
+ "../src/main/agent-state-file-reader.ts",
+ "../src/main/agent-hooks/agent-hook-file-comparison.ts",
+ "../src/main/agent-hooks/agent-hook-file-limits.ts",
+ "../src/main/agent-hooks/agent-hook-sftp-text-reader.ts",
"../src/main/agent-hooks/hook-stdin-contract.ts",
"../src/main/agent-hooks/hook-config-write-path.ts",
"../src/main/agent-hooks/hooks-json-read.ts",
@@ -19,21 +23,31 @@
"../src/main/codex/codex-app-server-client.ts",
"../src/main/codex/codex-app-server-grant-bridge.ts",
"../src/main/codex/codex-app-server-grant-envelope.ts",
+ "../src/main/codex/codex-app-server-grant-json.ts",
"../src/main/codex/codex-app-server-session.ts",
"../src/main/codex/codex-config-mirror.ts",
"../src/main/codex/codex-config-path-reference-rewrite.ts",
+ "../src/main/codex/codex-config-settings-preservation.ts",
+ "../src/main/codex/codex-config-settings-removal.ts",
+ "../src/main/codex/codex-config-settings-upsert.ts",
"../src/main/codex/codex-home-paths.ts",
+ "../src/main/codex/codex-host-retry-deadlines.ts",
"../src/main/codex/codex-hook-identity.ts",
"../src/main/codex/codex-hook-trust-grant.ts",
"../src/main/codex/codex-managed-trust-reconciliation.ts",
"../src/main/codex/codex-process-exit-deadline.ts",
"../src/main/codex/codex-trust-config-rollback.ts",
+ "../src/main/codex/codex-trust-grant-telemetry.ts",
"../src/main/codex/codex-trust-grant-host.ts",
"../src/main/codex/codex-trust-grant-ledger.ts",
"../src/main/codex/codex-user-hook-trust-rebase-client.ts",
"../src/main/codex/codex-user-hook-trust-rebase.ts",
"../src/main/codex/codex-wsl-hook-install-plan.ts",
+ "../src/main/codex/codex-wsl-reconciliation-generations.ts",
+ "../src/main/codex/config-settings-baseline.ts",
+ "../src/main/codex/config-settings-conflict-resolution.ts",
"../src/main/codex/config-settings-promotion.ts",
+ "../src/main/codex/config-toml-key-path.ts",
"../src/main/codex/config-toml-line-scan.ts",
"../src/main/codex/config-toml-trust.ts",
"../src/main/codex/hook-service.ts",
@@ -41,12 +55,14 @@
"../src/main/codex-accounts/fs-utils.ts",
"../src/main/codex-accounts/wsl-codex-command.ts",
"../src/main/codex-cli/command.ts",
+ "../src/main/codex-cli/nvm-version-directory-discovery.ts",
"../src/main/command-code/command-code-managed-script.ts",
"../src/main/command-code/hook-service.ts",
"../src/main/copilot/hook-service.ts",
"../src/main/cursor/hook-service.ts",
"../src/main/droid/hook-service.ts",
"../src/main/gemini/hook-service.ts",
+ "../src/main/generated-node-bounded-file-reader.ts",
"../src/main/grok/hook-service.ts",
"../src/main/devin/hook-settings.ts",
"../src/main/devin/hook-service.ts",
diff --git a/docs/assets/readme-downloads.svg b/docs/assets/readme-downloads.svg
index 7cc5ece98ff..790c0df3df0 100644
--- a/docs/assets/readme-downloads.svg
+++ b/docs/assets/readme-downloads.svg
@@ -1,5 +1,5 @@
-