fix(codex): a CLI that cannot execute no longer reports CODEX_MODE: ready

Follow-up to #2477. The model probe it added does a real round trip, but its
final branch is the `else` of a "model 400" grep, so it swallowed spawn ENOENT,
non-executable binaries and missing vendor payloads alongside genuine network
timeouts. All three are deterministic — retrying never helps — yet they landed
in the fail-open bucket and resolved to `ready`, so every Codex pass was
skipped in silence and the review reported itself complete.

Observed live: @openai/codex was on PATH with an empty
vendor/aarch64-apple-darwin/codex/ directory. gstack said `ready` for two
months while no Codex pass ran.

Three changes:

- `_gstack_codex_model_probe` classifies deterministic install failures (exit
  126/127, or stderr matching ENOENT/ENOEXEC/EACCES/"cannot execute binary
  file") as MODEL_UNUSABLE_INSTALL, exit 2, never cached — a reinstall is
  picked up on the next probe. Exit 124 and genuine transients still fail open,
  which is what #2477 intended.

- The preflight chain captures the probe's code instead of testing it for
  truthiness, so exit 2 routes to a new `broken_install` mode whose remedy is
  `npm install -g @openai/codex` rather than "check your model pin". A missing
  binary and an unusable model are different problems with different fixes.

- `_gstack_codex_version_check` no longer reads a broken CLI as healthy. It ran
  `codex --version 2>/dev/null | head -1`, which captures head's status, not
  codex's — and 2>/dev/null discarded the one diagnostic available. It now
  captures the real exit code and warns on non-zero. Empty-but-successful
  output stays silent, per the existing "empty output → OK" case.

Tests: 6 added to test/codex-hardening.test.ts covering both broken-install
shapes, the exit-2 contract, no caching, the transient still failing open, the
model 400 still classifying as MODEL_UNUSABLE, and the version-check warning.
845 pass / 0 fail across all 8 suites touching the changed files.

Closes #2742

Wave-amended: autoplan hand-maintained preflight chain completed (tmpl+render); install-signature grep gated on failed spawn only; goldens regenerated against the wave tree (author's golden commit 5797d326 superseded); +2 tests
This commit is contained in:
Udhdhav kheni
2026-08-31 20:52:20 +00:00
committed by Garry Tan
parent 567f06217a
commit 4dc3516bcc
12 changed files with 316 additions and 36 deletions
+14 -5
View File
@@ -793,12 +793,21 @@ elif ! _gstack_codex_auth_probe >/dev/null; then
# Round-trip model probe (#2477): auth can pass while the account's configured
# model is rejected with an HTTP 400 (stale `model =` pin in ~/.codex/config.toml).
# ~10s on first run, cached 1h; timeouts fail open (probe returns 0).
elif ! _gstack_codex_model_probe; then
echo "[codex-unavailable: configured model rejected] — proceeding with Claude subagent only. Fix the \`model =\` pin in ~/.codex/config.toml (see [notice.model_migrations] there for the replacement)."
_CODEX_AVAILABLE=false
# Exit 2 = broken install (#2742: spawn ENOENT / non-executable binary /
# missing vendor payload) — a different problem with a different fix, so
# capture the code instead of testing truthiness.
else
_gstack_codex_version_check # non-blocking warn if known-bad
_CODEX_AVAILABLE=true
_gstack_codex_model_probe; _CODEX_MP=$?
if [ "$_CODEX_MP" -eq 2 ]; then
echo "[codex-unavailable: binary cannot run] — proceeding with Claude subagent only. Reinstall: \`npm install -g @openai/codex\` (#2742)."
_CODEX_AVAILABLE=false
elif [ "$_CODEX_MP" -ne 0 ]; then
echo "[codex-unavailable: configured model rejected] — proceeding with Claude subagent only. Fix the \`model =\` pin in ~/.codex/config.toml (see [notice.model_migrations] there for the replacement)."
_CODEX_AVAILABLE=false
else
_gstack_codex_version_check # non-blocking warn if known-bad
_CODEX_AVAILABLE=true
fi
fi
```
+14 -5
View File
@@ -278,12 +278,21 @@ elif ! _gstack_codex_auth_probe >/dev/null; then
# Round-trip model probe (#2477): auth can pass while the account's configured
# model is rejected with an HTTP 400 (stale `model =` pin in ~/.codex/config.toml).
# ~10s on first run, cached 1h; timeouts fail open (probe returns 0).
elif ! _gstack_codex_model_probe; then
echo "[codex-unavailable: configured model rejected] — proceeding with Claude subagent only. Fix the \`model =\` pin in ~/.codex/config.toml (see [notice.model_migrations] there for the replacement)."
_CODEX_AVAILABLE=false
# Exit 2 = broken install (#2742: spawn ENOENT / non-executable binary /
# missing vendor payload) — a different problem with a different fix, so
# capture the code instead of testing truthiness.
else
_gstack_codex_version_check # non-blocking warn if known-bad
_CODEX_AVAILABLE=true
_gstack_codex_model_probe; _CODEX_MP=$?
if [ "$_CODEX_MP" -eq 2 ]; then
echo "[codex-unavailable: binary cannot run] — proceeding with Claude subagent only. Reinstall: \`npm install -g @openai/codex\` (#2742)."
_CODEX_AVAILABLE=false
elif [ "$_CODEX_MP" -ne 0 ]; then
echo "[codex-unavailable: configured model rejected] — proceeding with Claude subagent only. Fix the \`model =\` pin in ~/.codex/config.toml (see [notice.model_migrations] there for the replacement)."
_CODEX_AVAILABLE=false
else
_gstack_codex_version_check # non-blocking warn if known-bad
_CODEX_AVAILABLE=true
fi
fi
```