#!/bin/bash # Docker/Podman entrypoint: bootstrap config files into the mounted volume, then run hermes. set -e HERMES_HOME="${HERMES_HOME:-/opt/data}" INSTALL_DIR="/opt/hermes" # 这个脚本太烂了,我重新写一个逻辑好了 # 原本的脚本根本没有考虑用户可以自行修改目录属主的问题 if [ "$(id -u)" = "0" ]; then groupadd -g ${HERMES_UID} runner useradd -u ${HERMES_UID} -g ${HERMES_UID} -d ${HERMES_HOME} runner echo "!!! Changing ${INSTALL_DIR}/.venv to ${HERMES_UID}:${HERMES_GID}" chown -R ${HERMES_UID}:${HERMES_GID} "${INSTALL_DIR}/.venv" || exit 1 # Drop root privilege echo "Dropping root privileges" exec gosu ${HERMES_UID} "$0" "$@" fi # --- Running as hermes from here --- source "${INSTALL_DIR}/.venv/bin/activate" # 这狗*的环境变量! export PATH=${HERMES_HOME}/home/.local/bin:${HERMES_HOME}/.local/bin:${INSTALL_DIR}/bin:${PATH} # Create essential directory structure. Cache and platform directories # (cache/images, cache/audio, platforms/whatsapp, etc.) are created on # demand by the application — don't pre-create them here so new installs # get the consolidated layout from get_hermes_dir(). # The "home/" subdirectory is a per-profile HOME for subprocesses (git, # ssh, gh, npm …). Without it those tools write to /root which is # ephemeral and shared across profiles. See issue #4426. mkdir -p "$HERMES_HOME"/{cron,sessions,logs,hooks,memories,skills,skins,plans,workspace,home} # .env if [ ! -f "$HERMES_HOME/.env" ]; then cp "$INSTALL_DIR/.env.example" "$HERMES_HOME/.env" fi # config.yaml if [ ! -f "$HERMES_HOME/config.yaml" ]; then cp "$INSTALL_DIR/cli-config.yaml.example" "$HERMES_HOME/config.yaml" fi # SOUL.md if [ ! -f "$HERMES_HOME/SOUL.md" ]; then cp "$INSTALL_DIR/docker/SOUL.md" "$HERMES_HOME/SOUL.md" fi # auth.json: bootstrap from env on first boot only. Used by orchestrators # (e.g. provisioning a Hermes VPS from an account-management service) that # need to seed the OAuth refresh credential non-interactively, instead of # walking the user through `hermes setup` + the device-flow login dance. # Subsequent token rotations write back to the same file, which lives on a # persistent volume — so this env var is consumed exactly once at first # boot. The `[ ! -f ... ]` guard is critical: without it, a container # restart would clobber a rotated refresh token with the now-stale value # the orchestrator originally seeded. if [ ! -f "$HERMES_HOME/auth.json" ] && [ -n "$HERMES_AUTH_JSON_BOOTSTRAP" ]; then printf '%s' "$HERMES_AUTH_JSON_BOOTSTRAP" > "$HERMES_HOME/auth.json" chmod 600 "$HERMES_HOME/auth.json" fi # Sync bundled skills (manifest-based so user edits are preserved) if [ -d "$INSTALL_DIR/skills" ]; then python3 "$INSTALL_DIR/tools/skills_sync.py" fi # Optionally start `hermes dashboard` as a side-process. # # Toggled by HERMES_DASHBOARD=1 (also accepts "true"/"yes", case-insensitive). # Host/port/TUI can be overridden via: # HERMES_DASHBOARD_HOST (default 0.0.0.0 — exposed outside the container) # HERMES_DASHBOARD_PORT (default 9119, matches `hermes dashboard` default) # HERMES_DASHBOARD_TUI (already honored by `hermes dashboard` itself) # # The dashboard is a long-lived server. We background it *before* the final # `exec hermes "$@"` so the user's chosen foreground command (chat, gateway, # sleep infinity, …) remains PID-of-interest for the container runtime. When # the container stops the whole process tree is torn down, so no explicit # cleanup is needed. case "${HERMES_DASHBOARD:-}" in 1|true|TRUE|True|yes|YES|Yes) dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}" dash_port="${HERMES_DASHBOARD_PORT:-9119}" dash_args=(--host "$dash_host" --port "$dash_port" --no-open) # Binding to anything other than localhost requires --insecure — the # dashboard refuses otherwise because it exposes API keys. Inside a # container this is the expected deployment (host reaches it via # published port), so opt in automatically. if [ "$dash_host" != "127.0.0.1" ] && [ "$dash_host" != "localhost" ]; then dash_args+=(--insecure) fi echo "Starting hermes dashboard on ${dash_host}:${dash_port} (background)" # Prefix dashboard output so it's distinguishable from the main # process in `docker logs`. stdbuf keeps the pipe line-buffered. ( stdbuf -oL -eL hermes dashboard "${dash_args[@]}" 2>&1 \ | sed -u 's/^/[dashboard] /' ) & ;; esac # Final exec: two supported invocation patterns. # # docker run -> exec `hermes` with no args (legacy default) # docker run chat -q "..." -> exec `hermes chat -q "..."` (legacy wrap) # docker run sleep infinity -> exec `sleep infinity` directly # docker run bash -> exec `bash` directly # # If the first positional arg resolves to an executable on PATH, we assume the # caller wants to run it directly (needed by the launcher which runs long-lived # `sleep infinity` sandbox containers — see tools/environments/docker.py). # Otherwise we treat the args as a hermes subcommand and wrap with `hermes`, # preserving the documented `docker run ` behavior. if [ $# -gt 0 ] && command -v "$1" >/dev/null 2>&1; then exec "$@" fi exec hermes "$@"