diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 12b937b187f..2a2980d2427 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "updatedAt": "2026-08-20", + "updatedAt": "2026-08-23", "policy": { "maturityLevels": ["experimental", "soak", "blocking", "accepted-gap", "deprecated"], "blockingPromotion": { @@ -2361,6 +2361,82 @@ ], "demotionRule": "Demote if a reload revision settles more than once, a stale document or competing window can publish, a timer survives settlement, desktop failure stays indefinitely reloading, headless fallback returns after successful promotion, or either isolated Electron journey loses runtime or PTY identity." }, + { + "id": "runtime.headless-renderer-process-containment", + "title": "Headless renderer process death stays isolated from runtime-owned PTYs", + "maturity": "experimental", + "protection": "partial", + "owner": "runtime-platform", + "layer": "electron-process-containment", + "surfaces": ["headless orca serve", "offscreen browser page", "daemon-backed PTYs"], + "platforms": ["macos", "linux", "windows"], + "providers": ["local", "daemon"], + "coveredPlatforms": ["macos"], + "coveredProviders": ["local", "daemon"], + "coverageNotes": "An isolated headless serve host creates two daemon-backed terminal sessions and one offscreen browser renderer. Exact WebContents and OS-process fencing prevents a fault from reaching an unrelated process. The oracle is host-local and does not claim paired-viewer continuity, renderer reload, systemd cgroup restart persistence, SSH or WSL coverage, or aggregate memory policy.", + "motivatingLinks": [ + "https://linear.app/stably/issue/STA-5228", + "https://github.com/stablyai/orca/issues/16084", + "https://github.com/stablyai/orca/pull/16120" + ], + "invariant": "Killing one exact offscreen Chromium renderer cannot replace or terminate the headless main process, runtime, daemon, or unrelated daemon-backed PTYs. The host session inventory retains each original tab-to-PTY binding, and both original PTY processes continue accepting input.", + "oracle": "Start an isolated headless serve runtime with zero initial BrowserWindows, create two daemon-backed terminal fixtures, and record main, runtime, daemon, tab, PTY, fixture-process, WebContents, and renderer-process identities. Fence one marked offscreen page by WebContents, URL, and OS process, force that renderer to crash, and require its original process to die while main/runtime/daemon identities, both host tab-to-PTY bindings, and I/O through both original fixture processes remain unchanged.", + "commands": [ + "pnpm exec electron-vite build --mode e2e", + "SKIP_BUILD=1 pnpm exec playwright test tests/e2e/headless-serve-renderer-kill-retention.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1" + ], + "testFiles": ["tests/e2e/headless-serve-renderer-kill-retention.spec.ts"], + "assertionRefs": [ + { + "file": "tests/e2e/headless-serve-renderer-kill-retention.spec.ts", + "assertions": [ + "the exact original renderer process dies after the fenced WebContents fault", + "main, runtime, and daemon identities remain unchanged and reachable", + "both original host tab-to-PTY bindings remain published", + "both original PTY processes accept post-fault input" + ] + } + ], + "evidenceRuns": [ + { + "date": "2026-08-23", + "runner": "local", + "platform": "macos", + "command": "SKIP_BUILD=1 pnpm exec playwright test tests/e2e/headless-serve-renderer-kill-retention.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", + "result": "passed", + "durationSeconds": 31, + "summary": "The exact offscreen renderer process died while the headless main process, runtime, daemon, both original host tab-to-PTY bindings, and both original PTY processes remained live; both PTYs accepted post-fault input." + } + ], + "runtimeBudget": { + "p95Seconds": 60, + "scope": "one isolated headless Electron host, one offscreen renderer fault, and two daemon-backed PTY fixtures" + }, + "flakeHistory": { + "status": "unknown", + "evidence": "Fresh local macOS runs are green; CI and cross-platform soak history are not yet available." + }, + "redGreenEvidence": { + "status": "partial", + "evidence": "The byte-identical oracle passes on reported v1.4.184 and current main, falsifying renderer-only death as the cause of runtime/session loss. The candidate adds characterization coverage only; reverting it removes the oracle without changing product behavior." + }, + "performanceBudget": { + "required": false, + "evidence": "The change adds test and gate metadata only. Production startup, fault handling, session inventory, and PTY hot paths are unchanged." + }, + "promotionCriteria": [ + "Pass the changed-spec Ubuntu/Xvfb PR lane.", + "Collect at least 100 consecutive CI or soak passes or 14 days without an unexplained flake.", + "Add Windows containment evidence if Electron process behavior diverges." + ], + "knownGaps": [ + "The fault is an exact WebContents renderer crash, not kernel OOM victim selection or systemd cgroup failure.", + "The host-local runtime client proves control-plane reachability but not a separate paired viewer, reconnect, or replay path.", + "The test does not require the crashed browser surface to recover; browser-page recovery is a separate lifecycle contract.", + "SSH, WSL, folder-workspace, multi-client, mixed-version, and whole-service restart journeys are uncollected; no production or wire behavior changes in this gate." + ], + "demotionRule": "Demote if the exact renderer fault changes main/runtime/daemon identity, removes either tab-to-PTY binding, replaces either fixture process, loses post-fault PTY I/O, or cannot identify the killed renderer unambiguously." + }, { "id": "runtime.websocket-heartbeat-cadence", "title": "Runtime WebSockets enter an owned shared heartbeat cadence", diff --git a/tests/e2e/headless-serve-renderer-kill-retention.spec.ts b/tests/e2e/headless-serve-renderer-kill-retention.spec.ts index 5be02793184..819b0078a61 100644 --- a/tests/e2e/headless-serve-renderer-kill-retention.spec.ts +++ b/tests/e2e/headless-serve-renderer-kill-retention.spec.ts @@ -7,6 +7,7 @@ import { expect, test } from './helpers/orca-app' import { createHostCliTerminal, proveSameLivePty, + readHostTerminalInventory, writeRetentionFixture } from './helpers/host-created-terminal-retention-oracle' import { launchHeadlessPairedRuntimeHost } from './helpers/headless-paired-runtime-host' @@ -114,6 +115,9 @@ test('STA-5228 renderer death keeps headless runtime and PTYs alive', async ({ t ) await proveSameLivePty(call, terminalA, 'BEFORE_RENDERER_KILL_A') await proveSameLivePty(call, terminalB, 'BEFORE_RENDERER_KILL_B') + const beforeInventory = await readHostTerminalInventory(call, worktreeId) + expect(beforeInventory.ptyIdByTabId[terminalA.tabId]).toBe(terminalA.ptyId) + expect(beforeInventory.ptyIdByTabId[terminalB.tabId]).toBe(terminalB.ptyId) const pageId = `sta-5228-${Date.now()}` const markerUrl = `data:text/html,${encodeURIComponent( @@ -212,13 +216,11 @@ test('STA-5228 renderer death keeps headless runtime and PTYs alive', async ({ t expect(await processIdentityIsAlive(daemonIdentity)).toBe(true) await proveSameLivePty(call, terminalA, 'AFTER_RENDERER_KILL_A') await proveSameLivePty(call, terminalB, 'AFTER_RENDERER_KILL_B') + const afterInventory = await readHostTerminalInventory(call, worktreeId) + expect(afterInventory.ptyIdByTabId[terminalA.tabId]).toBe(terminalA.ptyId) + expect(afterInventory.ptyIdByTabId[terminalB.tabId]).toBe(terminalB.ptyId) const afterRenderer = await readOffscreenRenderer(launchedHost.app, pageId, false) - expect(afterRenderer).toMatchObject({ - crashed: true, - pid: 0, - webContentsId: renderer.webContentsId - }) console.log('[STA-5228] renderer lifecycle snapshot', { afterRenderer, beforeRenderer: renderer,