Skip to content

fix(agent): make free Codex usable end-to-end (ENG-4785)#265

Merged
LarsenCundric merged 2 commits into
mainfrom
larsen/eng-4785-codex-fixes
May 26, 2026
Merged

fix(agent): make free Codex usable end-to-end (ENG-4785)#265
LarsenCundric merged 2 commits into
mainfrom
larsen/eng-4785-codex-fixes

Conversation

@LarsenCundric
Copy link
Copy Markdown
Contributor

@LarsenCundric LarsenCundric commented May 26, 2026

Why

Staging dogfood of the free DeepSeek Codex path hit three bugs. Fixes all three.

Fixes

1. wire_api = "chat" rejected by Codex (codex#7782, removed Feb 2026). The config now writes wire_api = "responses"; Codex hits {base_url}/responses, which the CP proxy (cloud #4578) forwards to OpenRouter's drop-in Responses API.

2. Web terminal opened claude, not codex. bux-ttyd.service hardcoded /usr/bin/claude, so a codex-only (free) box launched Claude and hit "Not logged in". New bux-agent-shell launcher picks codex (free-DeepSeek profile active, or codex signed in) vs claude; ttyd runs it; symlinked in bootstrap.

3. Telegram showed "Not signed in. Pick an agent." Both signed-in checks (_is_agent_authed and _login_status_cached) shelled out to codex login status, which the free path never passes (no codex login — auth is the profile + bu-cp-token). Added _codex_free_profile_active() so an active browser-use-free profile counts as codex-authed in both checks.

Pairs with

cloud #4578 (/v1/responses proxy route).

Gates

ruff check agent/ clean; bash -n on bootstrap + launcher valid; free-profile detection unit-tested (top-level vs in-table profile).


Summary by cubic

Makes the free DeepSeek Codex path usable end-to-end (ENG-4785). Fixes API routing, terminal agent selection, and Telegram auth detection so free Codex works without a login.

  • Bug Fixes
    • Config writes wire_api = "responses" so Codex hits /responses; CP proxy forwards to the OpenRouter Responses API (cloud #4578).
    • Web terminal now launches the right agent via bux-agent-shell (prefers Codex when top-level profile = "browser-use-free" is active or Codex is signed in); bux-ttyd.service updated to use it. Profile detection is exact and ignores profile_dir/profiles and indented table headers.
    • Telegram treats an active browser-use-free profile as “signed in” for Codex; added _codex_free_profile_active() and updated both auth checks to use it.

Written for commit c1816b8. Summary will update on new commits. Review in cubic

Three bugs found on the staging dogfood:

1. wire_api: Codex rejects "chat" (removed Feb 2026) -> config now writes
   wire_api = "responses"; Codex hits {base_url}/responses, which the CP proxy
   forwards to OpenRouter's drop-in Responses API.

2. Terminal opened claude, not codex: bux-ttyd hardcoded /usr/bin/claude.
   New bux-agent-shell launcher picks codex (free-DeepSeek profile active, or
   signed in) vs claude, and ttyd now runs it. Symlinked in bootstrap.

3. TG showed 'Not signed in': both signed-in checks (_is_agent_authed and
   _login_status_cached) shelled out to 'codex login status', which the free
   path never passes (no codex login). Added _codex_free_profile_active() so an
   active browser-use-free profile counts as codex-authed in both checks.
@LarsenCundric
Copy link
Copy Markdown
Contributor Author

Retest gotcha: the existing staging box already has the old [model_providers.browser-use-free] block (with wire_api = "chat") in ~/.codex/config.toml. Bootstrap is idempotent and skips writing the block if it already exists, so an /update alone won't rewrite wire_api. To retest cleanly on the current box, delete the stale block (or the whole ~/.codex/config.toml) and re-run bootstrap, so it rewrites with wire_api = "responses". Fresh boxes (re-bake) are unaffected.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 4 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread agent/bux-agent-shell Outdated
Comment thread agent/bux-agent-shell
…ell (Cubic, ENG-4785)

Cubic P2s on the launcher's free-profile detection:
- profile*=* matched non-profile keys (profile_dir, profiles); now extract the
  key before '=', trim it, and require an EXACT 'profile' match.
- table-header detection missed indented headers; now trim leading whitespace
  before matching so '  [profiles.x]' is correctly seen as a table and its
  in-table 'profile' isn't read as the top-level default.
Tested: top-level free, no-space, decoy keys, in-table-only, indented table.
@LarsenCundric
Copy link
Copy Markdown
Contributor Author

pr-reviewer (automated)

Reviewed gh pr diff 265 as four specialists (correctness, security, conventions, architecture). Verified the two profile parsers against 9 edge cases and traced the fresh-install service-start ordering. One blocking issue, the rest are nice-to-haves.

Blocking

install.sh never symlinks bux-agent-shell, but it starts ttyd that now execs it — fresh installs get a crash-looping web terminal. (agent/bux-ttyd.service:9, install.sh:677, agent/bootstrap.sh:213)

The new ExecStart is /usr/local/bin/ttyd -i lo -p 7681 -W /usr/local/bin/bux-agent-shell. That /usr/local/bin/bux-agent-shell symlink is created only in bootstrap.sh:213. But the documented install path is curl install.sh | sudo bash (README.md:11), and install.sh never invokes bootstrap.sh — it symlinks tg-send, bux-restart, tg-schedule, etc. (install.sh:434-465) but not bux-agent-shell, then starts bux-ttyd.service at install.sh:677. So on a fresh box:

  1. ttyd starts with ExecStart pointing at a non-existent path
  2. ttyd fails to exec → Restart=always crash-loop
  3. README.md:43 explicitly tells the user to verify systemctl is-active bux-ttyd right after install — that check now fails

The old /usr/bin/claude target worked because install.sh npm install -g's claude (install.sh:182-184). The new target has no such guarantee until a /update (which runs bootstrap) happens.

Fix: add the symlink to install.sh next to the other helper symlinks, e.g.

ln -sfn "$REPO_DIR/agent/bux-agent-shell" /usr/local/bin/bux-agent-shell

(bootstrap re-asserting it is still correct for the already-provisioned case; install.sh needs it for first boot.)

Nice-to-have

1. Path source inconsistency across the 3 mirrored helpers. (agent/box_agent.py:261, agent/telegram_bot.py:76, agent/bux-agent-shell:11)

  • box_agent.py:261 hardcodes /home/bux/.codex/config.toml
  • telegram_bot.py:76 uses CODEX_CONFIG env override defaulting to /home/bux/.codex/config.toml
  • bux-agent-shell:11 uses ${CODEX_CONFIG:-$HOME/.codex/config.toml}

All three resolve to the same file when run as bux (and ttyd runs as User=bux, so $HOME is correct), so behavior is fine today. But it's three different ways to name one path. Not blocking; worth a one-line note that they intentionally diverge, or aligning box_agent.py to the env-override form.

2. Triplicated free-profile detection logic is a real maintenance liability. (agent/box_agent.py:251, agent/telegram_bot.py:1398, agent/bux-agent-shell:14)
The TOML default-profile detection now lives in 3 places (two Python copies + one bash reimplementation). I verified all three agree across 9 cases (top-level quoted/unquoted, indented key, in-table-only, profile_dir decoy, other-value, table-first, inline-comment) — they're correct and in parity now. The risk is drift: a future TOML quirk fix has to land in 3 spots or the terminal and the bot disagree on which agent is "authed". The bash copy can't import the Python helper, so full dedup isn't free, but at minimum the two Python copies (box_agent.py, telegram_bot.py) should be one shared function. The launcher-script approach for the ttyd-hardcodes-claude problem is the right call; it's specifically the detection-logic fan-out that's the concern.

3. codex_signed_in() shells out to codex login status twice. (agent/bux-agent-shell:38-39)

codex login status 2>&1 | grep -qi "logged in" && \
  ! (codex login status 2>&1 | grep -qi "not logged in")

Two subprocess invocations to dodge the "not logged in" substring matching "logged in". Capturing once into a var is cheaper and avoids a TOCTOU window:

out="$(codex login status 2>&1)"
printf '%s' "$out" | grep -qi "not logged in" && return 1
printf '%s' "$out" | grep -qi "logged in"

Minor — this path only runs when the free profile is inactive.

4. "codex picked but not actually usable" gap — pre-existing, not introduced here. All three helpers treat profile = "browser-use-free" as authed without checking the CP proxy / bu-cp-token is live. If the free profile is set but the control plane is down, the terminal/bot will launch codex and fail at first request rather than falling back to claude. This matches the existing check_codex_authed() contract (box_agent.py:280) so it's consistent, not a regression — flagging only because the task asked. Out of scope for this PR.

Confirmed correct

  • bux-agent-shell parser: exact top-level profile key only (rejects profile_dir/profiles/in-table profile), handles indented headers, exec codex "$@" vs exec claude "$@" passes args through. Matches the Python helper byte-for-byte in behavior.
  • Both TG short-circuits (_is_agent_authed codex branch caches (now, True) correctly; _login_status_cached codex branch short-circuits before the provider check and still goes through the existing lock/cache write).
  • wire_api = "responses" change is correct; the grep-guard skip-if-exists caveat is already documented by the author in the PR comment (existing boxes need the stale block deleted to pick up the new wire_api).
  • Conventions: bux-agent-shell shebang #!/usr/bin/env bash matches bux-restart; set -u is lighter than siblings' set -euo pipefail but appropriate here (the parser relies on non-zero exits from return 1, and -e would be awkward with the &&/|| style). ruff-clean Python, consistent quoting.
  • Security: no new exposure. bux-agent-shell runs as bux under ttyd, only reads config + shells codex login status. bu-cp-token parsing remains grep-not-source.

@LarsenCundric LarsenCundric merged commit 3a5f9d2 into main May 26, 2026
7 checks passed
@LarsenCundric
Copy link
Copy Markdown
Contributor Author

Fixed the blocking issue in dfe1808 — added the bux-agent-shell symlink to install.sh, placed before the ttyd (re)start block so a fresh install.sh box (which never runs bootstrap) doesn't crash-loop ttyd on a missing ExecStart target. Good catch — CI/install-smoke didn't exercise the fresh-install ttyd path.

On the nice-to-haves: agreed the free-profile detection is now triplicated (2 Python + 1 bash) — they're in parity and tested, but I'll note deduping the two Python copies (box_agent + telegram_bot) as a follow-up. The others (config-path naming, double shell-out in codex_signed_in, proxy-down gap) are cosmetic/pre-existing; leaving for now to keep this PR scoped to the staging bugs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant