mirror of
https://github.com/hanzoai/bot.git
synced 2026-10-30 17:24:26 +00:00
The default was hardcoded:
AUTH_JSON="${AUTH_JSON}},\"default\":\"hanzo:default\"}"
but "hanzo:default" is only written when HANZO_API_KEY is set. A pod
without one — or one whose key was revoked and removed, which is exactly
where bot-gateway is now — published a default pointing into its own file
at a profile that is not there.
Two consequences, both worse than a missing key:
- The agent fails on the profile LOOKUP, not on the credential, so it
reads as a bot bug rather than "nobody gave me a key".
- ANTHROPIC_API_KEY and OPENAI_API_KEY profiles sitting right beside it
are skipped, because the default never falls back to them.
Now the default is whichever profile was actually written — hanzo first,
then anthropic, then openai — and when none were, the file carries no
default at all and the entrypoint says so, naming the KMS path.
Proven across all three shapes (no keys / hanzo / anthropic-only): the
default is either absent or present in profiles, never dangling. The old
code dangles in two of the three.
Adjacent and NOT done here: bot already has IAM M2M
(src/node-host/auto-auth.ts, /v1/iam/oauth/token) and the profile store
already understands "oauth" and "token" types, so bot can hold a client
id/secret in KMS and mint short-lived tokens instead of carrying a
long-lived bearer. That is the fix that makes "rotate the leaked key"
stop being a recurring task, and it deserves its own change.
Co-authored-by: Hanzo Dev <[email protected]>
300 lines
9.8 KiB
Bash
300 lines
9.8 KiB
Bash
#!/bin/bash
|
|
# Do NOT use set -e — background processes may return non-zero on warnings
|
|
|
|
echo "[cloud-agent] Starting combined bot + desktop environment"
|
|
|
|
# ── 1. Start the desktop environment (Xvfb + VNC + WM) ──────────────
|
|
export HOME=/home/operative
|
|
export DISPLAY=:${DISPLAY_NUM:-1}
|
|
|
|
echo "[cloud-agent] Starting desktop on DISPLAY=$DISPLAY"
|
|
cd "$HOME"
|
|
|
|
# Start Xvfb
|
|
Xvfb $DISPLAY -ac -screen 0 ${WIDTH:-1920}x${HEIGHT:-1080}x24 -retro -dpi 96 \
|
|
-nolisten tcp -nolisten unix &
|
|
XVFB_PID=$!
|
|
echo "[cloud-agent] Xvfb started (PID $XVFB_PID)"
|
|
|
|
# Wait for X to be ready (up to 10 seconds)
|
|
for i in $(seq 1 20); do
|
|
if xdpyinfo -display $DISPLAY >/dev/null 2>&1; then
|
|
echo "[cloud-agent] X display ready"
|
|
break
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
|
|
# Start window manager — openbox works headless, mutter needs a full session
|
|
if command -v openbox &>/dev/null; then
|
|
openbox --display=$DISPLAY 2>/dev/null &
|
|
sleep 1
|
|
if kill -0 $! 2>/dev/null; then
|
|
echo "[cloud-agent] Window manager: openbox (PID $!)"
|
|
else
|
|
echo "[cloud-agent] WARNING: openbox failed to start"
|
|
fi
|
|
elif command -v mutter &>/dev/null; then
|
|
mutter --replace --display=$DISPLAY 2>/dev/null &
|
|
sleep 1
|
|
echo "[cloud-agent] Window manager: mutter"
|
|
fi
|
|
|
|
# Start tint2 panel
|
|
if [ -f "$HOME/.config/tint2/tint2rc" ]; then
|
|
tint2 -c "$HOME/.config/tint2/tint2rc" 2>/dev/null &
|
|
echo "[cloud-agent] tint2 panel started"
|
|
fi
|
|
|
|
# Setup desktop icons — create .desktop shortcut files and libfm config
|
|
# (ported from operative's start_all.sh)
|
|
echo "[cloud-agent] Setting up desktop icons..."
|
|
mkdir -p "$HOME/Desktop"
|
|
mkdir -p "$HOME/.config/libfm"
|
|
|
|
# Configure libfm to auto-execute .desktop files without prompting
|
|
cat > "$HOME/.config/libfm/libfm.conf" << 'LIBFM'
|
|
[config]
|
|
quick_exec=1
|
|
|
|
[ui]
|
|
always_show_tabs=0
|
|
LIBFM
|
|
|
|
# Create desktop shortcut files
|
|
cat > "$HOME/Desktop/Terminal.desktop" << 'DESK'
|
|
[Desktop Entry]
|
|
Name=Terminal
|
|
Exec=xterm -fa "Monospace" -fs 14
|
|
Icon=utilities-terminal
|
|
Type=Application
|
|
DESK
|
|
|
|
cat > "$HOME/Desktop/Firefox.desktop" << 'DESK'
|
|
[Desktop Entry]
|
|
Name=Firefox
|
|
Exec=firefox-esr
|
|
Icon=firefox-esr
|
|
Type=Application
|
|
DESK
|
|
|
|
cat > "$HOME/Desktop/Calculator.desktop" << 'DESK'
|
|
[Desktop Entry]
|
|
Name=Calculator
|
|
Exec=galculator
|
|
Icon=galculator
|
|
Type=Application
|
|
DESK
|
|
|
|
cat > "$HOME/Desktop/TextEditor.desktop" << 'DESK'
|
|
[Desktop Entry]
|
|
Name=Text Editor
|
|
Exec=gedit
|
|
Icon=text-editor
|
|
Type=Application
|
|
DESK
|
|
|
|
cat > "$HOME/Desktop/Spreadsheet.desktop" << 'DESK'
|
|
[Desktop Entry]
|
|
Name=Spreadsheet
|
|
Exec=libreoffice --calc
|
|
Icon=libreoffice-calc
|
|
Type=Application
|
|
DESK
|
|
|
|
cat > "$HOME/Desktop/Files.desktop" << 'DESK'
|
|
[Desktop Entry]
|
|
Name=Files
|
|
Exec=pcmanfm
|
|
Icon=system-file-manager
|
|
Type=Application
|
|
DESK
|
|
|
|
chmod +x "$HOME/Desktop/"*.desktop
|
|
|
|
# Start pcmanfm desktop mode (manages root window with desktop icons)
|
|
if command -v pcmanfm &>/dev/null; then
|
|
pcmanfm --desktop --display=$DISPLAY 2>/dev/null &
|
|
echo "[cloud-agent] pcmanfm desktop icons started"
|
|
fi
|
|
|
|
# Give desktop components time to initialize
|
|
sleep 2
|
|
|
|
# Start VNC server
|
|
echo "[cloud-agent] Starting VNC server on port 5900"
|
|
pkill -9 -f "x0vncserver" 2>/dev/null || true
|
|
sleep 0.5
|
|
|
|
if command -v x0vncserver &>/dev/null; then
|
|
x0vncserver -display $DISPLAY -rfbport 5900 -SecurityTypes None &
|
|
VNC_PID=$!
|
|
elif command -v x11vnc &>/dev/null; then
|
|
x11vnc -display $DISPLAY -forever -shared -wait 50 -rfbport 5900 -nopw -nolookup -noxdamage -nap &
|
|
VNC_PID=$!
|
|
fi
|
|
|
|
# Wait for VNC to be ready
|
|
for i in $(seq 1 10); do
|
|
if netstat -tuln 2>/dev/null | grep -q ":5900 "; then
|
|
echo "[cloud-agent] VNC server ready on port 5900"
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
# ── 2. Start the operative API (computer-use tools) ──────────────────
|
|
# The operative Streamlit app provides screenshot, click, type tools
|
|
# on port 8501. The bot's LLM uses these for desktop automation.
|
|
if [ -d "$HOME/.operative" ] && [ -f "$HOME/.operative/operative/operative.py" ]; then
|
|
echo "[cloud-agent] Starting operative API on port 8501"
|
|
cd "$HOME/.operative"
|
|
if [ -d ".venv" ]; then
|
|
(
|
|
source .venv/bin/activate 2>/dev/null || . .venv/bin/activate 2>/dev/null
|
|
DISPLAY=$DISPLAY STREAMLIT_SERVER_PORT=8501 \
|
|
python -m streamlit run operative/operative.py \
|
|
--server.headless true \
|
|
--server.address 0.0.0.0 \
|
|
> /tmp/streamlit_stdout.log 2>&1 &
|
|
echo "[cloud-agent] operative Streamlit started (PID $!)"
|
|
)
|
|
else
|
|
echo "[cloud-agent] WARNING: operative venv not found, skipping"
|
|
fi
|
|
# Also start the operative HTTP server if present
|
|
if [ -f "http_server.py" ]; then
|
|
(
|
|
source .venv/bin/activate 2>/dev/null || . .venv/bin/activate 2>/dev/null
|
|
python http_server.py > /tmp/server_logs.txt 2>&1 &
|
|
echo "[cloud-agent] operative HTTP server started"
|
|
)
|
|
fi
|
|
fi
|
|
|
|
# Start noVNC websocket proxy (port 6080 → VNC 5900)
|
|
if command -v websockify &>/dev/null || [ -f /usr/share/novnc/utils/novnc_proxy ]; then
|
|
echo "[cloud-agent] Starting noVNC on port 6080"
|
|
websockify --web /usr/share/novnc/ 6080 localhost:5900 > /tmp/novnc.log 2>&1 &
|
|
echo "[cloud-agent] noVNC started (PID $!)"
|
|
fi
|
|
|
|
# ── 3. Start the bot agent ───────────────────────────────────────────
|
|
echo "[cloud-agent] Starting bot agent"
|
|
cd /app
|
|
|
|
# Create workspace directories.
|
|
# The bot expects its workspace at /home/node/.hanzo-bot/workspace-{nodeId}
|
|
# (where node is the Node.js HOME). Create both the generic and node-specific
|
|
# workspace paths so the exec tool's canonical cwd check succeeds.
|
|
NODE_ID="${AGENT_NODE_ID:-${HANZO_NODE_ID:-cloud-unknown}}"
|
|
mkdir -p "$HOME/.hanzo-bot/workspace"
|
|
mkdir -p "/home/node/.hanzo-bot/workspace-${NODE_ID}"
|
|
|
|
# Create exec approvals file with permissive allowlist for cloud agents.
|
|
# Cloud agents need to run desktop commands (firefox, ls, etc.) without
|
|
# manual approval prompts.
|
|
# Write to $HOME/.hanzo-bot/ (the path the bot process actually reads).
|
|
# HOME is /home/operative in the combined bot-cloud image.
|
|
mkdir -p "$HOME/.hanzo-bot"
|
|
cat > "$HOME/.hanzo-bot/exec-approvals.json" << 'APPROVALS'
|
|
{
|
|
"version": 1,
|
|
"defaults": {
|
|
"security": "full",
|
|
"ask": "off"
|
|
},
|
|
"agents": {
|
|
"*": {
|
|
"security": "full",
|
|
"ask": "off",
|
|
"allowlist": [
|
|
{"pattern": "*"}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
APPROVALS
|
|
|
|
# Write auth-profiles.json so the agent can authenticate LLM calls.
|
|
# Provisioned pods get HANZO_API_KEY from the playground provisioner.
|
|
# Writing to the "main" agent dir lets all per-agent dirs inherit it.
|
|
mkdir -p "/home/node/.hanzo-bot/agents/main/agent"
|
|
AUTH_JSON="{\"version\":1,\"profiles\":{"
|
|
FIRST=1
|
|
DEFAULT_PROFILE=""
|
|
if [ -n "$HANZO_API_KEY" ]; then
|
|
AUTH_JSON="${AUTH_JSON}\"hanzo:default\":{\"type\":\"api_key\",\"provider\":\"hanzo\",\"key\":\"${HANZO_API_KEY}\"}"
|
|
DEFAULT_PROFILE="hanzo:default"
|
|
FIRST=0
|
|
fi
|
|
if [ -n "$ANTHROPIC_API_KEY" ]; then
|
|
[ $FIRST -eq 0 ] && AUTH_JSON="${AUTH_JSON},"
|
|
AUTH_JSON="${AUTH_JSON}\"anthropic:default\":{\"type\":\"api_key\",\"provider\":\"anthropic\",\"key\":\"${ANTHROPIC_API_KEY}\"}"
|
|
[ -z "$DEFAULT_PROFILE" ] && DEFAULT_PROFILE="anthropic:default"
|
|
FIRST=0
|
|
fi
|
|
if [ -n "$OPENAI_API_KEY" ]; then
|
|
[ $FIRST -eq 0 ] && AUTH_JSON="${AUTH_JSON},"
|
|
AUTH_JSON="${AUTH_JSON}\"openai:default\":{\"type\":\"api_key\",\"provider\":\"openai\",\"key\":\"${OPENAI_API_KEY}\"}"
|
|
[ -z "$DEFAULT_PROFILE" ] && DEFAULT_PROFILE="openai:default"
|
|
fi
|
|
# Name a default only if that profile was actually WRITTEN. This used to be
|
|
# hardcoded to "hanzo:default", so a pod with no HANZO_API_KEY — or one whose key
|
|
# had been revoked and removed — published a default pointing at a profile that
|
|
# does not exist in the file it points into. The agent then fails on the lookup
|
|
# rather than on the credential, which reads as a bot bug instead of a missing
|
|
# key, and it silently skips the anthropic/openai profiles sitting right there.
|
|
# Prefer hanzo, fall back in order, and say so plainly when there is nothing.
|
|
if [ -n "$DEFAULT_PROFILE" ]; then
|
|
AUTH_JSON="${AUTH_JSON}},\"default\":\"${DEFAULT_PROFILE}\"}"
|
|
else
|
|
AUTH_JSON="${AUTH_JSON}}}"
|
|
fi
|
|
echo "$AUTH_JSON" > "/home/node/.hanzo-bot/agents/main/agent/auth-profiles.json"
|
|
if [ -n "$DEFAULT_PROFILE" ]; then
|
|
echo "[cloud-agent] auth-profiles.json written (default: ${DEFAULT_PROFILE})"
|
|
else
|
|
echo "[cloud-agent] auth-profiles.json written with NO profiles — no HANZO_API_KEY," \
|
|
"ANTHROPIC_API_KEY or OPENAI_API_KEY in the environment; LLM calls will fail" \
|
|
"until one is provided (KMS path hanzo/prod/bot)"
|
|
fi
|
|
|
|
# Create bot config that routes exec to the local node (not sandbox).
|
|
# Without this, the embedded LLM agent defaults to host="sandbox" which
|
|
# fails because cloud pods don't have sandbox configured. Setting host="node"
|
|
# makes the agent dispatch commands through the gateway back to this node,
|
|
# where they execute in the same environment as the VNC desktop.
|
|
cat > "$HOME/.hanzo-bot/hanzo-bot.json" << 'BOTCONFIG'
|
|
{
|
|
"tools": {
|
|
"exec": {
|
|
"host": "node",
|
|
"security": "full",
|
|
"ask": "off"
|
|
}
|
|
}
|
|
}
|
|
BOTCONFIG
|
|
|
|
# The bot connects to the gateway as a node and handles:
|
|
# - Chat (LLM calls via Hanzo API)
|
|
# - Exec (runs commands locally — same env as desktop)
|
|
# - Browser control (Playwright on the desktop's browser)
|
|
# - VNC tunnel (proxies VNC to gateway)
|
|
# Gateway URL from env (read by bot's runner.ts)
|
|
export BOT_NODE_GATEWAY_URL="${BOT_NODE_GATEWAY_URL:-${BOT_GATEWAY_URL:-ws://bot-gateway.hanzo.svc:80}}"
|
|
|
|
# Node ID: prefer AGENT_NODE_ID (set by playground provisioner)
|
|
NODE_ID="${AGENT_NODE_ID:-${HANZO_NODE_ID:-cloud-unknown}}"
|
|
|
|
# Security mode: cloud agents need full access to run desktop commands (firefox,
|
|
# ls, etc.) without approval. "full" with ask=off allows all commands.
|
|
# "allowlist" would block shell wrappers (sh -c) used by terminal and AI chat.
|
|
SECURITY_MODE="${AGENT_SECURITY_MODE:-full}"
|
|
|
|
exec node hanzo-bot.mjs node run \
|
|
--node-id "$NODE_ID" \
|
|
--security "$SECURITY_MODE" \
|
|
--ask off
|