Skip to content
Draft
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
53 changes: 53 additions & 0 deletions agent/HERMES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Bux context for Hermes

You are Hermes running inside Browser Use Box.

## Runtime

- Default workspace: `/home/bux`.
- Persistent user state lives under `/home/bux`.
- The Bux repo is usually at `/opt/bux/repo`.
- User-private context belongs in `/opt/bux/repo/private/` or Hermes' own
private memory, not in repo-tracked files.

## Browser

Use the existing Browser Use Cloud browser. Do not install local Chrome,
Chromium, Playwright browsers, or other desktop browser runtimes.

Before browser work:

```bash
source /home/bux/.claude/browser.env
```

Then use `browser-harness-js`:

```bash
browser-harness-js 'await session.connect({wsUrl: process.env.BU_CDP_WS}); await session.Page.navigate({url: "https://example.com"})'
```

The profile is persistent. Cookies and logins should survive between turns.

## Telegram

Telegram lanes are separate user-facing sessions. The environment may include:

- `TG_CHAT_ID`
- `TG_THREAD_ID`
- `TG_USER_ID`
- `TG_USERNAME`
- `TG_FROM_NAME`
- `TG_OWNER_ID`
- `TG_IS_OWNER`

For background work that should report back to the same lane, use `tg-send`.

## Behavior

- Be action-first and concise.
- If blocked by login, 2FA, CAPTCHA, or a human-only browser step, explain the
blocker and share the Browser Use live URL if available.
- Prefer the existing browser harness over raw HTTP for websites with user
state.
- Keep the box tidy. Avoid unnecessary global installs.
13 changes: 13 additions & 0 deletions agent/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ chmod 0644 "$CODEX_CONFIG"
# agent/ after a box has already been provisioned never get linked into
# /usr/local/bin without a re-bootstrap. Re-assert here on every update so
# the symlinks track agent/ as new helpers ship. Idempotent (ln -sfn).
HERMES_WAS_ENABLED=0
if [ -x /usr/local/bin/bux-hermes ] || [ -f /home/bux/.hermes/SOUL.md ]; then
HERMES_WAS_ENABLED=1
fi
ln -sfn "$REPO_DIR/agent/tg-send" /usr/local/bin/tg-send
ln -sfn "$REPO_DIR/agent/tg-buttons" /usr/local/bin/tg-buttons
ln -sfn "$REPO_DIR/agent/tg-schedule" /usr/local/bin/tg-schedule
Expand All @@ -118,6 +122,15 @@ 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
ln -sfn "$REPO_DIR/agent/bux-hermes" /usr/local/bin/bux-hermes

# --- Hermes support (optional third lane agent) ---------------------------
# Keep existing Hermes-enabled boxes current, but do not make every production
# box opt into Hermes just because this helper exists in the repo.
if [ "${WITH_HERMES:-0}" = "1" ] || [ "$HERMES_WAS_ENABLED" = "1" ]; then
/bin/bash "$AGENT_DIR/install-hermes" \
|| echo "bootstrap: hermes install/update failed (non-fatal)" >&2
fi

# --- system prompt + CLAUDE.md/AGENTS.md symlinks --------------------------
# The one source of truth is /home/bux/system-prompt.md (copied from the
Expand Down
10 changes: 10 additions & 0 deletions agent/box_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
ENV_PATH = Path('/etc/bux/env')
HEARTBEAT_INTERVAL = 30
TG_ENV = Path('/etc/bux/tg.env')
VALID_DEFAULT_AGENTS = {'claude', 'codex', 'hermes'}

