mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
* 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.
148 lines
5.3 KiB
TypeScript
148 lines
5.3 KiB
TypeScript
import type { ClaudeRuntimeAuthPreparation } from '../claude-accounts/runtime-auth-service'
|
|
import { applyClaudeEnvPatch } from '../claude-accounts/environment'
|
|
import { readShellStartupEnvVar } from '../pty/shell-startup-env'
|
|
import { parseWslUncPath } from '../../shared/wsl-paths'
|
|
|
|
export type CommitMessageAgentEnvironmentResolvers = {
|
|
prepareForCodexLaunch?: (
|
|
target?: CommitMessageAgentRuntimeTarget
|
|
) => string | null | Promise<string | null>
|
|
prepareForClaudeLaunch?: (
|
|
target?: CommitMessageAgentRuntimeTarget
|
|
) => Promise<ClaudeRuntimeAuthPreparation>
|
|
}
|
|
|
|
export type CommitMessageAgentRuntimeTarget = {
|
|
runtime?: 'host' | 'wsl'
|
|
wslDistro?: string | null
|
|
}
|
|
|
|
function cloneProcessEnv(): Record<string, string> {
|
|
const env: Record<string, string> = {}
|
|
for (const [key, value] of Object.entries(process.env)) {
|
|
if (value !== undefined) {
|
|
env[key] = value
|
|
}
|
|
}
|
|
return env
|
|
}
|
|
|
|
// Why: with system-default real-home routing, the headless Codex commit run
|
|
// must use the user's own ~/.codex. If Orca itself was launched from a nested
|
|
// Orca terminal it can inherit an Orca-owned CODEX_HOME override; strip only
|
|
// that (CODEX_HOME matching the private ORCA_CODEX_HOME marker), preserving a
|
|
// user-set CODEX_HOME.
|
|
function cloneProcessEnvWithoutOrcaCodexHomeOverride(): Record<string, string> {
|
|
const env = cloneProcessEnv()
|
|
if (env.ORCA_CODEX_HOME && env.CODEX_HOME === env.ORCA_CODEX_HOME) {
|
|
delete env.CODEX_HOME
|
|
}
|
|
delete env.ORCA_CODEX_HOME
|
|
return env
|
|
}
|
|
|
|
function readInheritedOrShellEnvVar(name: string, sourceName?: string): string | undefined {
|
|
return (
|
|
(sourceName ? process.env[sourceName] : undefined) ??
|
|
process.env[name] ??
|
|
readShellStartupEnvVar(name, process.env.HOME, process.env.SHELL)
|
|
)
|
|
}
|
|
|
|
function prepareShellConfigDirEnv(agentId: string): { ok: true; env?: NodeJS.ProcessEnv } | null {
|
|
const configVar =
|
|
agentId === 'opencode'
|
|
? 'OPENCODE_CONFIG_DIR'
|
|
: agentId === 'pi' || agentId === 'omp'
|
|
? 'PI_CODING_AGENT_DIR'
|
|
: agentId === 'grok'
|
|
? 'GROK_HOME'
|
|
: null
|
|
if (!configVar) {
|
|
return null
|
|
}
|
|
// Why: each kind owns a distinct ORCA_*_SOURCE_* shadow so a headless commit
|
|
// run from inside a legacy OMP overlay restores the OMP source dir, never
|
|
// the Pi one (and vice versa). PI_CODING_AGENT_DIR is the binary-facing var
|
|
// both kinds consume — see src/main/pi/titlebar-extension-service.ts.
|
|
const sourceVar =
|
|
agentId === 'opencode'
|
|
? 'ORCA_OPENCODE_SOURCE_CONFIG_DIR'
|
|
: agentId === 'pi'
|
|
? 'ORCA_PI_SOURCE_AGENT_DIR'
|
|
: agentId === 'omp'
|
|
? 'ORCA_OMP_SOURCE_AGENT_DIR'
|
|
: undefined
|
|
|
|
const value = readInheritedOrShellEnvVar(configVar, sourceVar)
|
|
if (!value) {
|
|
return { ok: true }
|
|
}
|
|
|
|
// Why: GUI-launched Orca may not inherit shell startup exports, but these
|
|
// vars point the headless CLI at the user's auth/config root. Nested Orca
|
|
// launches inherit PTY overlays, so prefer ORCA_*_SOURCE_* when present.
|
|
return { ok: true, env: { ...cloneProcessEnv(), [configVar]: value } }
|
|
}
|
|
|
|
export async function prepareLocalCommitMessageAgentEnv(
|
|
agentId: string,
|
|
resolvers: CommitMessageAgentEnvironmentResolvers | undefined,
|
|
target?: CommitMessageAgentRuntimeTarget
|
|
): Promise<{ ok: true; env?: NodeJS.ProcessEnv } | { ok: false; error: string }> {
|
|
// Why: a non-null result short-circuits the resolvers below, so any agent added
|
|
// to prepareShellConfigDirEnv must not also need a Codex/Claude-style resolver.
|
|
const shellConfigEnv = target?.runtime === 'wsl' ? null : prepareShellConfigDirEnv(agentId)
|
|
if (shellConfigEnv) {
|
|
return shellConfigEnv
|
|
}
|
|
if (!resolvers) {
|
|
return { ok: true }
|
|
}
|
|
|
|
try {
|
|
if (agentId === 'codex' && resolvers.prepareForCodexLaunch) {
|
|
const codexHomePath = await resolvers.prepareForCodexLaunch(target)
|
|
const wslCodexHome = codexHomePath ? parseWslUncPath(codexHomePath) : null
|
|
if (target?.runtime === 'wsl') {
|
|
const codexHomeForTarget = wslCodexHome?.linuxPath ?? null
|
|
// Why: the fallback must still strip Orca-owned overrides, or a
|
|
// system-default WSL run inherits the managed CODEX_HOME.
|
|
return {
|
|
ok: true,
|
|
env: codexHomeForTarget
|
|
? { ...cloneProcessEnvWithoutOrcaCodexHomeOverride(), CODEX_HOME: codexHomeForTarget }
|
|
: cloneProcessEnvWithoutOrcaCodexHomeOverride()
|
|
}
|
|
}
|
|
if (codexHomePath && wslCodexHome) {
|
|
// Why: this local generation path spawns the host Codex binary. A WSL
|
|
// managed home is only valid when the process is routed through wsl.exe.
|
|
return { ok: true }
|
|
}
|
|
return {
|
|
ok: true,
|
|
env: codexHomePath
|
|
? { ...cloneProcessEnv(), CODEX_HOME: codexHomePath }
|
|
: cloneProcessEnvWithoutOrcaCodexHomeOverride()
|
|
}
|
|
}
|
|
|
|
if (agentId === 'claude' && resolvers.prepareForClaudeLaunch) {
|
|
const preparation = await resolvers.prepareForClaudeLaunch(target)
|
|
const env = applyClaudeEnvPatch(cloneProcessEnv(), preparation.envPatch, {
|
|
stripAuthEnv: preparation.stripAuthEnv
|
|
})
|
|
return { ok: true, env }
|
|
}
|
|
} catch (error) {
|
|
console.error('[commit-message] Failed to prepare agent environment:', error)
|
|
return {
|
|
ok: false,
|
|
error: 'Failed to prepare the selected agent account for commit message generation.'
|
|
}
|
|
}
|
|
|
|
return { ok: true }
|
|
}
|