FROM ghcr.io/tunmax/openclaw_computer:hermes_latest ARG TS_VERSION=1.98.9 RUN set -eux; \ curl -fsSL "https://pkgs.tailscale.com/stable/tailscale_${TS_VERSION}_amd64.tgz" -o /tmp/tailscale.tgz; \ tar -xzf /tmp/tailscale.tgz -C /tmp; \ install -m 0755 /tmp/tailscale_${TS_VERSION}_amd64/tailscale /usr/local/bin/tailscale; \ install -m 0755 /tmp/tailscale_${TS_VERSION}_amd64/tailscaled /usr/local/bin/tailscaled; \ rm -rf /tmp/tailscale.tgz /tmp/tailscale_${TS_VERSION}_amd64; \ mkdir -p /var/lib/tailscale /var/run/tailscale /root/bz-startup /usr/local/lib/hermes-startup /var/run/sshd /usr/local/share/hermes; \ export DEBIAN_FRONTEND=noninteractive; \ if [ -f /etc/apt/sources.list ]; then \ sed -i 's|mirrors.tuna.tsinghua.edu.cn|mirrors.aliyun.com|g' /etc/apt/sources.list || true; \ sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list || true; \ fi; \ (apt-get update -y || true); \ (apt-get install -y --no-install-recommends openssh-server rsync || true); \ rm -rf /var/lib/apt/lists/*; \ ssh-keygen -A || true RUN if [ -f /entrypoint.sh ]; then sed -i 's|hermes dashboard|env HERMES_DASHBOARD_TUI=1 hermes dashboard --tui|g' /entrypoint.sh || true; fi RUN cat > /usr/local/lib/hermes-startup/main.sh <<'HERMES_MAIN_EOF'
set -u TS_AUTHKEY="${TS_AUTHKEY:-${TAILSCALE_AUTHKEY:-}}" TS_HOSTNAME="${TS_HOSTNAME:-ms-hermes}" TS_STATE_DIR="${TS_STATE_DIR:-/mnt/workspace/tailscale}" TS_SOCK="${TS_SOCK:-/var/run/tailscale/tailscaled.sock}" TS_EXTRA_ARGS="${TS_EXTRA_ARGS:---accept-dns=false --accept-routes --reset}" ROOT_PASSWD="${ROOT_PASSWD:-123456}" mkdir -p "$TS_STATE_DIR" 2>/dev/null || true [ -n "$ROOT_PASSWD" ] && echo "root:${ROOT_PASSWD}" | chpasswd 2>/dev/null || true [ -z "$TS_AUTHKEY" ] && exit 0 command -v tailscaled >/dev/null 2>&1 || exit 0 if [ ! -S "$TS_SOCK" ]; then if [ -e /dev/net/tun ]; then nohup tailscaled --state="${TS_STATE_DIR}/tailscaled.state" --socket="$TS_SOCK" --port=41641 >"${TS_STATE_DIR}/tailscaled.log" 2>&1 & else nohup tailscaled --tun=userspace-networking --state="${TS_STATE_DIR}/tailscaled.state" --socket="$TS_SOCK" --port=41641 >"${TS_STATE_DIR}/tailscaled.log" 2>&1 & fi sleep 3 fi
tailscale --socket="$TS_SOCK" up --authkey="$TS_AUTHKEY" --hostname="$TS_HOSTNAME" $TS_EXTRA_ARGS || true exit 0
HERMES_MAIN_EOF RUN install -m 0755 /usr/local/lib/hermes-startup/main.sh /root/bz-startup/main.sh RUN cat > /usr/local/bin/hermes-boot.sh <<'HERMES_BOOT_EOF'
set -u log(){ echo "[hermes-boot] $*" | tee -a /var/log/hermes-boot.log; }
REAL="/usr/local/lib/hermes-startup/main.sh" WRAPPER="/root/bz-startup/main.sh" ROOT_PASSWD="${ROOT_PASSWD:-123456}" ENABLE_SSH="${ENABLE_SSH:-1}" SSH_PORT="${SSH_PORT:-22}"
ENABLE_WORKSPACE_LINKS="${ENABLE_WORKSPACE_LINKS:-1}" WS_ROOT="${WS_ROOT:-/mnt/workspace/root}"
WS_WAIT_SECS="${WS_WAIT_SECS:-120}"
TS_AUTHKEY="${TS_AUTHKEY:-${TAILSCALE_AUTHKEY:-}}" TS_HOSTNAME="${TS_HOSTNAME:-ms-hermes}" TS_STATE_DIR="${TS_STATE_DIR:-/mnt/workspace/tailscale}" TS_SOCK="${TS_SOCK:-/var/run/tailscale/tailscaled.sock}" TS_VERSION="${TS_VERSION:-1.98.9}" TS_EXTRA_ARGS="${TS_EXTRA_ARGS:---accept-dns=false --accept-routes --reset}"
ENABLE_HERMES_GATEWAY="${ENABLE_HERMES_GATEWAY:-1}"
HERMES_GATEWAY_WATCH_SECS="${HERMES_GATEWAY_WATCH_SECS:-45}"
ENABLE_HERMES_DASHBOARD_TUI="${ENABLE_HERMES_DASHBOARD_TUI:-1}"
mkdir -p /root/bz-startup /usr/local/lib/hermes-startup /var/lib/tailscale /var/run/tailscale \ /var/run/sshd /tmp /var/log 2>/dev/null || true
if [ -f "$REAL" ]; then install -m 0755 "$REAL" "$WRAPPER" 2>/dev/null || cp -f "$REAL" "$WRAPPER" 2>/dev/null || true chmod +x "$REAL" "$WRAPPER" 2>/dev/null || true fi
port_open() { local p="$1" if command -v ss >/dev/null 2>&1; then ss -lnt 2>/dev/null | grep -qE ":${p}\\b" && return 0; fi if command -v netstat >/dev/null 2>&1; then netstat -lnt 2>/dev/null | grep -qE ":${p}\\b" && return 0; fi timeout 1 bash -c "echo > /dev/tcp/127.0.0.1/${p}" 2>/dev/null && return 0 return 1 }
sync_root_pass() { if [ -n "${ROOT_PASSWD}" ] && command -v chpasswd >/dev/null 2>&1; then echo "root:${ROOT_PASSWD}" | chpasswd 2>/dev/null || true log "root password synced" fi }
stop_xrdp_if_any() { pkill -9 -x xrdp >/dev/null 2>&1 || true pkill -9 -x xrdp-sesman >/dev/null 2>&1 || true pkill -9 -f '/usr/sbin/xrdp' >/dev/null 2>&1 || true pkill -9 -f 'xrdp-sesman' >/dev/null 2>&1 || true }
wait_workspace() { local i for i in $(seq 1 "$WS_WAIT_SECS"); do if [ -d /mnt/workspace ] && touch /mnt/workspace/.ws_write_test 2>/dev/null; then rm -f /mnt/workspace/.ws_write_test 2>/dev/null || true log "workspace ready at ${i}s: /mnt/workspace" return 0 fi sleep 1 done log "WARN: /mnt/workspace not ready after ${WS_WAIT_SECS}s, skip home links" return 1 }
link_one_dir() { local name="$1" local src="/root/${name}" local dst="${WS_ROOT}/${name}" local parent
mkdir -p "$(dirname "$dst")" 2>/dev/null || true
mkdir -p "$dst" 2>/dev/null || true
if [ -L "$src" ]; then local cur cur="$(readlink "$src" 2>/dev/null || true)" if [ "$cur" = "$dst" ]; then return 0 fi rm -f "$src" 2>/dev/null || true fi
if [ -d "$src" ] && [ ! -L "$src" ]; then if command -v rsync >/dev/null 2>&1; then rsync -a "$src"/ "$dst"/ 2>/dev/null || true else cp -a "$src"/. "$dst"/ 2>/dev/null || true fi rm -rf "$src" 2>/dev/null || true elif [ -e "$src" ] || [ -L "$src" ]; then if [ -f "$src" ] && [ ! -e "$dst" ]; then cp -a "$src" "$dst" 2>/dev/null || true fi rm -rf "$src" 2>/dev/null || true fi
parent="$(dirname "$src")" mkdir -p "$parent" 2>/dev/null || true ln -sfn "$dst" "$src" log "link /root/${name} -> ${dst}" }
setup_workspace_home_links() { if [ "$ENABLE_WORKSPACE_LINKS" != "1" ]; then log "ENABLE_WORKSPACE_LINKS!=1, skip" return 0 fi wait_workspace || return 0 mkdir -p "$WS_ROOT" 2>/dev/null || true
local dirs=( Desktop Documents Downloads .openclaw .ssh .claude .codex )
if [ -d /root/.hermes ] && [ ! -L /root/.hermes ]; then local hd p tgt tmp for hd in config data sessions logs memory workspace; do p="/root/.hermes/${hd}" if [ -L "$p" ]; then tgt="$(readlink "$p" || true)" log "unsymlink hermes data path ${p} -> ${tgt}" tmp="${p}.real.$$" mkdir -p "$tmp" if [ -d "$tgt" ]; then if command -v rsync >/dev/null 2>&1; then rsync -a "$tgt"/ "$tmp"/ 2>/dev/null || true else cp -a "$tgt"/. "$tmp"/ 2>/dev/null || true fi fi rm -f "$p" mv "$tmp" "$p" chmod 700 "$p" 2>/dev/null || true elif [ ! -d "$p" ]; then [ -e "$p" ] && rm -rf "$p" mkdir -p "$p" chmod 700 "$p" 2>/dev/null || true fi done fi
local d for d in "${dirs[@]}"; do link_one_dir "$d" || log "WARN link failed: $d" done
if ! grep -q 'MODELSCOPE_WORKSPACE_HOME' /root/.bashrc 2>/dev/null; then cat >> /root/.bashrc <<'EOF'
export WORKSPACE_HOME=/mnt/workspace/root
EOF fi
log "workspace home links done under ${WS_ROOT}" ls -la /root 2>/dev/null | head -n 40 | while read -r L; do log " $L"; done || true }
start_tailscale() { if ! command -v tailscaled >/dev/null 2>&1; then curl -fsSL "https://pkgs.tailscale.com/stable/tailscale_${TS_VERSION}_amd64.tgz" -o /tmp/ts.tgz || return 0 tar -xzf /tmp/ts.tgz -C /tmp || return 0 install -m 0755 /tmp/tailscale_${TS_VERSION}_amd64/tailscale /usr/local/bin/tailscale install -m 0755 /tmp/tailscale_${TS_VERSION}_amd64/tailscaled /usr/local/bin/tailscaled rm -rf /tmp/ts.tgz /tmp/tailscale_${TS_VERSION}_amd64 fi [ -z "$TS_AUTHKEY" ] && { log "no TS_AUTHKEY, skip tailscale"; return 0; } mkdir -p "$TS_STATE_DIR" 2>/dev/null || true if ! [ -S "$TS_SOCK" ]; then if [ -e /dev/net/tun ]; then nohup tailscaled --state="${TS_STATE_DIR}/tailscaled.state" --socket="$TS_SOCK" --port=41641 \ >"${TS_STATE_DIR}/tailscaled.log" 2>&1 & else nohup tailscaled --tun=userspace-networking --state="${TS_STATE_DIR}/tailscaled.state" \ --socket="$TS_SOCK" --port=41641 >"${TS_STATE_DIR}/tailscaled.log" 2>&1 & fi for _ in $(seq 1 40); do [ -S "$TS_SOCK" ] && break; sleep 1; done fi tailscale --socket="$TS_SOCK" up --authkey="$TS_AUTHKEY" --hostname="$TS_HOSTNAME" $TS_EXTRA_ARGS || true IP4="$(tailscale --socket="$TS_SOCK" ip -4 2>/dev/null | head -n1 || true)" [ -n "$IP4" ] && log "tailnet IPv4=${IP4} hostname=${TS_HOSTNAME}" }
start_hermes_dashboard() { local want_tui=1 if [ "${ENABLE_HERMES_DASHBOARD_TUI:-1}" = "0" ]; then want_tui=0 fi
local H="" if [ -x /root/.hermes/hermes-agent/venv/bin/hermes ]; then H=/root/.hermes/hermes-agent/venv/bin/hermes elif [ -x /root/.local/bin/hermes ]; then H=/root/.local/bin/hermes elif command -v hermes >/dev/null 2>&1; then H=$(command -v hermes) fi if [ -z "$H" ]; then log "hermes binary not found, skip dashboard" return 0 fi
export PATH="/root/.hermes/hermes-agent/venv/bin:/root/.local/bin:${PATH:-}" export HERMES_HOME="${HERMES_HOME:-/root/.hermes}" if [ -f /root/.hermes/.env ]; then set -a . /root/.hermes/.env 2>/dev/null || true set +a fi
if port_open 9119; then log "dashboard already on :9119" return 0 fi
pkill -f "hermes dashboard" >/dev/null 2>&1 || true sleep 1
if [ "$want_tui" = "1" ]; then export HERMES_DASHBOARD_TUI=1 log "starting hermes dashboard --tui on 0.0.0.0:9119" nohup "$H" dashboard --tui --host 0.0.0.0 --port 9119 --no-open --insecure \ >>/tmp/hermes-dashboard.log 2>&1 < /dev/null & else unset HERMES_DASHBOARD_TUI 2>/dev/null || true log "starting hermes dashboard on 0.0.0.0:9119" nohup "$H" dashboard --host 0.0.0.0 --port 9119 --no-open --insecure \ >>/tmp/hermes-dashboard.log 2>&1 < /dev/null & fi
local i for i in $(seq 1 45); do if port_open 9119; then log "dashboard OK :9119 tui=${want_tui}" return 0 fi sleep 2 done log "dashboard FAIL; see /tmp/hermes-dashboard.log" tail -n 30 /tmp/hermes-dashboard.log 2>/dev/null || true }
ensure_persist_browser() { local launcher="/mnt/workspace/hailing/bin/hermes-chrome-persist" local profile="/mnt/workspace/hailing/browser-profile" local desk_dir desktop_file mkdir -p /mnt/workspace/hailing/bin "$profile" /tmp /var/log 2>/dev/null || true if [ -f /usr/local/share/hermes/hermes-chrome-persist ]; then install -m 0755 /usr/local/share/hermes/hermes-chrome-persist "$launcher" 2>/dev/null || cp -f /usr/local/share/hermes/hermes-chrome-persist "$launcher" chmod +x "$launcher" 2>/dev/null || true fi mkdir -p /root/.hermes 2>/dev/null || true ln -sfn "$profile" /root/.hermes/chrome-debug 2>/dev/null || true for desk_dir in /root/Desktop /mnt/workspace/root/Desktop /usr/share/applications; do mkdir -p "$desk_dir" 2>/dev/null || true desktop_file="$desk_dir/Hermes-Chrome-Persist.desktop" cat > "$desktop_file" <<'DESK' [Desktop Entry] Version=1.0 Type=Application Name=Hermes Chrome (Persist) Name[zh_CN]=Hermes 持久浏览器 Comment=Chrome with workspace profile + CDP for Hermes Exec=env DISPLAY=:1 HERMES_BROWSER_HEADED=1 /mnt/workspace/hailing/bin/hermes-chrome-persist %U Icon=google-chrome Terminal=false Categories=Network;WebBrowser; StartupNotify=true DESK chmod +x "$desktop_file" 2>/dev/null || true done if [ -f /usr/share/applications/google-chrome.desktop ]; then mkdir -p /root/.local/share/applications /mnt/workspace/root/.local/share/applications 2>/dev/null || true for desk_dir in /root/.local/share/applications /mnt/workspace/root/.local/share/applications; do cat > "$desk_dir/google-chrome.desktop" <<'DESK' [Desktop Entry] Version=1.0 Type=Application Name=Google Chrome Name[zh_CN]=谷歌浏览器(持久) GenericName=Web Browser Comment=Hermes persistent profile Exec=env DISPLAY=:1 HERMES_BROWSER_HEADED=1 /mnt/workspace/hailing/bin/hermes-chrome-persist %U Terminal=false Icon=google-chrome Categories=Network;WebBrowser; MimeType=text/html;text/xml;application/xhtml+xml;x-scheme-handler/http;x-scheme-handler/https; StartupNotify=true DESK chmod +x "$desk_dir/google-chrome.desktop" 2>/dev/null || true done fi if [ -x "$launcher" ]; then HERMES_BROWSER_HEADED=0 "$launcher" >/var/log/hermes-chrome-persist.boot.log 2>&1 || true if port_open 9222; then log "persist chrome CDP OK :9222"; else log "persist chrome CDP not ready yet"; fi else log "WARN: missing $launcher" fi }
start_sshd() { [ "$ENABLE_SSH" = "1" ] || return 0 mkdir -p /var/run/sshd /etc/ssh 2>/dev/null || true if ! command -v sshd >/dev/null 2>&1 && ! [ -x /usr/sbin/sshd ]; then export DEBIAN_FRONTEND=noninteractive if [ -f /etc/apt/sources.list ]; then sed -i 's|mirrors.tuna.tsinghua.edu.cn|mirrors.aliyun.com|g' /etc/apt/sources.list || true sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list || true fi apt-get update -y >/tmp/ssh-apt.log 2>&1 || true apt-get install -y --no-install-recommends openssh-server >/tmp/ssh-apt.log 2>&1 || true fi [ -f /etc/ssh/ssh_host_rsa_key ] || ssh-keygen -A >/tmp/ssh-keygen.log 2>&1 || true if [ -f /etc/ssh/sshd_config ]; then sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config sed -i "s/^#\?Port .*/Port ${SSH_PORT}/" /etc/ssh/sshd_config grep -q '^PermitRootLogin' /etc/ssh/sshd_config || echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config grep -q '^PasswordAuthentication' /etc/ssh/sshd_config || echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config fi pkill -9 -x sshd >/dev/null 2>&1 || true sleep 1 if [ -x /usr/sbin/sshd ]; then /usr/sbin/sshd >>/var/log/sshd.log 2>&1 || true sleep 1 if ! port_open "$SSH_PORT"; then nohup /usr/sbin/sshd -D >>/var/log/sshd.log 2>&1 & sleep 1 fi fi if port_open "$SSH_PORT"; then log "sshd OK :${SSH_PORT}"; else log "sshd FAIL"; tail -n 20 /var/log/sshd.log 2>/dev/null || true; fi }
sync_hermes_api_keys_from_env() { python3 - <<'PY' 2>/dev/null || true from pathlib import Path import os env_path = Path("/root/.hermes/.env") env_path.parent.mkdir(parents=True, exist_ok=True)
def from_pid1(name: str): try: for x in Path("/proc/1/environ").read_bytes().split(b"\x00"): if x.startswith(name.encode() + b"="): return x.split(b"=", 1)[1].decode("utf-8", "replace") except Exception: return "" return "" keys = {} for name in ( "MODELSCOPE_API_KEY", "STEPFUN_API_KEY", "STEP_PLAN_API_KEY", "JIEKANS_API_KEY", "OPENAI_API_KEY", "CUSTOM_API_KEY", "OPENROUTER_API_KEY", "DEEPSEEK_API_KEY", ): v = (os.environ.get(name) or from_pid1(name) or "").strip() if v: keys[name] = v if not keys: raise SystemExit(0) text = env_path.read_text(encoding="utf-8", errors="replace") if env_path.exists() else "" lines = text.splitlines() out, seen = [], set() for ln in lines: if not ln.strip() or ln.strip().startswith("#") or "=" not in ln: out.append(ln) continue k, _ = ln.split("=", 1) if k in keys: out.append(f"{k}={keys[k]}") seen.add(k) else: out.append(ln) for k, v in keys.items(): if k not in seen: out.append(f"{k}={v}") env_path.write_text("\n".join(out) + "\n", encoding="utf-8") try: env_path.chmod(0o600) except Exception: pass print("synced", ",".join(sorted(keys))) PY }
hermes_bin() { if [ -x /root/.hermes/hermes-agent/venv/bin/hermes ]; then echo /root/.hermes/hermes-agent/venv/bin/hermes return 0 fi if [ -x /root/.local/bin/hermes ]; then echo /root/.local/bin/hermes return 0 fi if command -v hermes >/dev/null 2>&1; then command -v hermes return 0 fi return 1 }
gateway_running() { local H H="$(hermes_bin 2>/dev/null || true)" if [ -n "$H" ] && "$H" gateway status 2>/dev/null | grep -q 'Gateway is running'; then return 0 fi ps -eo args 2>/dev/null | grep -q '[h]ermes gateway run' && return 0 return 1 }
start_hermes_gateway() { if [ "$ENABLE_HERMES_GATEWAY" != "1" ]; then log "ENABLE_HERMES_GATEWAY!=1, skip gateway" return 0 fi local H H="$(hermes_bin 2>/dev/null || true)" if [ -z "$H" ]; then log "hermes binary not found yet, skip gateway start" return 0 fi if gateway_running; then log "Hermes gateway already running" return 0 fi sync_hermes_api_keys_from_env || true local i for i in $(seq 1 30); do if [ -d /root/.hermes ]; then break; fi sleep 2 done export PATH="/root/.hermes/hermes-agent/venv/bin:/root/.local/bin:${PATH:-}" export HERMES_HOME="/root/.hermes" export HERMES_ACCEPT_HOOKS="${HERMES_ACCEPT_HOOKS:-1}" if [ -f /root/.hermes/.env ]; then set -a . /root/.hermes/.env 2>/dev/null || true set +a fi mkdir -p /var/log /tmp 2>/dev/null || true log "starting Hermes gateway (auto)" nohup setsid "$H" gateway run --accept-hooks >>/var/log/hermes-gateway-auto.log 2>&1 < /dev/null & sleep 4 if gateway_running; then log "Hermes gateway OK (auto-start)" else log "Hermes gateway start may have failed; see /var/log/hermes-gateway-auto.log" tail -n 30 /var/log/hermes-gateway-auto.log 2>/dev/null | while read -r L; do log "gateway: $L"; done || true fi }
watch_hermes_gateway() { if [ "$ENABLE_HERMES_GATEWAY" != "1" ]; then return 0; fi local n=0 while true; do sleep "${HERMES_GATEWAY_WATCH_SECS}" n=$((n + 1)) if ! gateway_running; then log "watchdog: gateway not running (tick=${n}), restarting" start_hermes_gateway fi start_hermes_dashboard || true done }
( sleep 2 sync_root_pass stop_xrdp_if_any setup_workspace_home_links start_tailscale start_sshd start_hermes_dashboard || true ensure_persist_browser || true ( sleep 25 start_hermes_gateway for j in 20 40 80; do sleep "$j" start_hermes_gateway done ) >>/var/log/hermes-boot.log 2>&1 & if [ "$ENABLE_HERMES_GATEWAY" = "1" ]; then if [ -x /usr/local/bin/hermes-gateway-watchdog.sh ]; then pkill -f '/usr/local/bin/hermes-gateway-watchdog.sh' >/dev/null 2>&1 || true sleep 1 nohup setsid /usr/local/bin/hermes-gateway-watchdog.sh \ >>/var/log/hermes-gateway-watchdog.log 2>&1 < /dev/null & log "started hermes-gateway-watchdog.sh pid=$!" else log "WARN: missing /usr/local/bin/hermes-gateway-watchdog.sh" fi fi for i in 5 10 20 35 55 90 120 180; do sleep "$i" setup_workspace_home_links if ! port_open "$SSH_PORT"; then start_sshd; fi start_hermes_dashboard || true ensure_persist_browser || true if [ "$ENABLE_HERMES_GATEWAY" = "1" ] && [ -x /usr/local/bin/hermes-gateway-watchdog.sh ]; then if ! pgrep -f '/usr/local/bin/hermes-gateway-watchdog.sh' >/dev/null 2>&1; then nohup setsid /usr/local/bin/hermes-gateway-watchdog.sh \ >>/var/log/hermes-gateway-watchdog.log 2>&1 < /dev/null & log "re-started hermes-gateway-watchdog.sh" fi fi stop_xrdp_if_any log "re-applied workspace links after sleep ${i}s" done ) >>/var/log/hermes-boot.log 2>&1 &
if [ -x "$REAL" ]; then nohup bash -c "sleep 4; exec '$REAL'" >>/var/log/hermes-startup.log 2>&1 & fi
( sleep 45 echo "===== hermes-boot.log =====" tail -n 120 /var/log/hermes-boot.log 2>/dev/null || true echo "===== /root symlinks =====" ls -la /root 2>/dev/null | head -n 50 || true echo "ports novnc=$(port_open 7860 && echo up || echo down) vnc=$(port_open 5901 && echo up || echo down) rdp=$(port_open 3389 && echo up || echo down) ssh=$(port_open $SSH_PORT && echo up || echo down)" ) &
exec /entrypoint.sh
HERMES_BOOT_EOF RUN cat > /usr/local/bin/hermes-gateway-watchdog.sh <<'HERMES_WATCH_EOF'
set -u LOG=/var/log/hermes-gateway-watchdog.log mkdir -p /var/log /tmp 2>/dev/null || true log(){ echo "[$(date '+%F %T')] $*" | tee -a "$LOG"; }
export PATH="/root/.hermes/hermes-agent/venv/bin:/root/.local/bin:/usr/local/bin:${PATH:-}" export HERMES_HOME="${HERMES_HOME:-/root/.hermes}" export HERMES_ACCEPT_HOOKS="${HERMES_ACCEPT_HOOKS:-1}"
hermes_bin(){ if [ -x /root/.hermes/hermes-agent/venv/bin/hermes ]; then echo /root/.hermes/hermes-agent/venv/bin/hermes return 0 fi if [ -x /root/.local/bin/hermes ]; then echo /root/.local/bin/hermes return 0 fi if command -v hermes >/dev/null 2>&1; then command -v hermes return 0 fi return 1 }
gateway_running(){ local H H="$(hermes_bin 2>/dev/null || true)" if [ -n "$H" ] && "$H" gateway status 2>/dev/null | grep -q 'Gateway is running'; then return 0 fi if ps -eo args 2>/dev/null | grep -E 'hermes([ -]|-agent.*/hermes).*gateway( run)?|bin/hermes gateway' | grep -v grep | grep -v watchdog >/dev/null 2>&1; then return 0 fi return 1 }
sync_keys(){ python3 - <<'PY' 2>/dev/null || true from pathlib import Path import os env_path = Path("/root/.hermes/.env") env_path.parent.mkdir(parents=True, exist_ok=True)
def pid1(name: str) -> str: try: for x in Path("/proc/1/environ").read_bytes().split(b"\0"): if x.startswith(name.encode() + b"="): return x.split(b"=", 1)[1].decode("utf-8", "replace") except Exception: return "" return ""
keys = {} for n in [ "MODELSCOPE_API_KEY", "STEPFUN_API_KEY", "STEP_PLAN_API_KEY", "JIEKANS_API_KEY", "OPENAI_API_KEY", "CUSTOM_API_KEY", "OPENROUTER_API_KEY", "DEEPSEEK_API_KEY", "TS_AUTHKEY", "TAILSCALE_AUTHKEY", ]: v = (os.environ.get(n) or pid1(n) or "").strip() if v: keys[n] = v if not keys: raise SystemExit(0) text = env_path.read_text(encoding="utf-8", errors="replace") if env_path.exists() else "" lines = text.splitlines() out, seen = [], set() for ln in lines: if not ln.strip() or ln.strip().startswith("#") or "=" not in ln: out.append(ln) continue k, _ = ln.split("=", 1) if k in keys: out.append(f"{k}={keys[k]}") seen.add(k) else: out.append(ln) for k, v in keys.items(): if k not in seen: out.append(f"{k}={v}") env_path.write_text("\n".join(out) + "\n", encoding="utf-8") try: env_path.chmod(0o600) except Exception: pass print("synced", ",".join(sorted(keys))) PY }
start_gateway(){ local H H="$(hermes_bin 2>/dev/null || true)" if [ -z "$H" ]; then log "no hermes binary yet" return 1 fi if [ ! -d /root/.hermes ]; then log "~/.hermes missing" return 1 fi sync_keys export PATH="/root/.hermes/hermes-agent/venv/bin:/root/.local/bin:/usr/local/bin:${PATH:-}" export HERMES_HOME=/root/.hermes export HERMES_ACCEPT_HOOKS="${HERMES_ACCEPT_HOOKS:-1}" if [ -f /root/.hermes/.env ]; then set -a . /root/.hermes/.env 2>/dev/null || true set +a fi log "starting gateway via $H" nohup setsid "$H" gateway run --accept-hooks >>/var/log/hermes-gateway-auto.log 2>&1 < /dev/null & sleep 5 if gateway_running; then log "gateway OK" return 0 fi log "gateway FAIL" tail -n 30 /var/log/hermes-gateway-auto.log 2>/dev/null || true return 1 }
port_open() { local p="$1" if command -v ss >/dev/null 2>&1; then ss -lnt 2>/dev/null | grep -qE ":${p}\\b" && return 0; fi if command -v netstat >/dev/null 2>&1; then netstat -lnt 2>/dev/null | grep -qE ":${p}\\b" && return 0; fi timeout 1 bash -c "echo > /dev/tcp/127.0.0.1/${p}" 2>/dev/null && return 0 return 1 }
WATCH_SECS="${HERMES_GATEWAY_WATCH_SECS:-45}" log "watchdog start (interval=${WATCH_SECS}s)" if gateway_running; then log "gateway already running" else start_gateway || true fi
while true; do sleep "$WATCH_SECS" if ! gateway_running; then log "watchdog: gateway down, restarting" start_gateway || true fi done
HERMES_WATCH_EOF RUN cat > /usr/local/share/hermes/hermes-chrome-persist <<'HERMES_CHROME_EOF'
set -u PROFILE_DIR="${HERMES_BROWSER_PROFILE:-/mnt/workspace/hailing/browser-profile}" CDP_PORT="${HERMES_BROWSER_CDP_PORT:-9222}"
HEADED_MODE="${HERMES_BROWSER_HEADED:-auto}" LOG="${HERMES_BROWSER_LOG:-/var/log/hermes-chrome-persist.log}" URL="${1:-about:blank}"
mkdir -p "$PROFILE_DIR" "$(dirname "$LOG")" /mnt/workspace/hailing/bin 2>/dev/null || true
CHROME="" for c in google-chrome-stable google-chrome chromium chromium-browser; do if command -v "$c" >/dev/null 2>&1; then CHROME=$(command -v "$c") break fi done if [ -z "$CHROME" ]; then echo "chrome not found" >&2 exit 1 fi
use_headed=0 if [ "$HEADED_MODE" = "1" ]; then use_headed=1 elif [ "$HEADED_MODE" = "0" ]; then use_headed=0 elif [ -n "${DISPLAY:-}" ]; then use_headed=1 fi
port_open() { local p="$1" if command -v ss >/dev/null 2>&1; then ss -lnt 2>/dev/null | grep -qE ":${p}\\b" && return 0 fi if command -v netstat >/dev/null 2>&1; then netstat -lnt 2>/dev/null | grep -qE ":${p}\\b" && return 0 fi return 1 }
if port_open "$CDP_PORT"; then echo "CDP already up on :${CDP_PORT}" if [ "$use_headed" = "1" ] && [ -n "${DISPLAY:-}" ]; then nohup "$CHROME" --user-data-dir="$PROFILE_DIR" --no-sandbox --disable-dev-shm-usage \ "$URL" >>"$LOG" 2>&1 < /dev/null & echo "opened URL in existing profile: $URL" fi exit 0 fi
ARGS=( --user-data-dir="$PROFILE_DIR" --remote-debugging-port="$CDP_PORT" --remote-debugging-address=127.0.0.1 --no-first-run --no-default-browser-check --no-sandbox --disable-dev-shm-usage --disable-gpu --window-size=1400,900 ) if [ "$use_headed" = "1" ]; then export DISPLAY="${DISPLAY:-:1}" else ARGS+=(--headless=new) fi
nohup "$CHROME" "${ARGS[@]}" "$URL" >>"$LOG" 2>&1 < /dev/null & echo "started chrome pid=$! profile=$PROFILE_DIR cdp=$CDP_PORT headed=$use_headed url=$URL"
for i in $(seq 1 30); do if port_open "$CDP_PORT"; then echo "CDP ready http://127.0.0.1:${CDP_PORT}" exit 0 fi sleep 1 done echo "CDP port not open; see $LOG" >&2 tail -n 40 "$LOG" 2>/dev/null || true exit 1
HERMES_CHROME_EOF RUN cat > /usr/local/share/hermes/google-chrome-wrapper <<'HERMES_CHROME_WRAP_EOF'
set -u PROFILE_DIR="${HERMES_BROWSER_PROFILE:-/mnt/workspace/hailing/browser-profile}" CDP_PORT="${HERMES_BROWSER_CDP_PORT:-9222}" LAUNCHER="/mnt/workspace/hailing/bin/hermes-chrome-persist" mkdir -p "$PROFILE_DIR" 2>/dev/null || true
if [ -x "$LAUNCHER" ]; then export DISPLAY="${DISPLAY:-:1}" export HERMES_BROWSER_HEADED="${HERMES_BROWSER_HEADED:-1}" if [ "$#" -gt 0 ]; then exec "$LAUNCHER" "$@" else exec "$LAUNCHER" about:blank fi fi
CHROME="/usr/bin/google-chrome-stable" [ -x "$CHROME" ] || CHROME="/usr/bin/google-chrome" exec "$CHROME" \ --user-data-dir="$PROFILE_DIR" \ --remote-debugging-port="$CDP_PORT" \ --remote-debugging-address=127.0.0.1 \ --no-first-run \ --no-default-browser-check \ --no-sandbox \ --disable-dev-shm-usage \ --disable-gpu \ --test-type \ "$@"
HERMES_CHROME_WRAP_EOF RUN install -m 0755 /usr/local/share/hermes/hermes-chrome-persist /usr/local/bin/hermes-chrome-persist && \ install -m 0755 /usr/local/share/hermes/google-chrome-wrapper /usr/local/bin/google-chrome-wrapper && \ mkdir -p /mnt/workspace/hailing/bin /mnt/workspace/hailing/browser-profile && \ cp -f /usr/local/share/hermes/hermes-chrome-persist /mnt/workspace/hailing/bin/hermes-chrome-persist && \ chmod +x /mnt/workspace/hailing/bin/hermes-chrome-persist /usr/local/bin/hermes-chrome-persist /usr/local/bin/google-chrome-wrapper
RUN cat > /usr/local/bin/hailing-register-secret <<'HERMES_REG_EOF'
"""Register an account secret for 海灵: write ~/.hermes/.env + accounts.md index.
Usage examples: hailing-register-secret --name github-work --env GITHUB_TOKEN_WORK --value 'ghp_xxx' \\ --type api_token --username myuser --purpose '工作仓库' --url 'https://github.com'
hailing-register-secret --name site-foo --vault-only --username u --purpose '某站' \\ --url 'https://example.com' --note '密码用户稍后提供'
Never prints the secret value back. """ from __future__ import annotations
import argparse import re import sys from pathlib import Path
ENV_PATH = Path("/root/.hermes/.env") ACCOUNTS = Path("/mnt/workspace/hailing/secrets/accounts.md") WS_ENV = Path("/mnt/workspace/root/.hermes/.env")
def valid_env_name(name: str) -> bool: return bool(re.fullmatch(r"[A-Z][A-Z0-9_]{1,63}", name))
def valid_short_name(name: str) -> bool: return bool(re.fullmatch(r"[a-z][a-z0-9_-]{1,47}", name))
def upsert_env(key: str, value: str) -> None: ENV_PATH.parent.mkdir(parents=True, exist_ok=True) text = ENV_PATH.read_text(encoding="utf-8", errors="replace") if ENV_PATH.exists() else "" lines = text.splitlines() out, found = [], False for ln in lines: if ln.startswith(f"{key}="): out.append(f"{key}={value}") found = True else: out.append(ln) if not found: if out and out[-1].strip(): out.append("") out.append(f"# hailing auto-registered") out.append(f"{key}={value}") ENV_PATH.write_text("\n".join(out) + "\n", encoding="utf-8") try: ENV_PATH.chmod(0o600) except Exception: pass if WS_ENV.parent.exists(): try: WS_ENV.write_text(ENV_PATH.read_text(encoding="utf-8"), encoding="utf-8") WS_ENV.chmod(0o600) except Exception: pass
def upsert_accounts( name: str, *, typ: str, env_key: str | None, vault: str | None, username: str, purpose: str, url: str, note: str, ) -> None: ACCOUNTS.parent.mkdir(parents=True, exist_ok=True) if not ACCOUNTS.exists(): ACCOUNTS.write_text("# 账号索引(无完整 secret)\n\n", encoding="utf-8")
read_via = f"env:{env_key}" if env_key else (f"vault:{vault}" if vault else "unknown") section = ( f"## {name}\n" f"- 类型: {typ}\n" f"- 用户名: {username or '(未填)'}\n" f"- 登录/控制台: {url or '(未填)'}\n" f"- 用途: {purpose or '(未填)'}\n" f"- 读取方式: {read_via}\n" f"- 备注: {note or '由海灵自动登记;勿在回复中打印 secret'}\n" )
text = ACCOUNTS.read_text(encoding="utf-8", errors="replace") pattern = re.compile( rf"(?ms)^## {re.escape(name)}\n.*?(?=^## |\Z)" ) if pattern.search(text): text = pattern.sub(section + "\n", text) else: text = text.rstrip() + "\n\n" + section + "\n" ACCOUNTS.write_text(text, encoding="utf-8")
def main() -> int: p = argparse.ArgumentParser(description="Register secret for 海灵") p.add_argument("--name", required=True, help="short name e.g. github-work") p.add_argument("--type", dest="typ", default="api_token", choices=["api_token", "password", "oauth", "other"]) p.add_argument("--env", dest="env_key", default="", help="ENV var name e.g. GITHUB_TOKEN_WORK") p.add_argument("--value", default="", help="secret value (api token/password)") p.add_argument("--vault", default="", help="vault entry name if not using env") p.add_argument("--vault-only", action="store_true", help="only write index, no .env value") p.add_argument("--username", default="") p.add_argument("--purpose", default="") p.add_argument("--url", default="") p.add_argument("--note", default="") args = p.parse_args()
name = args.name.strip().lower() if not valid_short_name(name): print("ERROR: --name must be like github-work (lowercase, start letter)", file=sys.stderr) return 2
env_key = (args.env_key or "").strip().upper() value = args.value vault = (args.vault or "").strip()
if args.vault_only: if not vault: vault = name upsert_accounts( name, typ=args.typ, env_key=None, vault=vault, username=args.username, purpose=args.purpose, url=args.url, note=args.note or "仅索引;密文不在 env", ) print(f"OK index-only name={name} vault={vault}") return 0
if not env_key: env_key = re.sub(r"[^a-z0-9]+", "_", name).upper().strip("_") if not env_key[0].isalpha(): env_key = "ACC_" + env_key if not valid_env_name(env_key): print("ERROR: invalid env name", env_key, file=sys.stderr) return 2 if not value: print("ERROR: --value required unless --vault-only", file=sys.stderr) return 2
upsert_env(env_key, value) upsert_accounts( name, typ=args.typ, env_key=env_key, vault=None, username=args.username, purpose=args.purpose, url=args.url, note=args.note, ) print(f"OK registered name={name} env={env_key} value_len={len(value)} index=accounts.md") return 0
if __name__ == "__main__": raise SystemExit(main())
HERMES_REG_EOF
RUN chmod +x /usr/local/bin/hermes-boot.sh /usr/local/lib/hermes-startup/main.sh /root/bz-startup/main.sh \ /usr/local/bin/hermes-gateway-watchdog.sh /usr/local/bin/hermes-chrome-persist \ /usr/local/bin/google-chrome-wrapper /usr/local/bin/hailing-register-secret 2>/dev/null || true EXPOSE 7860 22 9119 ENTRYPOINT ["/usr/local/bin/hermes-boot.sh"]
|