Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
35 changes: 29 additions & 6 deletions agent/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -131,19 +131,22 @@ install -d -o bux -g bux -m 0755 /var/lib/bux
# Why MCP at all: cloud holds the platform's Composio API key plus every
# integration the user OAuth'd via cloud.browser-use.com. Rather than
# duplicating that ceremony on each box (Composio key, per-toolkit auth
# configs, OAuth callbacks, refresh-token storage), we point Claude Code
# configs, OAuth callbacks, refresh-token storage), we point the local CLIs
# at a cloud-hosted MCP endpoint that proxies tool calls through with the
# box's project_id as the Composio entity_id. Net effect: any toolkit the
# user has connected on cloud (Gmail, Calendar, Slack, …) is automatically
# available to the box agent as native tools — zero per-box setup.
#
# Token rotation: BUX_BOX_TOKEN gets baked into ~/.claude.json by
# `claude mcp add` at registration time. If the cloud rotates the token,
# Token rotation: Claude stores the bearer header in ~/.claude.json at
# registration time, while Codex stores the env-var name in ~/.codex/config.toml
# and reads BUX_BOX_TOKEN when a turn starts. If the cloud rotates the token,
# the next /update re-runs this section, which removes + re-adds the MCP
# server with the fresh token. Manual rotation: re-run bootstrap.sh.
# server with the fresh token/header shape. Manual rotation: re-run
# bootstrap.sh.
#
# To disable: as the bux user, `claude mcp remove composio`. The next
# /update will re-add it unless this section is removed too.
# To disable: as the bux user, `claude mcp remove composio` and/or
# `codex mcp remove composio`. The next /update will re-add it unless this
# section is removed too.
if [ -f /etc/bux/env ]; then
# shellcheck disable=SC1091
. /etc/bux/env || true
Expand Down Expand Up @@ -193,6 +196,26 @@ else
fi
fi

if [ -z "${BUX_BOX_TOKEN:-}" ]; then
echo "bootstrap: BUX_BOX_TOKEN not set; skipping Codex Composio MCP registration" >&2
elif ! sudo -iu bux command -v codex >/dev/null 2>&1; then
echo "bootstrap: codex CLI not on PATH; skipping Codex Composio MCP registration" >&2
else
# Codex keeps MCP configuration separately from Claude. Register the same
# cloud proxy here so `/codex` Agency workers can inspect connected context
# instead of seeing an empty MCP tool list while Claude has Composio.
sudo -iu bux codex mcp remove composio >/dev/null 2>&1 || true
sudo -iu bux codex mcp add composio \
--url https://api.browser-use.com/cloud/composio/mcp \
--bearer-token-env-var BUX_BOX_TOKEN >/dev/null || \
echo "bootstrap: WARN failed to register Codex cloud Composio MCP server; continuing bootstrap" >&2
if ! sudo -iu bux codex mcp list 2>/dev/null | grep -q '^composio[[:space:]]'; then
echo "bootstrap: WARN Codex composio MCP registration didn't take" >&2
else
echo "bootstrap: registered Codex cloud Composio MCP server"
fi
fi

# --- login banner: live browser URL on each ssh login ---------------------
if ! grep -q 'BU_BROWSER_LIVE_URL' /home/bux/.profile 2>/dev/null; then
cat >> /home/bux/.profile <<'PROFILE'
Expand Down
4 changes: 4 additions & 0 deletions agent/telegram_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -3829,6 +3829,8 @@ def _build_env(
the per-user `~/.npm-global/bin/codex` lookup, ~/.local/bin tools).
- BU_* + BROWSER_USE_API_KEY + BUX_PROFILE_ID so claude can drive
the browser without re-fetching credentials.
- BUX_BOX_TOKEN for Codex only, so Codex's Composio MCP server can
read its configured bearer-token env var at turn runtime.
- OPENAI_* from /home/bux/.secrets/openai.env (codex needs this).
- TG_CHAT_ID + TG_THREAD_ID so `tg-send` routes back to this
forum topic when the agent shells out for background work.
Expand All @@ -3852,6 +3854,8 @@ def _build_env(
}
if box_env.get("BROWSER_USE_API_KEY"):
env["BROWSER_USE_API_KEY"] = box_env["BROWSER_USE_API_KEY"]
if agent == AGENT_CODEX and box_env.get("BUX_BOX_TOKEN"):
env["BUX_BOX_TOKEN"] = box_env["BUX_BOX_TOKEN"]
if box_env.get("BUX_PROFILE_ID"):
env["BUX_PROFILE_ID"] = box_env["BUX_PROFILE_ID"]
env["BU_PROFILE_ID"] = box_env["BUX_PROFILE_ID"]
Expand Down
33 changes: 33 additions & 0 deletions agent/test_telegram_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import json
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
Expand Down Expand Up @@ -90,6 +91,38 @@ def test_auth_and_quota_errors_trigger_login_picker_detection(self) -> None:
self.assertTrue(telegram_bot._is_codex_auth_error("usage limit reached"))


class AgentEnvTest(unittest.TestCase):
def test_codex_gets_box_token_for_mcp_but_claude_does_not(self) -> None:
with tempfile.TemporaryDirectory() as td:
root = Path(td)
box_env = root / "box.env"
browser_env = root / "browser.env"
openai_env = root / "openai.env"
box_env.write_text(
"BROWSER_USE_API_KEY=bu_test\n"
"BUX_PROFILE_ID=profile_123\n"
"BUX_BOX_TOKEN=box_secret\n"
)
browser_env.write_text("")
openai_env.write_text("OPENAI_API_KEY=sk_test\n")

