Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion agent/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,10 @@ if [ ! -f "$CODEX_CONFIG" ] || ! grep -qE "^\[model_providers\.browser-use-free\
[model_providers.browser-use-free]
name = "Browser Use free (DeepSeek V4)"
base_url = "$CP_BASE"
wire_api = "chat"
# Codex hard-removed wire_api = "chat" (Feb 2026); "responses" is the only
# supported value. Codex calls {base_url}/responses, which the CP proxy
# forwards to OpenRouter's drop-in Responses API. See ENG-4785.
wire_api = "responses"
stream_idle_timeout_ms = 300000

[model_providers.browser-use-free.auth]
Expand Down Expand Up @@ -205,6 +208,9 @@ ln -sfn /usr/local/bin/tg-schedule /usr/local/bin/schedule
ln -sfn "$REPO_DIR/agent/agency-report" /usr/local/bin/agency-report
ln -sfn "$REPO_DIR/agent/bux-restart" /usr/local/bin/bux-restart
ln -sfn "$REPO_DIR/agent/bux-miniapp-tunnel" /usr/local/bin/bux-miniapp-tunnel
# Web-terminal agent launcher: picks codex (free-DeepSeek profile or signed-in)
# vs claude, so ttyd doesn't hardcode claude on a codex-only box (ENG-4785).
ln -sfn "$REPO_DIR/agent/bux-agent-shell" /usr/local/bin/bux-agent-shell

# --- system prompt + CLAUDE.md/AGENTS.md symlinks --------------------------
# The one source of truth is /home/bux/system-prompt.md (copied from the
Expand Down
39 changes: 39 additions & 0 deletions agent/bux-agent-shell
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Launch the agent the box is actually configured for in the web terminal /
# SSH session, instead of hardcoding claude (ENG-4785).
#
# Picks codex when the box is on the free DeepSeek path (the browser-use-free
# profile is the default in ~/.codex/config.toml) or when codex is signed in;
# otherwise falls back to claude. Mirrors the agent-selection logic the
# telegram bot uses so the terminal and the bot agree on which agent runs.
set -u

CODEX_CONFIG="${CODEX_CONFIG:-$HOME/.codex/config.toml}"

# True if the top-level `profile` key selects the free DeepSeek profile.
# Top-level only: stop scanning at the first [table] header. Exact key match.
free_profile_active() {
[ -f "$CODEX_CONFIG" ] || return 1
while IFS= read -r line; do
case "$line" in
\[*) return 1 ;; # first table header -> no top-level profile line
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
profile*=*)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
val="${line#*=}"
val="$(printf '%s' "$val" | tr -d ' "'\''')"
[ "$val" = "browser-use-free" ] && return 0 || return 1
;;
esac
done < "$CODEX_CONFIG"
return 1
}

codex_signed_in() {
command -v codex >/dev/null 2>&1 || return 1
codex login status 2>&1 | grep -qi "logged in" && \
! (codex login status 2>&1 | grep -qi "not logged in")
}

if command -v codex >/dev/null 2>&1 && { free_profile_active || codex_signed_in; }; then
exec codex "$@"
fi
exec claude "$@"
2 changes: 1 addition & 1 deletion agent/bux-ttyd.service
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ After=network-online.target
Type=simple
User=bux
Group=bux
ExecStart=/usr/local/bin/ttyd -i lo -p 7681 -W /usr/bin/claude
ExecStart=/usr/local/bin/ttyd -i lo -p 7681 -W /usr/local/bin/bux-agent-shell
Restart=always
RestartSec=5

Expand Down
45 changes: 41 additions & 4 deletions agent/telegram_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,30 @@ def _write_session_uuid(path: Path, sid: str) -> None:
_AGENT_AUTH_TTL_S = 30.0


def _codex_free_profile_active() -> bool:
"""True if config.toml selects the free DeepSeek profile as the default.

The free path (ENG-4785) needs no `codex login` — the control plane holds
the credential, the box just routes to it. So "authed" for Codex means the
top-level `profile = "browser-use-free"` line is present. Matches the exact
top-level `profile` key (not startswith), so `profile_dir`/`profiles` aren't
mistaken for the default-profile selector. Mirror of the same helper in
box_agent.py.
"""
try:
with open(CODEX_CONFIG, encoding="utf-8") as f:
for line in f:
stripped = line.strip()
if stripped.startswith("["):
break # top-level keys only — stop at the first table header
if "=" in stripped and stripped.split("=", 1)[0].strip() == "profile":
value = stripped.split("=", 1)[1].strip().strip('"').strip("'")
return value == "browser-use-free"
except Exception:
return False
return False


def _is_agent_authed(agent: str) -> bool:
"""Cheap check: is this CLI signed in? Shells out to the CLI's
`auth status` and caches the result for `_AGENT_AUTH_TTL_S` seconds.
Expand All @@ -1421,6 +1445,13 @@ def _is_agent_authed(agent: str) -> bool:
cached = _AGENT_AUTH_CACHE.get(agent)
if cached and now - cached[0] < _AGENT_AUTH_TTL_S:
return cached[1]
# Free DeepSeek path (ENG-4785): Codex has no `codex login` here — auth is
# the browser-use-free profile + bu-cp-token, so `codex login status` would
# report "not logged in" and misroute the user to Claude / the picker.
# Treat an active free profile as authed (mirrors box_agent.check_codex_authed).
if agent == AGENT_CODEX and _codex_free_profile_active():
_AGENT_AUTH_CACHE[agent] = (now, True)
return True
sub = ["auth", "status"] if agent == AGENT_CLAUDE else ["login", "status"]
cmd = ["sudo", "-iu", "bux", agent, *sub]
try:
Expand Down Expand Up @@ -3637,10 +3668,16 @@ def _login_status_cached(provider: str) -> bool:
except Exception:
ok = False
elif provider == "codex":
try:
ok, _ = CODEX_AUTH_PROVIDER.check()
except Exception:
ok = False
# Free DeepSeek path (ENG-4785): no `codex login`, so an active
# browser-use-free profile counts as signed in (else the picker shows
# "not signed in" for a box that's actually ready).
if _codex_free_profile_active():
ok = True
else:
try:
ok, _ = CODEX_AUTH_PROVIDER.check()
except Exception:
ok = False
else:
return False
with _login_status_lock:
Expand Down
Loading