Skip to content

Commit 9fc537f

Browse files
committed
Add Hermes runtime support to Bux
1 parent 760b026 commit 9fc537f

10 files changed

Lines changed: 1245 additions & 13 deletions

agent/HERMES.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Bux context for Hermes
2+
3+
You are Hermes running inside Browser Use Box.
4+
5+
## Runtime
6+
7+
- Default workspace: `/home/bux`.
8+
- Persistent user state lives under `/home/bux`.
9+
- The Bux repo is usually at `/opt/bux/repo`.
10+
- User-private context belongs in `/opt/bux/repo/private/` or Hermes' own
11+
private memory, not in repo-tracked files.
12+
13+
## Browser
14+
15+
Use the existing Browser Use Cloud browser. Do not install local Chrome,
16+
Chromium, Playwright browsers, or other desktop browser runtimes.
17+
18+
Before browser work:
19+
20+
```bash
21+
source /home/bux/.claude/browser.env
22+
```
23+
24+
Then use `browser-harness-js`:
25+
26+
```bash
27+
browser-harness-js 'await session.connect({wsUrl: process.env.BU_CDP_WS}); await session.Page.navigate({url: "https://example.com"})'
28+
```
29+
30+
The profile is persistent. Cookies and logins should survive between turns.
31+
32+
## Telegram
33+
34+
Telegram lanes are separate user-facing sessions. The environment may include:
35+
36+
- `TG_CHAT_ID`
37+
- `TG_THREAD_ID`
38+
- `TG_USER_ID`
39+
- `TG_USERNAME`
40+
- `TG_FROM_NAME`
41+
- `TG_OWNER_ID`
42+
- `TG_IS_OWNER`
43+
44+
For background work that should report back to the same lane, use `tg-send`.
45+
46+
## Behavior
47+
48+
- Be action-first and concise.
49+
- If blocked by login, 2FA, CAPTCHA, or a human-only browser step, explain the
50+
blocker and share the Browser Use live URL if available.
51+
- Prefer the existing browser harness over raw HTTP for websites with user
52+
state.
53+
- Keep the box tidy. Avoid unnecessary global installs.

agent/bootstrap.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ chmod 0644 "$CODEX_CONFIG"
109109
# agent/ after a box has already been provisioned never get linked into
110110
# /usr/local/bin without a re-bootstrap. Re-assert here on every update so
111111
# the symlinks track agent/ as new helpers ship. Idempotent (ln -sfn).
112+
HERMES_WAS_ENABLED=0
113+
if [ -x /usr/local/bin/bux-hermes ] || [ -f /home/bux/.hermes/soul.md ]; then
114+
HERMES_WAS_ENABLED=1
115+
fi
112116
ln -sfn "$REPO_DIR/agent/tg-send" /usr/local/bin/tg-send
113117
ln -sfn "$REPO_DIR/agent/tg-buttons" /usr/local/bin/tg-buttons
114118
ln -sfn "$REPO_DIR/agent/tg-schedule" /usr/local/bin/tg-schedule
@@ -118,6 +122,15 @@ ln -sfn /usr/local/bin/tg-schedule /usr/local/bin/schedule
118122
ln -sfn "$REPO_DIR/agent/agency-report" /usr/local/bin/agency-report
119123
ln -sfn "$REPO_DIR/agent/bux-restart" /usr/local/bin/bux-restart
120124
ln -sfn "$REPO_DIR/agent/bux-miniapp-tunnel" /usr/local/bin/bux-miniapp-tunnel
125+
ln -sfn "$REPO_DIR/agent/bux-hermes" /usr/local/bin/bux-hermes
126+
127+
# --- Hermes support (optional third lane agent) ---------------------------
128+
# Keep existing Hermes-enabled boxes current, but do not make every production
129+
# box opt into Hermes just because this helper exists in the repo.
130+
if [ "${WITH_HERMES:-0}" = "1" ] || [ "$HERMES_WAS_ENABLED" = "1" ]; then
131+
/bin/bash "$AGENT_DIR/install-hermes" \
132+
|| echo "bootstrap: hermes install/update failed (non-fatal)" >&2
133+
fi
121134

122135
# --- system prompt + CLAUDE.md/AGENTS.md symlinks --------------------------
123136
# The one source of truth is /home/bux/system-prompt.md (copied from the