# Where the OSS repo is cloned by install.sh at bake time. /opt/bux/agent is
# a symlink to /opt/bux/repo/agent so systemd units' ExecStart=/opt/bux/agent
Expand Down Expand Up @@ -917,11 +918,17 @@ async def _handle(self, raw: str | bytes) -> None:
owner_tg_user_id: int | None = int(raw_owner) if raw_owner else None
except (TypeError, ValueError):
owner_tg_user_id = None
raw_default_agent = msg.get('bux_default_agent') or msg.get('default_agent')
default_agent = str(raw_default_agent or '').strip().lower()
if default_agent and default_agent not in VALID_DEFAULT_AGENTS:
LOG.warning('ignoring invalid tg_install default_agent=%r', raw_default_agent)
default_agent = ''
await self._tg_install(
msg.get('bot_token', ''),
msg.get('setup_token', ''),
msg.get('bot_username', ''),
owner_tg_user_id=owner_tg_user_id,
default_agent=default_agent or None,
)
elif cmd == 'update':
# Pull latest agent code from the OSS repo and restart services.
Expand Down Expand Up @@ -1400,6 +1407,7 @@ async def _tg_install(
bot_username: str,
*,
owner_tg_user_id: int | None = None,
default_agent: str | None = None,
) -> None:
if not bot_token:
await self._send(
Expand All @@ -1420,6 +1428,8 @@ async def _tg_install(
# pre-dates this writer; we only need user_id for auth purposes
# (Telegram stamps `from.id` server-side, can't be forged).
lines.append(f'TG_OWNER_ID={int(owner_tg_user_id)}')
if default_agent:
lines.append(f'BUX_DEFAULT_AGENT={default_agent}')
TG_ENV.write_text('\n'.join(lines) + '\n', encoding='utf-8')
# Mode 0o600, owner bux:bux (we run as bux). Both readers can still
# get the token: the bux-telegram-bot.service runs as User=root
Expand Down
237 changes: 237 additions & 0 deletions agent/bux-hermes
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
#!/usr/bin/env bash
# Stable Bux wrapper around the real Hermes binary.
#
# Telegram and tests call this wrapper instead of calling `hermes` directly so
# Bux can provide the same workspace, browser, and routing environment every
# time while preserving Hermes' own auth/subscription files under /home/bux.
set -euo pipefail

BUX_HOME="${BUX_HOME:-/home/bux}"
BUX_USER="${BUX_USER:-bux}"
HERMES_HOME="${HERMES_HOME:-$BUX_HOME/.hermes}"
export HOME="$BUX_HOME"
export USER="$BUX_USER"
export HERMES_HOME
export PATH="$BUX_HOME/.local/bin:$BUX_HOME/.npm-global/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
export BUX_HERMES_SOUL="${BUX_HERMES_SOUL:-$HERMES_HOME/SOUL.md}"

BROWSER_ENV="${BROWSER_ENV:-$BUX_HOME/.claude/browser.env}"
if [ -r "$BROWSER_ENV" ]; then
set -a
# shellcheck disable=SC1090
. "$BROWSER_ENV"
set +a
fi
HERMES_ENV="${HERMES_ENV:-$HERMES_HOME/env}"
if [ -r "$HERMES_ENV" ]; then
set -a
# shellcheck disable=SC1090
. "$HERMES_ENV"
set +a
fi

if [ "${1:-}" = "--bux-env-check" ]; then
printf 'BUX_HOME=%s\n' "$BUX_HOME"
printf 'BUX_HERMES_SOUL=%s\n' "$BUX_HERMES_SOUL"
printf 'BU_CDP_WS=%s\n' "${BU_CDP_WS:-}"
printf 'BU_BROWSER_ID=%s\n' "${BU_BROWSER_ID:-}"
printf 'BU_PROFILE_ID=%s\n' "${BU_PROFILE_ID:-}"
exit 0
fi

find_hermes() {
if [ -n "${HERMES_BIN:-}" ]; then
if [ -x "$HERMES_BIN" ]; then
printf '%s\n' "$HERMES_BIN"
return 0
fi
printf 'bux-hermes: HERMES_BIN is not executable: %s\n' "$HERMES_BIN" >&2
return 1
fi
for candidate in \
"$BUX_HOME/.local/bin/hermes" \
"$BUX_HOME/.npm-global/bin/hermes" \
/usr/local/bin/hermes \
/usr/bin/hermes
do
if [ -x "$candidate" ]; then
printf '%s\n' "$candidate"
return 0
fi
done
if command -v hermes >/dev/null 2>&1; then
command -v hermes
return 0
fi
return 1
}

if ! hermes_bin="$(find_hermes)"; then
cat >&2 <<'EOF'
bux-hermes: Hermes is not installed or not executable.

Install or authenticate Hermes as the bux user, then retry. For a terminal
login flow from Telegram, use:

/terminal hermes login
EOF
exit 127
fi

hermes_python() {
local first
first="$(head -n 1 "$hermes_bin" 2>/dev/null || true)"
if [[ "$first" == "#!"*python* ]]; then
first="${first#\#!}"
if [ -x "$first" ]; then
printf '%s\n' "$first"
return 0
fi
fi
command -v python3
}

current_provider() {
"$(hermes_python)" <<'PY' 2>/dev/null || true
from pathlib import Path
import os
import yaml

path = Path(os.environ.get("HERMES_HOME", str(Path.home() / ".hermes"))) / "config.yaml"
try:
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
except Exception:
data = {}
model = data.get("model") if isinstance(data, dict) else {}
provider = model.get("provider") if isinstance(model, dict) else ""
print(str(provider or "").strip())
PY
}

configure_codex() {
local model="${1:-${HERMES_CODEX_MODEL:-gpt-5.5}}"
HERMES_CODEX_MODEL="$model" "$(hermes_python)" <<'PY'
from __future__ import annotations

import os
import sys

from hermes_cli.auth import (
DEFAULT_CODEX_BASE_URL,
AuthError,
_import_codex_cli_tokens,
_save_codex_tokens,
_update_config_for_provider,
resolve_codex_runtime_credentials,
)

base_url = os.environ.get("HERMES_CODEX_BASE_URL", "").strip().rstrip("/") or DEFAULT_CODEX_BASE_URL
source = ""
try:
creds = resolve_codex_runtime_credentials()
base_url = str(creds.get("base_url") or base_url).strip().rstrip("/") or base_url
source = "existing Hermes Codex credentials"
except Exception:
tokens = _import_codex_cli_tokens()
if not tokens:
print(
"No usable Codex CLI credentials found. Run /codex login first, "
"or use /terminal hermes model for a separate Hermes login.",
file=sys.stderr,
)
raise SystemExit(3)
_save_codex_tokens(tokens)
source = "imported Codex CLI credentials"

_update_config_for_provider("openai-codex", base_url)
print(source)
PY
"$hermes_bin" config set model.default "$model" >/dev/null
printf 'Hermes configured for OpenAI Codex (%s).\n' "$model"
}

configure_claude() {
local model="${1:-${HERMES_CLAUDE_MODEL:-claude-sonnet-4-6}}"
"$(hermes_python)" <<'PY'
from __future__ import annotations

import sys

from agent.anthropic_adapter import read_claude_code_credentials, _resolve_claude_code_token_from_credentials
from hermes_cli.config import use_anthropic_claude_code_credentials

creds = read_claude_code_credentials()
if not _resolve_claude_code_token_from_credentials(creds):
print(
"No usable Claude Code credentials found. Run /claude login first, "
"or use /terminal hermes model for a separate Hermes login.",
file=sys.stderr,
)
raise SystemExit(3)

use_anthropic_claude_code_credentials()
print("using Claude Code credentials")
PY
"$hermes_bin" config set model.provider anthropic >/dev/null
"$hermes_bin" config set model.base_url https://api.anthropic.com >/dev/null
"$hermes_bin" config set model.default "$model" >/dev/null
printf 'Hermes configured for Claude Code (%s).\n' "$model"
}

configure_auto() {
local quiet=0
if [ "${1:-}" = "--quiet" ]; then
quiet=1
fi
local provider
provider="$(current_provider)"
if [ -n "$provider" ]; then
if [ "$quiet" = 0 ]; then
printf 'Hermes already configured for %s.\n' "$provider"
fi
return 0
fi
if configure_codex; then
return 0
fi
if configure_claude; then
return 0
fi
return 3
}

cmd="${1:-}"
case "$cmd" in
run)
shift
if [ -n "${HERMES_RUN_COMMAND:-}" ]; then
export HERMES_PROMPT="$*"
exec bash -lc "$HERMES_RUN_COMMAND"
fi
exec "$hermes_bin" --oneshot "$*"
;;
status)
if "$hermes_bin" status >/dev/null 2>&1; then
exec "$hermes_bin" status
fi
exec "$hermes_bin" --version
;;
configure-codex|setup-codex)
shift
configure_codex "$@"
;;
configure-claude|setup-claude)
shift
configure_claude "$@"
;;
configure-auto|setup-auto)
shift
configure_auto "$@"
;;
"")
exec "$hermes_bin" --help
;;
*)
exec "$hermes_bin" "$@"
;;
esac
Loading
Loading