bot = telegram_bot.Bot.__new__(telegram_bot.Bot)
bot.state = {"offset": 0, "agents": {}, "codex_settings": {}, "owners": {}}

with (
mock.patch.object(telegram_bot, "BOX_ENV", box_env),
mock.patch.object(telegram_bot, "BROWSER_ENV", browser_env),
mock.patch.object(telegram_bot, "OPENAI_ENV", openai_env),
):
codex_env = bot._build_env((100, 55), telegram_bot.AGENT_CODEX)
claude_env = bot._build_env((100, 55), telegram_bot.AGENT_CLAUDE)

self.assertEqual(codex_env["BUX_BOX_TOKEN"], "box_secret")
self.assertEqual(codex_env["BROWSER_USE_API_KEY"], "bu_test")
self.assertEqual(codex_env["OPENAI_API_KEY"], "sk_test")
self.assertNotIn("BUX_BOX_TOKEN", claude_env)


class AgencyButtonPromptTest(unittest.TestCase):
def test_custom_button_prompt_includes_card_context(self) -> None:
prompt = telegram_bot._agency_build_custom_dispatch_prompt(
Expand Down
25 changes: 23 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,17 @@ fi
# --- collect config --------------------------------------------------------
BROWSER_USE_API_KEY="${BROWSER_USE_API_KEY:-}"
BUX_PROFILE_ID="${BUX_PROFILE_ID:-}"
BUX_BOX_TOKEN="${BUX_BOX_TOKEN:-}"

# If /etc/bux/env already exists (rerun), seed missing values from it so the
# script is truly idempotent without making the user re-type secrets.
if [ -z "$BROWSER_USE_API_KEY" ] && [ -r /etc/bux/env ]; then
if [ -r /etc/bux/env ]; then
# shellcheck disable=SC1091
BROWSER_USE_API_KEY="$(. /etc/bux/env && printf %s "${BROWSER_USE_API_KEY:-}")"
BROWSER_USE_API_KEY="${BROWSER_USE_API_KEY:-$(. /etc/bux/env && printf %s "${BROWSER_USE_API_KEY:-}")}"
# shellcheck disable=SC1091
BUX_PROFILE_ID="${BUX_PROFILE_ID:-$(. /etc/bux/env && printf %s "${BUX_PROFILE_ID:-}")}"
# shellcheck disable=SC1091
BUX_BOX_TOKEN="${BUX_BOX_TOKEN:-$(. /etc/bux/env && printf %s "${BUX_BOX_TOKEN:-}")}"
fi

if [ -z "$BROWSER_USE_API_KEY" ] && [ -t 0 ]; then
Expand Down Expand Up @@ -483,6 +486,9 @@ if [ ! -f /etc/bux/env ]; then
BROWSER_USE_API_KEY=$BROWSER_USE_API_KEY
BUX_PROFILE_ID=$BUX_PROFILE_ID
EOF
if [ -n "$BUX_BOX_TOKEN" ]; then
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot May 13, 2026

Choose a reason for hiding this comment

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

P2: BUX_BOX_TOKEN is not persisted on reruns when /etc/bux/env already exists, so existing installs can miss the token after upgrade.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At install.sh, line 489:

<comment>`BUX_BOX_TOKEN` is not persisted on reruns when `/etc/bux/env` already exists, so existing installs can miss the token after upgrade.</comment>

<file context>
@@ -483,6 +486,9 @@ if [ ! -f /etc/bux/env ]; then
 BROWSER_USE_API_KEY=$BROWSER_USE_API_KEY
 BUX_PROFILE_ID=$BUX_PROFILE_ID
 EOF
+	if [ -n "$BUX_BOX_TOKEN" ]; then
+		printf 'BUX_BOX_TOKEN=%s\n' "$BUX_BOX_TOKEN" >> /etc/bux/env
+	fi
</file context>
Fix with Cubic

printf 'BUX_BOX_TOKEN=%s\n' "$BUX_BOX_TOKEN" >> /etc/bux/env
fi
chmod 640 /etc/bux/env
chown root:bux /etc/bux/env
else
Expand Down Expand Up @@ -545,6 +551,21 @@ if ! sudo -iu bux command -v codex >/dev/null 2>&1; then
|| warn 'codex install failed (non-fatal — /codex login will hint how to install later)'
fi

# Codex keeps MCP configuration separately from Claude. Register the same
# cloud Composio proxy that bootstrap.sh registers for Claude so `/codex`
# Agency workers can inspect connected Gmail / Slack / GitHub context.
if [ -z "${BUX_BOX_TOKEN:-}" ]; then
warn 'BUX_BOX_TOKEN not set; skipping Codex Composio MCP registration'
elif sudo -iu bux command -v codex >/dev/null 2>&1; then
sudo -iu bux codex mcp remove composio >/dev/null 2>&1 || true
sudo -iu bux codex mcp add composio \
--url https://api.browser-use.com/cloud/composio/mcp \
--bearer-token-env-var BUX_BOX_TOKEN >/dev/null \
|| warn 'failed to register Codex cloud Composio MCP server'
else
warn 'codex CLI not on PATH; skipping Codex Composio MCP registration'
fi

# --- login banner: print live browser URL on each ssh login ---------------
if ! grep -q 'BU_BROWSER_LIVE_URL' /home/bux/.profile 2>/dev/null; then
cat >> /home/bux/.profile <<'PROFILE'
Expand Down
Loading