fix(codex): keep mock CLIs shadowed in bash login shells#26
Conversation
|
Claude finished @dmorosanu's task in 1m 22s —— View job Code Review in Progress
|
uipreliga
left a comment
There was a problem hiding this comment.
Review: coder_eval — pr:26
Scope: pr:26 · branch fix/codex-harness-shim-exec · b802957 · 2026-07-16T19:04Z · workflow variant
Change class: complex — introduces login-shell HOME shimming (temp-dir lifecycle, generated shell-profile files, PATH re-prepend) whose correctness depends on shell sourcing order, $HOME semantics, and shlex quoting
coder_eval is in exceptional health — a perfect 10/10 with clean type safety, tests, security, architecture, and harness scoring, and the only blemishes are two low-severity maintainability nits in the Codex agent that pose no correctness, security, or scoring risk, so the bottom line is ship-as-is while tidying the two codex_agent.py items opportunistically.
Summary
| Axis | Score | 🔴 | 🟠 | 🟡 | 🔵 | Top Issue |
|---|---|---|---|---|---|---|
| 1. Code Quality & Style | 9.9 / 10 | 0 | 0 | 0 | 1 | Implicit magic-string coupling between _LOGIN_PROFILE_NAMES tuple and the ".bash_profile" literal branch |
| 2. Type Safety | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 3. Test Health | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 4. Security | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 5. Architecture & Design | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 6. Error Handling & Resilience | 10 / 10 | 0 | 0 | 0 | 0 | — |
| 7. API Surface & Maintainability | 9.9 / 10 | 0 | 0 | 0 | 1 | _build_codex_env docstring omits the HOME/CODEX_HOME override it now performs |
| 8. Evaluation Harness Quality | 10 / 10 | 0 | 0 | 0 | 0 | — |
Overall Score: 10 / 10 · Weakest Axis: Code Quality & Style at 9.9 / 10
Totals: 🔴 0 · 🟠 0 · 🟡 0 · 🔵 2 across 8 axes.
Blockers
None.
Non-blocking, but please consider before merge
None.
Nits
- [Axis 1] Implicit magic-string coupling between _LOGIN_PROFILE_NAMES tuple and the ".bash_profile" literal branch (
src/coder_eval/agents/codex_agent.py:1168) — The set of generated profile files is declared once as_LOGIN_PROFILE_NAMES = (".bash_profile", ".profile")(line 223) and driven through the loopfor name in _LOGIN_PROFILE_NAMES:(line 1135), but_login_profile_contentdistinguishes the two kinds by re-matching the literal string:if profile_name == ".bash_profile":(line 1168), with everything else falling into the sh/.profileelse. This is a second source of truth: adding e.g..zprofileto the tuple would silently route it through the POSIX.profilebranch (which sources only.profile, not the bash first-found chain), with no error. Minor for two files, but the bash-vs-POSIX distinction would be clearer as an explicit flag/parameter (e.g. passis_bash=name == ".bash_profile"at the call site, or key a small mapping) rather than re-deriving it from the filename inside the content builder. - [Axis 7] _build_codex_env docstring omits the HOME/CODEX_HOME override it now performs (
src/coder_eval/agents/codex_agent.py:1063) — The method-level docstring at lines 1063-1074 still enumerates only the two env vars the method used to set: it says the env "Carries the API key (CODEX_API_KEY...) and, when the sandbox resolvedmock_path_dirs, a PATH with those directories prepended..." — but the method now also injectsenv["HOME"] = str(self._login_shell_home)andenv["CODEX_HOME"] = str(self._codex_home())(lines 1090-1091). Overriding HOME is the single most surprising side effect in this diff: it applies to the whole Codex app-server process (not just thebash -lcshells the design targets), which is a maintainability/discoverability trap for a future reader who reads the docstring as the method's contract. The behavior IS explained by the inline comment at lines 1086-1089, but the docstring — the first thing a maintainer reads — is now stale/incomplete. Add one sentence to the docstring noting that HOME is repointed at the per-task login-shell profile dir (with CODEX_HOME pinned back to the real codex state dir so auth/rollouts are unaffected), cross-referencing_setup_login_shell_home.
What's Missing
Parallel paths:
- 🟡 The login-shell PATH-restore guard lives only in CodexAgent, but ClaudeCodeAgent (claude_code_agent.py:706,1062) and antigravity_agent (antigravity_agent.py:314,378-396) consume the SAME
env_path_prependmock-PATH prepend the sandbox hands every agent. If either spawns a login shell (bash -lcor any-l), the identical/etc/profilePATH reset drops the mock prepend and bare commands resolve to the REAL CLIs (real-tenant contamination) with no error. The PR should state that only Codex issues login shells, or the guard belongs on the shared Agent base rather than one agent. (trigger: src/coder_eval/agents/codex_agent.py)
Daily/nightly:
- 🟡 The fix exists for the containerized nightly Codex path (mock CLIs shadowing real-tenant CLIs) and hard-codes container-shell assumptions: that
/etc/profileresets PATH and that~/.bash_profileis sourced AFTER it. If the nightly base image's/etc/profileor default login shell differs, the shim silently no-ops and contamination returns with no failure signal. The PR does not state whether the nightly container image must be rebuilt or validated against these assumptions before the fix is relied on. (trigger: src/coder_eval/agents/codex_agent.py) - 🔵
_build_codex_envrepoints HOME for the ENTIRE codex app-server process in the nightly container, not just thebash -lcshells the design targets; any$HOME-relative behavior of the codex binary itself now resolves to the ephemeral temp dir (only CODEX_HOME is pinned back). The whole-process blast radius of this override is undocumented and shares root cause with the Axis 7 docstring-omission finding. (trigger: src/coder_eval/agents/codex_agent.py) _(restates: Axis 7: build_codex_env docstring omits the HOME/CODEX_HOME override)
Tests:
- 🔵 The only real-shell end-to-end test (
test_login_shell_restores_mock_prepend_end_to_end) exercisesbash -lcexclusively; the generated.profiletwin (read bysh/dashlogin shells) is asserted at file-content level only and never executed by a realsh/dashlogin shell, so a sourcing bug or accidental bashism specific to the sh path would pass unit tests. (trigger: tests/test_codex_agent.py)
Harness & Lint Improvements
Static checks (lint / type):
- (none)
Harness improvements (not statically reachable):
- Add a parametrized unit test over
_LOGIN_PROFILE_NAMESintests/that, for every entry in the tuple, invokes_login_profile_content(name)and asserts the generated content matches that name's expected shell semantics — the bash first-found chain (.bash_profile/.bashrc) vs. the POSIX.profilesourcing — rather than asserting only that content is non-empty. This makes the tuple↔content-builder coupling self-checking: adding e.g..zprofileto_LOGIN_PROFILE_NAMESwithout teaching_login_profile_contentabout it (so it silently falls into the POSIXelsebranch) trips a failing assertion instead of shipping wrong login-shell wiring. Ideally the test also asserts_login_profile_contenthas an explicit branch/mapping entry for each name (no unhandled name falls through to a generic default). Why not static: The correct content for a given profile filename is a semantic property of shell login-file sourcing behavior (which chain a shell reads, in what order), not a grep-/AST-detectable pattern. A lint rule can't judge whether theelsebranch is right for a newly added filename; only executing the content builder and checking the emitted script against expected sourcing semantics can. And the underlying 'literal re-derived instead of parameterized' shape is exactly the kind of second-source-of-truth design judgment a mechanical rule would either miss or over-flag. Prevents: Finding 1 (implicit magic-string coupling between_LOGIN_PROFILE_NAMESand the.bash_profileliteral branch in_login_profile_content) — the silent mis-routing of a future profile name through the wrong shell branch.
Top 5 Priority Actions
- None of the confirmed findings can change a task's score or final_status for identical agent output, so there are no scoring-integrity fixes to front-load — proceed in pure impact order below.
- Remove the second source of truth in src/coder_eval/agents/codex_agent.py:1168 by passing an explicit
is_bashflag (or keying a small mapping) from the_LOGIN_PROFILE_NAMESloop into_login_profile_content, so adding a profile like.zprofilecan't silently fall through the POSIX.profilebranch. - Update the
_build_codex_envdocstring at src/coder_eval/agents/codex_agent.py:1063 to document the new HOME/CODEX_HOME repointing side effect (HOME → per-task login-shell profile dir, CODEX_HOME pinned to the real state dir), cross-referencing_setup_login_shell_home, since it is the most surprising behavior in the diff and the docstring is a maintainer's first contract. - Convert the magic-string coupling into permanent enforcement by adding a custom lint rule (tests/lint/rules/, CE-series) that flags re-deriving branch behavior from a literal already enumerated in a canonical tuple, per CLAUDE.md's 'could a lint rule have prevented this?' guidance.
- Hold the line on the perfect axes by keeping the merge-unification, reconciliation-invariant, and classification-aggregate test guards green as the Codex/login-shell surface evolves, treating any new drift between docstrings and behavior as a regression.
Stats: 0 🔴 · 0 🟠 · 0 🟡 · 2 🔵 across 8 axes reviewed.
bai-uipath
left a comment
There was a problem hiding this comment.
Approve — correct, well-contained, and now validated end-to-end in the real nightly container. Notes below are optional.
What we verified
- Unit + CI: all 15 login-shell tests pass (including the real-
bash -lce2e that simulates the/etc/profilereset), and CI's "Live Integration Tests (Codex)" is green on Linux. - Real-environment e2e: ran the actual PR-branch
CodexAgentinsideskills-image:latest— the container the nightly/weekly codex runs actually use — against its real/etc/profile, with and without the fix.
Findings
- ✅ The bug reproduces in-container. In
skills-image, a login shell resets PATH to the literal/usr/local/sbin:…:/binand drops the mock-CLI prepend, so a bareuipresolves to the baked-in real/usr/bin/uip. That is exactly the real-tenant contamination the PR targets, confirmed in the true environment (macOS couldn't reproduce it — zsh + non-destructivepath_helper; the VM host couldn't either — its profile augments rather than resets PATH). - ✅ The fix restores the prepend. With
_setup_login_shell_home+_build_codex_envcomposed the way the SDK merges the env overos.environ, a bareuipresolves to the mock and$HOMEis correctly restored to/root. Together with the unit test that assertsstart()folds HOME/CODEX_HOME/PATH into theCodexConfigenv, the full chain is covered. - ✅ Blast radius is contained. The HOME/CODEX_HOME redirect only activates when
_env_path_prependis set (mocked tasks) on POSIX — non-mocked codex tasks and the claude/antigravity agents are untouched (codex_agent.py:1085). - ✅
CODEX_HOMEpin is right. Redirecting HOME would otherwise move codex auth/rollout state into the temp dir; pinning it to_codex_home()(resolved in the parent's real env) keeps auth and sub-agent rollout recovery working.

Problem
Codex issues every shell command as
/bin/bash -lc. The login shell re-sources the system profile chain, which resets PATH and silently drops theenv_path_prependmock-CLI prepend - souipresolves to the real CLI and mocked eval tasks investigate the real tenant instead of the fixtures. In the 2026-07-15 gpt-5.4 weekly run, 5 of 16 uipath-troubleshoot failures were this contamination (transcripts cite real-tenant jobs absent from all fixtures, while the mocked evidence was never served).Fix
When mocks are configured (POSIX only),
CodexAgent.start()generates a per-task HOME containing a.bash_profile/.profilethat:$HOME-relative sourcing keep working),The app-server env points
HOMEat the generated dir and pinsCODEX_HOMEto the real codex state dir (auth and sub-agent rollout recovery unaffected). Cleaned up onstop()/kill_sync(), exception-safe, rerunnable acrossstart()retries.Known residual gap: a nested login shell inside a command re-reads the real profiles and loses the prepend again (documented in the docstring).
Validation
bash -lc+ the real skills-repo mock dispatcher in WSL Ubuntu 24.04: pre-fix resolves the real CLI, post-fix returns the scenario's manifest fixture.$HOMErestored, original$HOME/.bashrcsourced - even when the original profile resets PATH.