agent/box_agent.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
ENV_PATH = Path('/etc/bux/env')
3737
HEARTBEAT_INTERVAL = 30
3838
TG_ENV = Path('/etc/bux/tg.env')
39+
VALID_DEFAULT_AGENTS = {'claude', 'codex', 'hermes'}
3940

4041
# Where the OSS repo is cloned by install.sh at bake time. /opt/bux/agent is
4142
# a symlink to /opt/bux/repo/agent so systemd units' ExecStart=/opt/bux/agent
@@ -917,11 +918,17 @@ async def _handle(self, raw: str | bytes) -> None:
917918
owner_tg_user_id: int | None = int(raw_owner) if raw_owner else None
918919
except (TypeError, ValueError):
919920
owner_tg_user_id = None
921+
raw_default_agent = msg.get('bux_default_agent') or msg.get('default_agent')
922+
default_agent = str(raw_default_agent or '').strip().lower()
923+
if default_agent and default_agent not in VALID_DEFAULT_AGENTS:
924+
LOG.warning('ignoring invalid tg_install default_agent=%r', raw_default_agent)
925+
default_agent = ''
920926
await self._tg_install(
921927
msg.get('bot_token', ''),
922928
msg.get('setup_token', ''),
923929
msg.get('bot_username', ''),
924930
owner_tg_user_id=owner_tg_user_id,
931+
default_agent=default_agent or None,
925932
)
926933
elif cmd == 'update':
927934
# Pull latest agent code from the OSS repo and restart services.
@@ -1400,6 +1407,7 @@ async def _tg_install(
14001407
bot_username: str,
14011408
*,
14021409
owner_tg_user_id: int | None = None,
1410+
default_agent: str | None = None,
14031411
) -> None:
14041412
if not bot_token:
14051413
await self._send(
@@ -1420,6 +1428,8 @@ async def _tg_install(
14201428
# pre-dates this writer; we only need user_id for auth purposes
14211429
# (Telegram stamps `from.id` server-side, can't be forged).
14221430
lines.append(f'TG_OWNER_ID={int(owner_tg_user_id)}')
1431+
if default_agent:
1432+
lines.append(f'BUX_DEFAULT_AGENT={default_agent}')
14231433
TG_ENV.write_text('\n'.join(lines) + '\n', encoding='utf-8')
14241434
# Mode 0o600, owner bux:bux (we run as bux). Both readers can still
14251435
# get the token: the bux-telegram-bot.service runs as User=root

agent/bux-hermes

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/usr/bin/env bash
2+
# Stable Bux wrapper around the real Hermes binary.
3+
#
4+
# Telegram and tests call this wrapper instead of calling `hermes` directly so
5+
# Bux can provide the same workspace, browser, and routing environment every
6+
# time while preserving Hermes' own auth/subscription files under /home/bux.
7+
set -euo pipefail
8+
9+
BUX_HOME="${BUX_HOME:-/home/bux}"
10+
BUX_USER="${BUX_USER:-bux}"
11+
HERMES_HOME="${HERMES_HOME:-$BUX_HOME/.hermes}"
12+
export HOME="$BUX_HOME"
13+
export USER="$BUX_USER"
14+
export PATH="$BUX_HOME/.local/bin:$BUX_HOME/.npm-global/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
15+
export BUX_HERMES_SOUL="${BUX_HERMES_SOUL:-$HERMES_HOME/soul.md}"
16+
17+
BROWSER_ENV="${BROWSER_ENV:-$BUX_HOME/.claude/browser.env}"
18+
if [ -r "$BROWSER_ENV" ]; then
19+
set -a
20+
# shellcheck disable=SC1090
21+
. "$BROWSER_ENV"
22+
set +a
23+
fi
24+
HERMES_ENV="${HERMES_ENV:-$HERMES_HOME/env}"
25+
if [ -r "$HERMES_ENV" ]; then
26+
set -a
27+
# shellcheck disable=SC1090
28+
. "$HERMES_ENV"
29+
set +a
30+
fi
31+
32+
if [ "${1:-}" = "--bux-env-check" ]; then
33+
printf 'BUX_HOME=%s\n' "$BUX_HOME"
34+
printf 'BUX_HERMES_SOUL=%s\n' "$BUX_HERMES_SOUL"
35+
printf 'BU_CDP_WS=%s\n' "${BU_CDP_WS:-}"
36+
printf 'BU_BROWSER_ID=%s\n' "${BU_BROWSER_ID:-}"
37+
printf 'BU_PROFILE_ID=%s\n' "${BU_PROFILE_ID:-}"
38+
exit 0
39+
fi
40+
41+
find_hermes() {
42+
if [ -n "${HERMES_BIN:-}" ]; then
43+
if [ -x "$HERMES_BIN" ]; then
44+
printf '%s\n' "$HERMES_BIN"
45+
return 0
46+
fi
47+
printf 'bux-hermes: HERMES_BIN is not executable: %s\n' "$HERMES_BIN" >&2
48+
return 1
49+
fi
50+
for candidate in \
51+
"$BUX_HOME/.local/bin/hermes" \
52+
"$BUX_HOME/.npm-global/bin/hermes" \
53+
/usr/local/bin/hermes \
54+
/usr/bin/hermes
55+
do
56+
if [ -x "$candidate" ]; then
57+
printf '%s\n' "$candidate"
58+
return 0
59+
fi
60+
done
61+
if command -v hermes >/dev/null 2>&1; then
62+
command -v hermes
63+
return 0
64+
fi
65+
return 1
66+
}
67+
68+
if ! hermes_bin="$(find_hermes)"; then
69+
cat >&2 <<'EOF'
70+
bux-hermes: Hermes is not installed or not executable.
71+
72+
Install or authenticate Hermes as the bux user, then retry. For a terminal
73+
login flow from Telegram, use:
74+
75+
/terminal hermes login
76+
EOF
77+
exit 127
78+
fi
79+
80+
cmd="${1:-}"
81+
case "$cmd" in
82+
run)
83+
shift
84+
if [ -n "${HERMES_RUN_COMMAND:-}" ]; then
85+
export HERMES_PROMPT="$*"
86+
exec bash -lc "$HERMES_RUN_COMMAND"
87+
fi
88+
exec "$hermes_bin" --oneshot "$*"
89+
;;
90+
status)
91+
if "$hermes_bin" status >/dev/null 2>&1; then
92+
exec "$hermes_bin" status
93+
fi
94+
exec "$hermes_bin" --version
95+
;;
96+
"")
97+
exec "$hermes_bin" --help
98+
;;
99+
*)
100+
exec "$hermes_bin" "$@"
101+
;;
102+
esac

