Skip to content

Commit 6f6aae8

Browse files
committed
Add Hermes runtime support to Bux
1 parent 760b026 commit 6f6aae8

10 files changed

Lines changed: 1247 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: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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 HERMES_HOME
15+
export PATH="$BUX_HOME/.local/bin:$BUX_HOME/.npm-global/bin:/usr/local/bin:/usr/bin:/bin:${PATH:-}"
16+
export BUX_HERMES_SOUL="${BUX_HERMES_SOUL:-$HERMES_HOME/SOUL.md}"
17+
18+
BROWSER_ENV="${BROWSER_ENV:-$BUX_HOME/.claude/browser.env}"
19+
if [ -r "$BROWSER_ENV" ]; then
20+
set -a
21+
# shellcheck disable=SC1090
22+
. "$BROWSER_ENV"
23+
set +a
24+
fi
25+
HERMES_ENV="${HERMES_ENV:-$HERMES_HOME/env}"
26+
if [ -r "$HERMES_ENV" ]; then
27+
set -a
28+
# shellcheck disable=SC1090
29+
. "$HERMES_ENV"
30+
set +a
31+
fi
32+
33+
if [ "${1:-}" = "--bux-env-check" ]; then
34+
printf 'BUX_HOME=%s\n' "$BUX_HOME"
35+
printf 'BUX_HERMES_SOUL=%s\n' "$BUX_HERMES_SOUL"
36+
printf 'BU_CDP_WS=%s\n' "${BU_CDP_WS:-}"
37+
printf 'BU_BROWSER_ID=%s\n' "${BU_BROWSER_ID:-}"
38+
printf 'BU_PROFILE_ID=%s\n' "${BU_PROFILE_ID:-}"
39+
exit 0
40+
fi
41+
42+
find_hermes() {
43+
if [ -n "${HERMES_BIN:-}" ]; then
44+
if [ -x "$HERMES_BIN" ]; then
45+
printf '%s\n' "$HERMES_BIN"
46+
return 0
47+
fi
48+
printf 'bux-hermes: HERMES_BIN is not executable: %s\n' "$HERMES_BIN" >&2
49+
return 1
50+
fi
51+
for candidate in \
52+
"$BUX_HOME/.local/bin/hermes" \
53+
"$BUX_HOME/.npm-global/bin/hermes" \
54+
/usr/local/bin/hermes \
55+
/usr/bin/hermes
56+
do
57+
if [ -x "$candidate" ]; then
58+
printf '%s\n' "$candidate"
59+
return 0
60+
fi
61+
done
62+
if command -v hermes >/dev/null 2>&1; then
63+
command -v hermes
64+
return 0
65+
fi
66+
return 1
67+
}
68+
69+
if ! hermes_bin="$(find_hermes)"; then
70+
cat >&2 <<'EOF'
71+
bux-hermes: Hermes is not installed or not executable.
72+
73+
Install or authenticate Hermes as the bux user, then retry. For a terminal
74+
login flow from Telegram, use:
75+
76+
/terminal hermes login
77+
EOF
78+
exit 127
79+
fi
80+
81+
cmd="${1:-}"
82+
case "$cmd" in
83+
run)
84+
shift
85+
if [ -n "${HERMES_RUN_COMMAND:-}" ]; then
86+
export HERMES_PROMPT="$*"
87+
exec bash -lc "$HERMES_RUN_COMMAND"
88+
fi
89+
exec "$hermes_bin" --oneshot "$*"
90+
;;
91+
status)
92+
if "$hermes_bin" status >/dev/null 2>&1; then
93+
exec "$hermes_bin" status
94+
fi
95+
exec "$hermes_bin" --version
96+
;;
97+
"")
98+
exec "$hermes_bin" --help
99+
;;
100+
*)
101+
exec "$hermes_bin" "$@"
102+
;;
103+
esac

agent/install-hermes

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

0 commit comments

Comments
 (0)