mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-06 02:37:05 +08:00
Adds an optional dashboard side-process to the container entrypoint,
toggled by `HERMES_DASHBOARD=1` (also accepts `true` / `yes`). When set,
the entrypoint backgrounds `hermes dashboard` before `exec`-ing the main
command so the user's chosen foreground process (gateway, chat, `sleep
infinity`, …) remains PID-of-interest for the container runtime.
docker run -d \
-v ~/.hermes:/opt/data \
-p 8642:8642 -p 9119:9119 \
-e HERMES_DASHBOARD=1 \
nousresearch/hermes-agent gateway run
Defaults chosen for the container case:
- Host: 0.0.0.0 (reachable through published port; can override to
127.0.0.1 via HERMES_DASHBOARD_HOST for sidecar/reverse-proxy setups)
- Port: 9119 (matches `hermes dashboard`)
- Auto-adds `--insecure` when binding to non-localhost, matching the
dashboard's own safety gate for exposing API keys
- HERMES_DASHBOARD_TUI is read by `hermes dashboard` directly — no
entrypoint plumbing needed
Dashboard output is prefixed with `[dashboard]` via `stdbuf`+`sed -u` so
it's easy to separate from gateway logs in `docker logs`. No supervision:
if the dashboard crashes it stays down until the container restarts
(documented in the `:::note` panel).
Other changes bundled in:
- Deprecate GATEWAY_HEALTH_URL / GATEWAY_HEALTH_TIMEOUT env vars in
hermes_cli/web_server.py with a DEPRECATED block comment and a
`.. deprecated::` note on _probe_gateway_health. The feature still
works for this release; it'll be removed alongside the move to a
first-class dashboard config key.
- Rewrite the "Running the dashboard" doc section around the new
single-container pattern. Drops the previously-documented
dashboard-as-its-own-container setup — that pattern relied on the
deprecated env vars for cross-container gateway-liveness detection,
and without them the dashboard would permanently report the gateway
as "not running".
- Collapse the two-service Compose example (gateway + dashboard
container) into a single service with HERMES_DASHBOARD=1. Removes
the now-unnecessary bridge network and `depends_on`.
- Drop the ":::warning" caveat about "Running a dashboard container
alongside the gateway is safe" — that case no longer exists.
140 lines
6.2 KiB
Bash
Executable File
140 lines
6.2 KiB
Bash
Executable File
#!/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"
|
|
|
|
# --- Privilege dropping via gosu ---
|
|
# When started as root (the default for Docker, or fakeroot in rootless Podman),
|
|
# optionally remap the hermes user/group to match host-side ownership, fix volume
|
|
# permissions, then re-exec as hermes.
|
|
if [ "$(id -u)" = "0" ]; then
|
|
if [ -n "$HERMES_UID" ] && [ "$HERMES_UID" != "$(id -u hermes)" ]; then
|
|
echo "Changing hermes UID to $HERMES_UID"
|
|
usermod -u "$HERMES_UID" hermes
|
|
fi
|
|
|
|
if [ -n "$HERMES_GID" ] && [ "$HERMES_GID" != "$(id -g hermes)" ]; then
|
|
echo "Changing hermes GID to $HERMES_GID"
|
|
# -o allows non-unique GID (e.g. macOS GID 20 "staff" may already exist
|
|
# as "dialout" in the Debian-based container image)
|
|
groupmod -o -g "$HERMES_GID" hermes 2>/dev/null || true
|
|
fi
|
|
|
|
# Fix ownership of the data volume. When HERMES_UID remaps the hermes user,
|
|
# files created by previous runs (under the old UID) become inaccessible.
|
|
# Always chown -R when UID was remapped; otherwise only if top-level is wrong.
|
|
actual_hermes_uid=$(id -u hermes)
|
|
needs_chown=false
|
|
if [ -n "$HERMES_UID" ] && [ "$HERMES_UID" != "10000" ]; then
|
|
needs_chown=true
|
|
elif [ "$(stat -c %u "$HERMES_HOME" 2>/dev/null)" != "$actual_hermes_uid" ]; then
|
|
needs_chown=true
|
|
fi
|
|
if [ "$needs_chown" = true ]; then
|
|
echo "Fixing ownership of $HERMES_HOME to hermes ($actual_hermes_uid)"
|
|
# In rootless Podman the container's "root" is mapped to an unprivileged
|
|
# host UID — chown will fail. That's fine: the volume is already owned
|
|
# by the mapped user on the host side.
|
|
chown -R hermes:hermes "$HERMES_HOME" 2>/dev/null || \
|
|
echo "Warning: chown failed (rootless container?) — continuing anyway"
|
|
fi
|
|
|
|
# Ensure config.yaml is readable by the hermes runtime user even if it was
|
|
# edited on the host after initial ownership setup. Must run here (as root)
|
|
# rather than after the gosu drop, otherwise a non-root caller like
|
|
# `docker run -u $(id -u):$(id -g)` hits "Operation not permitted" (#15865).
|
|
if [ -f "$HERMES_HOME/config.yaml" ]; then
|
|
chown hermes:hermes "$HERMES_HOME/config.yaml" 2>/dev/null || true
|
|
chmod 640 "$HERMES_HOME/config.yaml" 2>/dev/null || true
|
|
fi
|
|
|
|
echo "Dropping root privileges"
|
|
exec gosu hermes "$0" "$@"
|
|
fi
|
|
|
|
# --- Running as hermes from here ---
|
|
source "${INSTALL_DIR}/.venv/bin/activate"
|
|
|
|
# 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
|
|
|
|
# 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 <image> -> exec `hermes` with no args (legacy default)
|
|
# docker run <image> chat -q "..." -> exec `hermes chat -q "..."` (legacy wrap)
|
|
# docker run <image> sleep infinity -> exec `sleep infinity` directly
|
|
# docker run <image> 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 <image> <subcommand>` behavior.
|
|
if [ $# -gt 0 ] && command -v "$1" >/dev/null 2>&1; then
|
|
exec "$@"
|
|
fi
|
|
exec hermes "$@"
|