diff --git a/docker_hermes/demo/.env.example b/docker_hermes/demo/.env.example index eb33927..01eeabc 100644 --- a/docker_hermes/demo/.env.example +++ b/docker_hermes/demo/.env.example @@ -7,7 +7,7 @@ TZ=Asia/Shanghai # Image to run. Build with REGISTRY_DST=quay.io via tool.sh before starting Compose. # Default local output: quay.io/labnow/hermes:local (no automatic push). -HERMES_IMAGE=quay.io/labnow/hermes:local +HERMES_IMAGE=quay.io/labnow/hermes # Host-side persistent data directory. HERMES_DATA_DIR=../../.data/hermes @@ -17,7 +17,6 @@ HERMES_DASHBOARD=true HERMES_DASHBOARD_HOST=0.0.0.0 HERMES_DASHBOARD_PUBLISH_HOST=127.0.0.1 HERMES_DASHBOARD_PORT=9119 -HERMES_DASHBOARD_INSECURE=false # Dashboard basic auth for local development. # Default local password: hermes-local diff --git a/docker_hermes/demo/docker-compose.yml b/docker_hermes/demo/docker-compose.yml index d17eacd..d04c4d9 100644 --- a/docker_hermes/demo/docker-compose.yml +++ b/docker_hermes/demo/docker-compose.yml @@ -1,24 +1,22 @@ -name: "svc-hermes" +name: "${PROFILE_ENV:-X}-svc-hermes" services: hermes: container_name: ${HERMES_CONTAINER_NAME:-svc-hermes} hostname: svc-hermes image: "${HERMES_IMAGE:-quay.io/labnow/hermes:latest}" - pull_policy: if_not_present + pull_policy: always restart: unless-stopped - # Keep standalone Compose behavior explicit even if the image CMD changes. - command: ["start-hermes.sh", "all"] + env_file: [".env.example"] environment: TZ: ${TZ:-Asia/Shanghai} HERMES_HOME: /root/.hermes HERMES_DASHBOARD: ${HERMES_DASHBOARD:-true} HERMES_DASHBOARD_HOST: ${HERMES_DASHBOARD_HOST:-0.0.0.0} HERMES_DASHBOARD_PORT: ${HERMES_DASHBOARD_PORT:-9119} - HERMES_DASHBOARD_INSECURE: ${HERMES_DASHBOARD_INSECURE:-false} - HERMES_DASHBOARD_BASIC_AUTH_USERNAME: ${HERMES_DASHBOARD_BASIC_AUTH_USERNAME:-} + HERMES_DASHBOARD_BASIC_AUTH_USERNAME: ${HERMES_DASHBOARD_BASIC_AUTH_USERNAME:-hermes} + HERMES_DASHBOARD_BASIC_AUTH_PASSWORD: ${HERMES_DASHBOARD_BASIC_AUTH_PASSWORD:-hermes} HERMES_DASHBOARD_BASIC_AUTH_PASSWORD_HASH: ${HERMES_DASHBOARD_BASIC_AUTH_PASSWORD_HASH:-} - HERMES_DASHBOARD_BASIC_AUTH_PASSWORD: ${HERMES_DASHBOARD_BASIC_AUTH_PASSWORD:-} HERMES_DASHBOARD_BASIC_AUTH_SECRET: ${HERMES_DASHBOARD_BASIC_AUTH_SECRET:-} HERMES_DASHBOARD_BASIC_AUTH_TTL_SECONDS: ${HERMES_DASHBOARD_BASIC_AUTH_TTL_SECONDS:-} OPENAI_API_KEY: ${OPENAI_API_KEY:-} @@ -29,16 +27,13 @@ services: DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-} ports: - "${HERMES_DASHBOARD_PUBLISH_HOST:-127.0.0.1}:${HERMES_DASHBOARD_PORT:-9119}:${HERMES_DASHBOARD_PORT:-9119}" - healthcheck: - test: ["CMD", "/usr/local/bin/healthcheck-hermes.sh"] - interval: 10s - timeout: 5s - start_period: 20s - retries: 3 volumes: - - ${HERMES_DATA_DIR:-../../.data/hermes}:/root/.hermes + - ${HERMES_DATA_DIR:-/data/storage/workspace/hermes}:/root/.hermes logging: driver: "json-file" options: max-size: "10m" max-file: "3" + # Keep standalone Compose behavior explicit even if the image CMD changes. + # command: ["tail", "-f", "/dev/null"] + command: ["start-hermes.sh", "all"] diff --git a/docker_hermes/hermes.Dockerfile b/docker_hermes/hermes.Dockerfile index 5da15e9..1916e81 100644 --- a/docker_hermes/hermes.Dockerfile +++ b/docker_hermes/hermes.Dockerfile @@ -24,15 +24,14 @@ RUN set -eux \ && . /opt/utils/script-utils.sh && install_apt /opt/utils/install_list_hermes.apt \ ## Clone source (full clone for reproducibility; depth 1 for speed) && git clone --depth 1 --branch main https://github.com/nousresearch/hermes-agent.git . \ - && chmod +x /opt/utils/*.sh && mv /opt/utils/*hermes*.sh /opt/utils/install_list_hermes.apt /opt/hermes/ \ + && chmod +x /opt/utils/*.sh && mv /opt/utils/*hermes*.sh /opt/utils/install_list_hermes.apt /opt/utils/supervisord.conf /opt/hermes/ \ ## ---------- hack python-olm for building compatible wheels ---------- && mkdir -pv /opt/hermes/vendor \ && mkdir -pv /tmp/olm && cd /tmp/olm \ && curl -s https://pypi.org/pypi/python-olm/3.2.16/json \ | jq -r '.urls[] | select(.packagetype=="sdist").url' \ | xargs curl -L -o python-olm-3.2.16.tar.gz \ - && tar xf python-olm-3.2.16.tar.gz \ - && cd python-olm-3.2.16 \ + && tar xf python-olm-3.2.16.tar.gz && cd python-olm-3.2.16 \ && sed -i 's/cmake_minimum_required(VERSION [0-9.]*)/cmake_minimum_required(VERSION 3.5)/' libolm/CMakeLists.txt \ && pip wheel . --no-build-isolation -w /tmp/olm/wheels \ && mv /tmp/olm/wheels/*olm*.whl /opt/hermes/vendor/ \ @@ -65,11 +64,10 @@ LABEL maintainer="postmaster@labnow.ai" # Production environment ENV NODE_ENV=production -ENV HERMES_HOME=/root/.hermes ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright ENV PYTHONPATH="/opt/hermes:${PYTHONPATH:-}" -ENV PATH="/opt/hermes/bin:/opt/node/bin:/opt/conda/bin:/root/.local/bin:${PATH}" -ENV HOME=/root/.hermes +ENV HERMES_HOME=/root/.hermes +ENV HERMES_ALLOW_ROOT_GATEWAY=1 # Copy the full hermes install tree from the builder (source + browsers + built frontends) COPY --from=builder /opt/hermes /opt/hermes @@ -77,11 +75,12 @@ COPY --from=builder /opt/hermes /opt/hermes # Keep explicit versioned fallbacks around in case detection runs before the first pip install. RUN set -eux && cd /opt/hermes \ && . /opt/utils/script-utils.sh && install_apt /opt/hermes/install_list_hermes.apt \ - && uv pip install ./vendor/*.whl \ + && uv pip install ./vendor/*.whl && rm -rf ./vendor \ && uv pip install -e ".[all,messaging,anthropic,bedrock,azure-identity,hindsight,matrix]" \ - && ln -sf /opt/hermes/*hermes*.sh /usr/local/bin/ \ - && ln -sf /opt/hermes/bin/*hermes*.sh /usr/local/bin/ \ + && rm -rf /opt/hermes/bin \ + && ln -sf /opt/hermes/start-hermes.sh /opt/conda/bin/hermes /usr/local/bin/ \ && . /opt/utils/script-setup-sys.sh && setup_supervisord \ + && mkdir -pv /etc/supervisord/ && mv /opt/hermes/supervisord.conf /etc/supervisord/supervisord.conf \ && install__clean # Data persistence is owned by the runtime orchestrator. @@ -92,5 +91,5 @@ RUN set -eux && cd /opt/hermes \ WORKDIR /root/.hermes CMD ["start-hermes.sh", "all"] EXPOSE 9119 -HEALTHCHECK --interval=10s --timeout=5s --start-period=40s --retries=5 \ - CMD ["/usr/local/bin/healthcheck-hermes.sh"] +HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=5 \ + CMD ["/usr/local/bin/start-hermes.sh", "healthcheck"] diff --git a/docker_hermes/work/healthcheck-hermes.sh b/docker_hermes/work/healthcheck-hermes.sh deleted file mode 100644 index b17cd2c..0000000 --- a/docker_hermes/work/healthcheck-hermes.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env sh -set -eu - -# Dashboard exposes the public, read-only /api/status liveness endpoint. -# Gateway-only containers have no Dashboard, so fall back to the foreground gateway process in that mode. -port="${HERMES_DASHBOARD_PORT:-9119}" -dashboard_enabled="${HERMES_DASHBOARD:-true}" - -case "$dashboard_enabled" in - 0|false|FALSE|False|no|NO|No) - ;; - *) - if curl --fail --silent --show-error --max-time 3 \ - "http://127.0.0.1:${port}/api/status" >/dev/null 2>&1; then - exit 0 - fi - ;; -esac - -if pgrep -f '[h]ermes gateway' >/dev/null 2>&1; then - exit 0 -fi - -exit 1 diff --git a/docker_hermes/work/start-hermes-supervisord.sh b/docker_hermes/work/start-hermes-supervisord.sh deleted file mode 100644 index 661e911..0000000 --- a/docker_hermes/work/start-hermes-supervisord.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash -# Generate /etc/supervisord.conf dynamically and run supervisord. -# This is the standalone `start-hermes.sh all` compatibility path. Workspace -# wrappers should manage gateway/dashboard directly through their own supervisor. -set -eu - -# Set up dashboard parameters -dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}" -dash_port="${HERMES_DASHBOARD_PORT:-9119}" -hermes_home="${HERMES_HOME:-/root/.hermes}" - -insecure="" -case "${HERMES_DASHBOARD_INSECURE:-}" in - 1|true|TRUE|True|yes|YES|Yes) insecure="--insecure" ;; -esac - -# Standalone `all` mode means Gateway and Dashboard together. An explicit -# HERMES_DASHBOARD=false can still disable Dashboard for debugging, but an -# unset variable must not silently start a Gateway-only container. -dashboard_autostart="true" -case "${HERMES_DASHBOARD:-}" in - 0|false|FALSE|False|no|NO|No) dashboard_autostart="false" ;; -esac - -# Generate /etc/supervisord.conf -cat < /etc/supervisord.conf -[supervisord] -nodaemon=true -user=root -logfile=/var/log/supervisord.log -pidfile=/var/run/supervisord.pid - -[program:gateway] -command=hermes gateway run --replace -directory=${hermes_home} -autostart=true -autorestart=true -stdout_logfile=/dev/stdout -stderr_logfile=/dev/stderr -stdout_logfile_maxbytes=0 -stderr_logfile_maxbytes=0 - -[program:dashboard] -command=hermes dashboard --host ${dash_host} --port ${dash_port} --no-open ${insecure} -directory=${hermes_home} -autostart=${dashboard_autostart} -autorestart=true -stdout_logfile=/dev/stdout -stderr_logfile=/dev/stderr -stdout_logfile_maxbytes=0 -stderr_logfile_maxbytes=0 -EOF - -echo "[start-hermes] Starting supervisord..." -exec supervisord -c /etc/supervisord.conf diff --git a/docker_hermes/work/start-hermes.sh b/docker_hermes/work/start-hermes.sh index 8a0d26e..3620922 100644 --- a/docker_hermes/work/start-hermes.sh +++ b/docker_hermes/work/start-hermes.sh @@ -2,42 +2,44 @@ set -eu HERMES_HOME="${HERMES_HOME:-/root/.hermes}" -mkdir -p "$HERMES_HOME" +mkdir -pv "$HERMES_HOME" + +# ── healthcheck ──────────────────────────────────────────────────────────────── +healthcheck() { + port="${HERMES_DASHBOARD_PORT:-9119}" + case "${HERMES_DASHBOARD:-true}" in + 0|false|FALSE|False|no|NO|No) ;; + *) + if curl -fsS --max-time 3 "http://127.0.0.1:${port}/api/status" >/dev/null 2>&1; then + exit 0 + fi + ;; + esac + pgrep -f '[h]ermes gateway' >/dev/null 2>&1 && exit 0 + exit 1 +} +# ── bootstrap ───────────────────────────────────────────────────────────────── bootstrap_lock_dir="$HERMES_HOME/.hermes-bootstrap.lock" bootstrap_marker="$HERMES_HOME/.hermes-bootstrap-v1.complete" seed_one() { - local dest=$1 - local src=$2 - if [ ! -f "$HERMES_HOME/$dest" ] && [ -f "/opt/hermes/$src" ]; then - cp "/opt/hermes/$src" "$HERMES_HOME/$dest" - fi + local dest=$1 src=$2 + [ ! -f "$HERMES_HOME/$dest" ] && [ -f "/opt/hermes/$src" ] && cp "/opt/hermes/$src" "$HERMES_HOME/$dest" } run_bootstrap() { for sub in cron sessions logs hooks memories skills skins plans workspace home pairing platforms/pairing logs/gateways; do - mkdir -p "$HERMES_HOME/$sub" + mkdir -pv "$HERMES_HOME/$sub" done - seed_one ".env" ".env.example" seed_one "config.yaml" "cli-config.yaml.example" seed_one "SOUL.md" "docker/SOUL.md" - - if [ -f "$HERMES_HOME/.env" ]; then - chmod 600 "$HERMES_HOME/.env" 2>/dev/null || true - fi - - # Keep old examples that mounted /opt/data working while standardizing on HERMES_HOME. - if [ ! -e /opt/data ]; then - ln -s "$HERMES_HOME" /opt/data 2>/dev/null || true - fi + [ -f "$HERMES_HOME/.env" ] && chmod 600 "$HERMES_HOME/.env" 2>/dev/null || true + [ ! -e /opt/data ] && ln -s "$HERMES_HOME" /opt/data 2>/dev/null || true if [ -f "$HERMES_HOME/config.yaml" ]; then - if ! python3 -m scripts.docker_config_migrate; then - echo "[start-hermes] config migration failed" >&2 - return 1 - fi + python3 -m scripts.docker_config_migrate || { echo "[start-hermes] config migration failed" >&2; return 1; } fi if [ ! -f "$HERMES_HOME/auth.json" ] && [ -n "${HERMES_AUTH_JSON_BOOTSTRAP:-}" ]; then @@ -50,29 +52,20 @@ run_bootstrap() { fi if [ -d "/opt/hermes/skills" ]; then - if ! python3 -m tools.skills_sync; then - echo "[start-hermes] skills sync failed" >&2 - return 1 - fi + python3 -m tools.skills_sync || { echo "[start-hermes] skills sync failed" >&2; return 1; } fi - touch "$bootstrap_marker" } while [ ! -f "$bootstrap_marker" ]; do if mkdir "$bootstrap_lock_dir" 2>/dev/null; then printf '%s\n' "$$" > "$bootstrap_lock_dir/pid" - if [ ! -f "$bootstrap_marker" ]; then - run_bootstrap - fi + [ ! -f "$bootstrap_marker" ] && run_bootstrap rmdir "$bootstrap_lock_dir" 2>/dev/null || true break fi - lock_pid="" - if [ -f "$bootstrap_lock_dir/pid" ]; then - lock_pid=$(cat "$bootstrap_lock_dir/pid" 2>/dev/null || true) - fi + [ -f "$bootstrap_lock_dir/pid" ] && lock_pid=$(cat "$bootstrap_lock_dir/pid" 2>/dev/null || true) if [ -n "$lock_pid" ] && ! kill -0 "$lock_pid" 2>/dev/null; then rm -rf "$bootstrap_lock_dir" continue @@ -80,41 +73,26 @@ while [ ! -f "$bootstrap_marker" ]; do sleep 0.1 done -# The base image Python version can change. Discover bundled static assets instead -# of requiring users to pass HERMES_WEB_DIST/HERMES_TUI_DIR manually. +# ── path detection ──────────────────────────────────────────────────────────── detect_hermes_path() { - local child=$1 - python3 - "$child" <<'PY' 2>/dev/null || true -import pathlib -import sys - -child = sys.argv[1] + python3 - "$1" <<'PY' 2>/dev/null || true +import pathlib, sys try: import hermes_cli except Exception: raise SystemExit(0) - -path = pathlib.Path(hermes_cli.__file__).resolve().parent / child +path = pathlib.Path(hermes_cli.__file__).resolve().parent / sys.argv[1] if path.exists(): print(path) PY } if [ -z "${HERMES_WEB_DIST:-}" ] || [ ! -d "${HERMES_WEB_DIST:-}" ]; then - detected_web_dist="$(detect_hermes_path web_dist)" - if [ -n "$detected_web_dist" ]; then - export HERMES_WEB_DIST="$detected_web_dist" - fi + d=$(detect_hermes_path web_dist); [ -n "$d" ] && export HERMES_WEB_DIST="$d" fi - if [ -z "${HERMES_TUI_DIR:-}" ] || [ ! -d "${HERMES_TUI_DIR:-}" ]; then - detected_tui_dir="$(detect_hermes_path tui_dist)" - if [ -n "$detected_tui_dir" ]; then - export HERMES_TUI_DIR="$detected_tui_dir" - fi + d=$(detect_hermes_path tui_dist); [ -n "$d" ] && export HERMES_TUI_DIR="$d" fi - -# Find agent-browser Playwright binary and set env if [ -z "${AGENT_BROWSER_EXECUTABLE_PATH:-}" ] && [ -n "${PLAYWRIGHT_BROWSERS_PATH:-}" ] && [ -d "$PLAYWRIGHT_BROWSERS_PATH" ]; then browser_bin=$(find "$PLAYWRIGHT_BROWSERS_PATH" -type f -executable \( -name 'chrome' -o -name 'chromium' -o -name 'chrome-headless-shell' -o -name 'headless_shell' -o -name 'chromium-browser' \) 2>/dev/null | head -n 1) if [ -n "$browser_bin" ]; then @@ -123,48 +101,35 @@ if [ -z "${AGENT_BROWSER_EXECUTABLE_PATH:-}" ] && [ -n "${PLAYWRIGHT_BROWSERS_PA fi fi -# Configure environments for command invocation export HOME="${HERMES_HOME}" cd "${HERMES_HOME}" -# Explicit service modes allow an outer supervisor (for example labnow-open) to -# manage Hermes processes directly instead of starting a nested supervisor. -if [ $# -eq 0 ]; then - # The standalone image overrides CMD to use `all`; a direct invocation uses - # the single foreground gateway mode, matching the OpenClaw entry contract. - set -- gateway -fi +# ── dispatch ────────────────────────────────────────────────────────────────── +dash_host="${HERMES_DASHBOARD_HOST:-0.0.0.0}" +dash_port="${HERMES_DASHBOARD_PORT:-9119}" -case "$1" in +case "${1:-all}" in + healthcheck) + healthcheck + ;; all) - if [ $# -ne 1 ]; then - echo "[start-hermes] the all mode does not accept extra arguments" >&2 - exit 2 - fi - exec /opt/hermes/start-hermes-supervisord.sh + echo "[start-hermes] Starting supervisord..." + exec supervisord -c /etc/supervisord/supervisord.conf ;; gateway) shift - if [ $# -eq 0 ]; then - set -- run --replace - fi + [ $# -eq 0 ] && set -- run --replace exec hermes gateway "$@" ;; dashboard) shift - if [ $# -eq 0 ]; then - set -- \ - --host "${HERMES_DASHBOARD_HOST:-0.0.0.0}" \ - --port "${HERMES_DASHBOARD_PORT:-9119}" \ - --no-open - fi + [ $# -eq 0 ] && set -- --host "$dash_host" --port "$dash_port" --no-open exec hermes dashboard "$@" ;; + *) + if command -v "$1" >/dev/null 2>&1; then + exec "$@" + fi + exec hermes "$@" + ;; esac - -# Preserve the generic compatibility behavior for shell commands and direct -# Hermes CLI invocations used during one-off debugging. -if command -v "$1" >/dev/null 2>&1; then - exec "$@" -fi -exec hermes "$@" diff --git a/docker_hermes/work/supervisord.conf b/docker_hermes/work/supervisord.conf new file mode 100644 index 0000000..56ccfaf --- /dev/null +++ b/docker_hermes/work/supervisord.conf @@ -0,0 +1,48 @@ +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[unix_http_server] +file=/var/run/supervisor.sock +chmod=0770 +chown=root:supervisor + +[inet_http_server] +port=:9001 + +[supervisorctl] +serverurl=unix:///var/run/supervisor.sock + +[supervisord] +identifier=lab +nodaemon=true +pidfile=/var/run/supervisord.pid +logfile=/dev/stdout +loglevel=warning +minfds=2048 +minprocs=256 + +[program-default] +# directory=/data +stdout_logfile=/dev/stdout +stderr_logfile=/dev/stderr +redirect_stderr=true +autostart=false +autorestart=true +startsecs=5 +startretries=3 +stopwaitsecs=30 +killasgroup=true +stopasgroup=true + +[group:hermes] +programs=hermes_gateway,hermes_dashboard +priority=999 + +[program:hermes_gateway] +command=start-hermes.sh gateway +priority=10 + +[program:hermes_dashboard] +command=start-hermes.sh dashboard +depends_on=hermes_gateway +priority=20