The pairing ratchet matched spawn|spawnProcess|spawnSync|runProcess only, so
a resolved CLI handed to execFile was the same unpaired launch with none of
the enforcement. codex-trust-grant-host.ts resolves codex and calls
execFileSync, and escaped the ratchet purely through that omission.
Widen to the exec/fork family. The negative lookbehind keeps method calls
such as `RE.exec(` out, which is what made the bare `exec` name safe to
include; a fixture mutation confirms `/x/.exec('x')` does not trip it, and
adding execFileSync(resolvedCli) to a paired file does.
codex-trust-grant-host is allowlisted rather than changed: its only exec is
a wsl.exe identity probe for the binary stamp, and its actual codex launch
is a CodexAppServerInvocation paired centrally in codex-app-server-session.
The entry records what would invalidate it.
Follow-up to #16365, which paired 8 spawn sites by hand. Hand-pairing is how
the class got introduced, so close it structurally instead.
cliPath is now required on CodexAppServerInvocation, `null` only for the
guest-side wsl.exe launcher where a host path pairs nothing. Optional let a
native builder omit it and silently fall back to pairing against a cmd.exe
wrapper with no type error. Every production site already passed it; only
test fixtures needed updating, which is the type doing its job.
Four more sites now pair. codex-state-db-backfill-recovery spawns the same
`codex app-server` subcommand #16365 fixed elsewhere. cli/handlers/account
was the worst case: addAgentNodePaths prepends the *newest* version-manager
bin, which is not necessarily where the CLI being launched lives, so it
actively created the mismatch — pairing now runs last so the CLI's own node
wins. commit-message-text-generation and skills/skill-update-run spawn
resolved binaries with inherited env.
cli/handlers/skills had grown its own buildNpxPath: a weaker local copy that
prepended unconditionally, ignored the Windows `Path` key, and special-cased
a '.' dirname. Deleted in favor of the shared helper, which checks the
sibling node actually exists — the behavior change one test had pinned.
The ratchet is the point: any file that resolves a CLI and spawns must
reference withCliRuntimeOnPath, with a shrink-only allowlist. It caught
skill-update-run, which I had missed. Its first draft required a call paren
and so let dependency-injected resolvers (`resolveCommand: resolveCodexCommand`)
through — verified by removing a pairing and watching it stay green, then
widened until it failed. A second assertion fails on a stale allowlist entry
so an exemption cannot outlive its reason.
external-editor-launch stays allowlisted: it launches a GUI editor, not a
Node CLI whose ABI matters.