agent/install-hermes

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#!/usr/bin/env bash
2+
# Idempotently add Bux integration around Hermes without touching Hermes auth.
3+
set -euo pipefail
4+
5+
WITH_HERMES="${WITH_HERMES:-1}"
6+
if [ "$WITH_HERMES" = "0" ]; then
7+
echo "install-hermes: skipped (WITH_HERMES=0)"
8+
exit 0
9+
fi
10+
11+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12+
REPO_DIR="${BUX_REPO_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
13+
BUX_HOME="${BUX_HOME:-/home/bux}"
14+
BUX_USER="${BUX_USER:-bux}"
15+
BUX_GROUP="${BUX_GROUP:-bux}"
16+
BUX_BIN_DIR="${BUX_BIN_DIR:-/usr/local/bin}"
17+
HERMES_HOME="${HERMES_HOME:-$BUX_HOME/.hermes}"
18+
HERMES_SOUL="${HERMES_SOUL:-$HERMES_HOME/soul.md}"
19+
HERMES_INSTALL_MODE="${HERMES_INSTALL_MODE:-detect}"
20+
21+
say() { printf 'install-hermes: %s\n' "$*"; }
22+
warn() { printf 'install-hermes: WARN %s\n' "$*" >&2; }
23+
24+
user_exists() {
25+
id -u "$BUX_USER" >/dev/null 2>&1
26+
}
27+
28+
chown_bux_if_possible() {
29+
if user_exists; then
30+
chown "$BUX_USER:$BUX_GROUP" "$@" 2>/dev/null || true
31+
fi
32+
}
33+
34+
run_as_bux() {
35+
if user_exists && [ "$(id -u)" -eq 0 ]; then
36+
sudo -iu "$BUX_USER" bash -lc "$*"
37+
else
38+
bash -lc "$*"
39+
fi
40+
}
41+
42+
find_hermes() {
43+
if [ -n "${HERMES_BIN:-}" ] && [ -x "$HERMES_BIN" ]; then
44+
printf '%s\n' "$HERMES_BIN"
45+
return 0
46+
fi
47+
for candidate in \
48+
"$BUX_HOME/.local/bin/hermes" \
49+
"$BUX_HOME/.npm-global/bin/hermes" \
50+
/usr/local/bin/hermes \
51+
/usr/bin/hermes
52+
do
53+
if [ -x "$candidate" ]; then
54+
printf '%s\n' "$candidate"
55+
return 0
56+
fi
57+
done
58+
return 1
59+
}
60+
61+
install -d -m 0755 "$HERMES_HOME"
62+
chown_bux_if_possible "$HERMES_HOME"
63+
64+
if hermes_bin="$(find_hermes)"; then
65+
say "found Hermes at $hermes_bin"
66+
elif [ -n "${HERMES_INSTALL_CMD:-}" ]; then
67+
say "running HERMES_INSTALL_CMD as $BUX_USER"
68+
run_as_bux "$HERMES_INSTALL_CMD" || warn "HERMES_INSTALL_CMD failed"
69+
elif [ "$HERMES_INSTALL_MODE" = "skip" ] || [ "$HERMES_INSTALL_MODE" = "detect" ]; then
70+
warn "Hermes binary not found; leaving wrapper installed so /hermes reports a clear error"
71+
else
72+
warn "unknown HERMES_INSTALL_MODE=$HERMES_INSTALL_MODE; not installing Hermes"
73+
fi
74+
75+
if [ -d "$BUX_BIN_DIR" ] || mkdir -p "$BUX_BIN_DIR" 2>/dev/null; then
76+
if ln -sfn "$REPO_DIR/agent/bux-hermes" "$BUX_BIN_DIR/bux-hermes" 2>/dev/null; then
77+
say "linked $BUX_BIN_DIR/bux-hermes"
78+
else
79+
warn "could not link $BUX_BIN_DIR/bux-hermes"
80+
fi
81+
else
82+
warn "could not create $BUX_BIN_DIR"
83+
fi
84+
85+
python3 - "$REPO_DIR/agent/HERMES.md" "$REPO_DIR/private/hermes/soul.local.md" "$HERMES_SOUL" <<'PY'
86+
from __future__ import annotations
87+
88+
import re
89+
import shutil
90+
import sys
91+
from pathlib import Path
92+
93+
source = Path(sys.argv[1])
94+
local = Path(sys.argv[2])
95+
dest = Path(sys.argv[3])
96+
97+
managed = source.read_text(encoding="utf-8").rstrip() + "\n"
98+
local_text = local.read_text(encoding="utf-8").rstrip() + "\n" if local.exists() else ""
99+
100+
block = (
101+
"<!-- BEGIN BUX HERMES MANAGED -->\n"
102+
+ managed
103+
+ "<!-- END BUX HERMES MANAGED -->\n\n"
104+
+ "<!-- BEGIN BUX HERMES LOCAL -->\n"
105+
+ local_text
106+
+ "<!-- END BUX HERMES LOCAL -->\n"
107+
)
108+
109+
dest.parent.mkdir(parents=True, exist_ok=True)
110+
existing = dest.read_text(encoding="utf-8") if dest.exists() else ""
111+
if existing and not (dest.with_suffix(dest.suffix + ".pre-bux")).exists():
112+
shutil.copy2(dest, dest.with_suffix(dest.suffix + ".pre-bux"))
113+
114+
pattern = re.compile(
115+
r"(?s)<!-- BEGIN BUX HERMES MANAGED -->.*<!-- END BUX HERMES LOCAL -->\n?"
116+
)
117+
if pattern.search(existing):
118+
out = pattern.sub(block, existing)
119+
elif existing.strip():
120+
out = existing.rstrip() + "\n\n" + block
121+
else:
122+
out = block
123+
124+
dest.write_text(out, encoding="utf-8")
125+
PY
126+
127+
chmod 0644 "$HERMES_SOUL"
128+
chown_bux_if_possible "$HERMES_SOUL"
129+
say "updated $HERMES_SOUL"
130+
131+
if hermes_bin="$(find_hermes)"; then
132+
printf 'HERMES_BIN=%s\n' "$hermes_bin" > "$HERMES_HOME/env"
133+
chmod 0644 "$HERMES_HOME/env"
134+
chown_bux_if_possible "$HERMES_HOME/env"
135+
"$hermes_bin" --version >/dev/null 2>&1 || true
136+
fi

0 commit comments

Comments
 (0)