Files
orca/src/main/codex/codex-trust-grant-telemetry.ts
T
Neil 26721bd632 fix(codex): stop blocking the main thread on trust grants (#16441) (#16594)
* fix(codex): stop blocking the main thread on trust grants (#16441)

Codex hook trust was granted by blocking the Electron main thread on
`spawnSync` of a bundled ELECTRON_RUN_AS_NODE entry for the whole
app-server deadline: 15s native, 35s WSL, ~45s on the real-home path
(rebase inspect + repair + grant). Cold start and every Codex pane
launch showed "Not Responding"; the reported event-loop gap was
15,049 ms.

The subprocess only ever existed to donate an event loop to a
deliberately blocked parent — `runCodexHookTrustGrantSession` was
already the real async implementation. Make the callers async and the
fork is unnecessary, so the bridge, the forked entry and its envelope
are deleted along with their build/knip/tsconfig registrations. The CLI
`agent hooks prepare-codex` handler is already async, so it awaits the
in-process session and saves a process spawn per managed-home shell.

`resolveCodexTrustGrantHost` is async too; the WSL identity probe moves
from `execFileSync` to `runProcess`, dropping that file from the
child-process import allowlist. Status reads keep a synchronous
native-only stamp path.

Two invariants that held only because the lane blocked:

- Overlapping capability probes were impossible by construction.
  `GitCapabilityCache`'s dedupe engine is extracted to a shared
  `CapabilityProbeCache` and `CodexAppServerCapabilityCache` now
  inherits it, so concurrent launches against a cold host share one
  app-server session instead of one each.
- Two grants on one `config.toml` could not interleave capture and
  restore. A reentrant per-file lane now serializes the whole install
  sequence (managed, WSL runtime, real-home ensure, legacy sweep) and
  the grant and rebase inside it.

Cold-start work moves off the critical path: retained-home
reconciliation (N sequential sessions) is fire-and-forget behind the
daemon provider, and the startup real-home ensure chains into managed
hook reconciliation instead of blocking app init.

Every preserved semantic is unchanged: never throws, the
ORCA_DISABLE_CODEX_TRUST_RPC kill switch, ledger hits, backfill-pending
and cooldown fallbacks, config rollback on every failure path,
pre-grant self-computed trust removal, the verify-failure taxonomy,
diagnostics and telemetry.

* fix(codex): widen the trust-config lane to every config.toml writer

Review follow-ups on #16441's async trust grant:

- `markCodexProjectTrusted` now runs inside the runtime+system config.toml
  lanes, so a project-trust write can no longer land inside a hook grant's
  capture->restore window and be silently reverted. Its callers await it.
- `install`/`refreshRuntimeUserHooks`/`remove` hold the system config.toml
  lane as well as the runtime one — they promote approvals into
  ~/.codex/config.toml and mirror it back. Lock order is runtime-before-system
  everywhere.
- The real-home ensure chain resumes after a rejection instead of returning
  the same rejected promise to every later pane launch, and resolving the real
  home is now inside the module's never-throws boundary.
- `buildSpawnEnv` awaits inside a cancelable pending-spawn registration, so
  shutdown during the (now long) env build stops the PTY from launching.
  `prepareLocalPtySpawn` generalizes into `awaitCancelableLocalPtySpawn`.
- CapabilityProbeCache drops the test-only `nowMs` passthrough; its probe
  backstop comment now describes what it actually guards.
- Preflight is a plain async function; the trust dispatch in orca-runtime
  collapses into one `markWorkspaceTrustedForAgent`.

* test(codex): exercise the trust-config lane under real concurrency

The async grant makes two pane launches overlap for the first time. These
drive the real modules end to end on real files: a rollback swallowing a
sibling's grant, a markCodexProjectTrusted write landing inside a capture
-> restore window, shared capability-probe dedupe on a cold host, the
host-scoped transient cooldown, and reentrancy from inside an installer.

Each was verified to fail against a deliberately broken implementation
(lane removed, dedupe disabled, cooldown made global, reentrancy pass-
through disabled).

* test(codex): stop hook-service suites spawning the developer's real codex

The forked grant bundle never existed under vitest, so the RPC lane was
unreachable in tests on main. Running it in-process makes these suites
spawn a real `codex app-server` when one is installed: 38 spawns and two
failures in hook-service-runtime-trust-repair on a machine with codex,
green in CI where there is none. Stand in for the missing binary so both
environments exercise the same fallback lane.

* docs(codex): scope the trust-RPC kill switch comment to what it actually gates

The comment read as though the flag forces the fallback lane everywhere. It
gates the managed grant only: the real-home rebase still runs its own
inspect/repair app-server sessions when Orca's insertion shifts a user's hook
positions, and never reads the flag.

Verified by exercise, not by reading — with the flag set, both
inspect-user-hook-trust and repair-user-hook-trust still ran. Pre-existing:
main has no check there either, it just blocked the main thread while doing it.

Widening the flag to cover the rebase is a follow-up; this only stops the
comment promising something the constant does not do.
2026-08-26 16:44:55 -07:00

91 lines
3.0 KiB
TypeScript

import type { CodexTrustGrantSessionVerifyClass } from './codex-app-server-client'
import { WSL_CODEX_NOT_FOUND_MESSAGE } from '../codex-accounts/wsl-codex-command'
/** Which install surface asked for the grant: the system-default real ~/.codex
* or a managed (mirror/per-account) home. Telemetry attribution only — the
* grant behaves identically; host_kind alone cannot distinguish the lanes
* (native hosts grant for both surfaces). */
export type CodexTrustGrantTelemetryLane = 'real-home' | 'managed'
export type CodexTrustGrantFallbackReason =
| 'disabled'
| 'no-managed-entries'
| 'unsupported'
| 'unsupported-cached'
| 'verify-failed'
| 'retry-cached'
| 'error'
/** Closed classification of `reason: 'error'` fallbacks. Only timeout and
* unsupported carry a stable error name, so the rest are matched on the
* bounded message shapes each layer produces — never forwarded raw. */
export type CodexTrustGrantErrorClass =
| 'binary-missing'
| 'timeout'
| 'entry-failed'
| 'early-exit'
| 'rpc-failed'
| 'unexpected'
export type CodexTrustGrantVerifyClass =
| CodexTrustGrantSessionVerifyClass
| 'unexpected-key'
| 'duplicate-key'
| 'coverage'
export function classifyCodexTrustGrantError(error: unknown): CodexTrustGrantErrorClass {
if (!(error instanceof Error)) {
return 'unexpected'
}
if (error.name === 'CodexAppServerTimeoutError') {
return 'timeout'
}
const message = error.message
if (message.includes('codex trust-grant entry')) {
return 'entry-failed'
}
if (
/^spawn (?:.*[\\/])?codex(?:\.(?:cmd|exe|bat))? ENOENT$/.test(message) ||
message.includes(WSL_CODEX_NOT_FOUND_MESSAGE)
) {
return 'binary-missing'
}
if (message.includes('exited before completing the session')) {
return 'early-exit'
}
if (/codex app-server \S+ failed:/.test(message)) {
return 'rpc-failed'
}
return 'unexpected'
}
export type CodexTrustGrantTelemetryEvent = {
outcome: 'granted' | 'fallback' | 'verify_failed'
hostKind: 'native' | 'wsl'
lane: CodexTrustGrantTelemetryLane
reason?: CodexTrustGrantFallbackReason
errorClass?: CodexTrustGrantErrorClass
verifyClass?: CodexTrustGrantVerifyClass
}
type CodexTrustGrantTelemetry = (event: CodexTrustGrantTelemetryEvent) => void
// Why: hook-service is bundled into plain-node CLI entries where electron
// (and therefore the telemetry client) cannot load; the Electron main process
// injects the tracker at startup instead of a static import.
let telemetry: CodexTrustGrantTelemetry = () => {}
export function setCodexTrustGrantTelemetry(tracker: CodexTrustGrantTelemetry): void {
telemetry = tracker
}
export function emitCodexTrustGrantTelemetry(event: CodexTrustGrantTelemetryEvent): void {
try {
telemetry(event)
} catch (error) {
// Why: observability must never turn a verified grant into fallback or
// violate the launch-prep no-throw contract of the grant lane.
console.warn('[codex-trust-grant] failed to emit telemetry', error)
}
}