hermes更新到v2026.05.29
This commit is contained in:
@@ -1,120 +0,0 @@
|
|||||||
#!/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 <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 "$@"
|
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# s6-overlay stage2 hook — runs as root after the supervision tree is
|
||||||
|
# up but before user services start. Handles UID/GID remap, volume
|
||||||
|
# chown, config seeding, and skills sync.
|
||||||
|
#
|
||||||
|
# Per-service privilege drop happens inside each service's `run` script
|
||||||
|
# (and in main-wrapper.sh) via s6-setuidgid, not here.
|
||||||
|
#
|
||||||
|
# Wired into the image as /etc/cont-init.d/01-hermes-setup by the
|
||||||
|
# Dockerfile. The shim at docker/entrypoint.sh forwards to this script
|
||||||
|
# so external references to docker/entrypoint.sh still work.
|
||||||
|
#
|
||||||
|
# NB: cont-init.d scripts run with no arguments — the user's CMD args
|
||||||
|
# are NOT visible here. That's fine: we use Architecture B (s6-overlay
|
||||||
|
# main-program model), so main-wrapper.sh runs the CMD with full
|
||||||
|
# stdin/stdout/stderr access and handles arg parsing there.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
HERMES_HOME="${HERMES_HOME:-/opt/data}"
|
||||||
|
INSTALL_DIR="/opt/hermes"
|
||||||
|
|
||||||
|
# --- Bootstrap HERMES_HOME as root ---
|
||||||
|
# Create the directory (and any missing parents) while we still have root
|
||||||
|
# privileges so the chown checks below see real metadata and the later
|
||||||
|
# `s6-setuidgid hermes mkdir -p` block doesn't EACCES on root-owned
|
||||||
|
# ancestors. Without this, custom HERMES_HOME paths whose parents only
|
||||||
|
# root can create (e.g. `HERMES_HOME=/home/hermes/.hermes` in a Compose
|
||||||
|
# file, or any path under a fresh / not pre-populated by the image)
|
||||||
|
# fail on first boot with `mkdir: cannot create directory '/...': Permission
|
||||||
|
# denied` and the cont-init hook exits non-zero. Idempotent — `mkdir -p`
|
||||||
|
# is a no-op if the dir already exists. (#18482, salvages #18488)
|
||||||
|
mkdir -p "$HERMES_HOME"
|
||||||
|
|
||||||
|
# --- UID/GID remap ---
|
||||||
|
# Accept PUID/PGID as aliases for HERMES_UID/HERMES_GID. NAS users (UGOS,
|
||||||
|
# Synology, unRAID) expect the LinuxServer.io PUID/PGID convention and
|
||||||
|
# bind-mount /opt/data from a host directory owned by their own UID; without
|
||||||
|
# this alias those vars are silently ignored and the s6-setuidgid drop to
|
||||||
|
# UID 10000 leaves the runtime unable to read the volume. HERMES_UID/
|
||||||
|
# HERMES_GID still win when both are set. See #15290, salvages #25872.
|
||||||
|
HERMES_UID="${HERMES_UID:-${PUID:-}}"
|
||||||
|
HERMES_GID="${HERMES_GID:-${PGID:-}}"
|
||||||
|
|
||||||
|
if [ -n "${HERMES_UID:-}" ] && [ "$HERMES_UID" != "$(id -u hermes)" ]; then
|
||||||
|
echo "[stage2] Changing hermes UID to $HERMES_UID"
|
||||||
|
# FIX: HOME 先转移到一个临时目录,改完UID再改回原 HOME
|
||||||
|
usermod -d /tmp/tmp-home hermes
|
||||||
|
usermod -u "$HERMES_UID" hermes
|
||||||
|
usermod -d "$HERMES_HOME" hermes
|
||||||
|
fi
|
||||||
|
if [ -n "${HERMES_GID:-}" ] && [ "$HERMES_GID" != "$(id -g hermes)" ]; then
|
||||||
|
echo "[stage2] 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
|
||||||
|
|
||||||
|
# --- Docker socket group membership (docker-in-docker / DooD) ---
|
||||||
|
# When the user bind-mounts the host Docker daemon socket
|
||||||
|
# (`-v /var/run/docker.sock:/var/run/docker.sock`) to use the `docker`
|
||||||
|
# terminal backend from inside the container, the socket is owned by the
|
||||||
|
# host's `docker` group (or root). The supervised hermes user (UID 10000)
|
||||||
|
# is not a member of any group that matches the socket's GID, so every
|
||||||
|
# `docker` invocation EACCES'es and `check_terminal_requirements()` fails.
|
||||||
|
# See #16703.
|
||||||
|
#
|
||||||
|
# Granting the supp group via `docker run --group-add <gid>` alone is
|
||||||
|
# NOT sufficient with our s6-setuidgid privilege drop: s6-setuidgid (and
|
||||||
|
# gosu, the older shim) calls initgroups() for the target user, which
|
||||||
|
# rebuilds the supplementary group list from /etc/group. Without an
|
||||||
|
# /etc/group entry whose GID matches the socket, the kernel-granted
|
||||||
|
# supp group is silently wiped between PID 1 and the dropped process.
|
||||||
|
# Confirmed empirically: `--group-add 998` alone leaves the dropped
|
||||||
|
# hermes process with `Groups: 10000` (998 gone); after this hook adds
|
||||||
|
# the entry, the dropped process has `Groups: 998 10000` as expected.
|
||||||
|
#
|
||||||
|
# Fix: detect the socket's GID at boot and ensure /etc/group has a
|
||||||
|
# matching entry that includes hermes. Idempotent across container
|
||||||
|
# restarts. Skipped silently when no socket is bind-mounted.
|
||||||
|
#
|
||||||
|
# Handles the awkward corner cases:
|
||||||
|
# - socket owned by GID 0 (root) — some Podman setups; usermod -aG root
|
||||||
|
# - socket GID already used by a known container group (e.g. tty=5):
|
||||||
|
# reuse that group's name rather than creating a duplicate
|
||||||
|
# - hermes is already a member of the right group (idempotent restart)
|
||||||
|
# - chown/groupadd failures under rootless containers — non-fatal
|
||||||
|
for sock in /var/run/docker.sock /run/docker.sock; do
|
||||||
|
[ -S "$sock" ] || continue
|
||||||
|
sock_gid=$(stat -c '%g' "$sock" 2>/dev/null) || continue
|
||||||
|
[ -n "$sock_gid" ] || continue
|
||||||
|
# Already a member? Nothing to do.
|
||||||
|
if id -G hermes 2>/dev/null | tr ' ' '\n' | grep -qx "$sock_gid"; then
|
||||||
|
echo "[stage2] hermes already in group $sock_gid for $sock"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
# Resolve or create a group name for this GID.
|
||||||
|
sock_group=$(getent group "$sock_gid" 2>/dev/null | cut -d: -f1)
|
||||||
|
if [ -z "$sock_group" ]; then
|
||||||
|
sock_group="hostdocker"
|
||||||
|
if ! groupadd -g "$sock_gid" "$sock_group" 2>/dev/null; then
|
||||||
|
echo "[stage2] Warning: groupadd -g $sock_gid $sock_group failed; skipping docker socket group setup"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "[stage2] Created group $sock_group (GID $sock_gid) for Docker socket"
|
||||||
|
fi
|
||||||
|
if usermod -aG "$sock_group" hermes 2>/dev/null; then
|
||||||
|
echo "[stage2] Added hermes to group $sock_group (GID $sock_gid) for $sock"
|
||||||
|
else
|
||||||
|
echo "[stage2] Warning: usermod -aG $sock_group hermes failed; docker backend may fail with EACCES"
|
||||||
|
fi
|
||||||
|
break
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Fix ownership of data volume ---
|
||||||
|
# When HERMES_UID is remapped or the top-level $HERMES_HOME isn't owned by
|
||||||
|
# the runtime hermes UID, restore ownership to hermes — but ONLY for the
|
||||||
|
# directories hermes actually writes to. The full $HERMES_HOME may be a
|
||||||
|
# host-mounted bind containing unrelated user files; `chown -R` would
|
||||||
|
# silently destroy host ownership of those (see issue #19788).
|
||||||
|
#
|
||||||
|
# The canonical list of hermes-owned subdirs is the same one the s6-setuidgid
|
||||||
|
# mkdir -p block below seeds. Keep them in sync if the seed list changes.
|
||||||
|
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
|
||||||
|
|
||||||
|
# FIX: K8S 环境下多此一举了
|
||||||
|
needs_chown=false
|
||||||
|
if [ "$needs_chown" = true ]; then
|
||||||
|
echo "[stage2] Fixing ownership of $HERMES_HOME (targeted) 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.
|
||||||
|
#
|
||||||
|
# Top-level $HERMES_HOME: chown the directory itself (not its contents)
|
||||||
|
# so hermes can mkdir new subdirs but bind-mounted host files keep
|
||||||
|
# their existing ownership.
|
||||||
|
chown hermes:hermes "$HERMES_HOME" 2>/dev/null || \
|
||||||
|
echo "[stage2] Warning: chown $HERMES_HOME failed (rootless container?) — continuing"
|
||||||
|
# Hermes-owned subdirs: recursive chown is safe here because these are
|
||||||
|
# created and managed exclusively by hermes (see the s6-setuidgid mkdir
|
||||||
|
# -p block below for the canonical list).
|
||||||
|
for sub in cron sessions logs hooks memories skills skins plans workspace home profiles; do
|
||||||
|
if [ -e "$HERMES_HOME/$sub" ]; then
|
||||||
|
chown -R hermes:hermes "$HERMES_HOME/$sub" 2>/dev/null || \
|
||||||
|
echo "[stage2] Warning: chown $HERMES_HOME/$sub failed (rootless container?) — continuing"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
# Hermes-owned trees under $INSTALL_DIR must be re-chowned when the UID
|
||||||
|
# is remapped — otherwise:
|
||||||
|
# - .venv: lazy_deps.py cannot install platform packages (discord.py,
|
||||||
|
# telegram, slack, etc.) with EACCES (#15012, #21100)
|
||||||
|
# - ui-tui: esbuild rebuilds dist/entry.js on every TUI launch (when
|
||||||
|
# the source mtime is newer than dist/ or when HERMES_TUI_FORCE_BUILD
|
||||||
|
# is set) and writes to ui-tui/dist/. Without this chown the new
|
||||||
|
# hermes UID can't write the build output (#28851).
|
||||||
|
# - node_modules: root-level dependencies (puppeteer, web tooling)
|
||||||
|
# that runtime code may walk/update.
|
||||||
|
# The set mirrors the build-time `chown -R hermes:hermes` line in the
|
||||||
|
# Dockerfile — keep them in sync if the Dockerfile chown set changes.
|
||||||
|
# These are under $INSTALL_DIR (not $HERMES_HOME), so the bind-mount
|
||||||
|
# concern doesn't apply — recursive is fine.
|
||||||
|
chown -R hermes:hermes \
|
||||||
|
"$INSTALL_DIR/.venv" \
|
||||||
|
"$INSTALL_DIR/ui-tui" \
|
||||||
|
"$INSTALL_DIR/node_modules" \
|
||||||
|
2>/dev/null || \
|
||||||
|
echo "[stage2] Warning: chown of build trees failed (rootless container?) — continuing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Always reset ownership of $HERMES_HOME/profiles to hermes on every
|
||||||
|
# boot. Profile dirs and files can land owned by root when commands
|
||||||
|
# are invoked via `docker exec <container> hermes …` (which defaults
|
||||||
|
# to root unless `-u` is passed), and that breaks the cont-init
|
||||||
|
# reconciler (02-reconcile-profiles) which runs as hermes and walks
|
||||||
|
# the profiles dir. Idempotent; skipped on rootless containers where
|
||||||
|
# chown would fail.
|
||||||
|
if [ -d "$HERMES_HOME/profiles" ]; then
|
||||||
|
chown -R hermes:hermes "$HERMES_HOME/profiles" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- config.yaml permissions ---
|
||||||
|
# Ensure config.yaml is readable by the hermes runtime user even if it
|
||||||
|
# was edited on the host after initial ownership setup.
|
||||||
|
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
|
||||||
|
|
||||||
|
# --- Seed directory structure as hermes user ---
|
||||||
|
# Run as hermes via s6-setuidgid so dirs end up owned correctly (matters
|
||||||
|
# under rootless Podman where chown back to root would fail).
|
||||||
|
#
|
||||||
|
# Use direct `mkdir -p` invocation (no `sh -c "..."` wrapper) so the
|
||||||
|
# shell isn't a second interpreter — defends against $HERMES_HOME values
|
||||||
|
# containing shell metacharacters. PR #30136 review item O2.
|
||||||
|
s6-setuidgid hermes mkdir -p \
|
||||||
|
"$HERMES_HOME/cron" \
|
||||||
|
"$HERMES_HOME/sessions" \
|
||||||
|
"$HERMES_HOME/logs" \
|
||||||
|
"$HERMES_HOME/hooks" \
|
||||||
|
"$HERMES_HOME/memories" \
|
||||||
|
"$HERMES_HOME/skills" \
|
||||||
|
"$HERMES_HOME/skins" \
|
||||||
|
"$HERMES_HOME/plans" \
|
||||||
|
"$HERMES_HOME/workspace" \
|
||||||
|
"$HERMES_HOME/home"
|
||||||
|
|
||||||
|
# --- Install-method stamp (read by detect_install_method() in hermes status) ---
|
||||||
|
# Preserved from the tini-era entrypoint (PR #27843). Must be written as
|
||||||
|
# the hermes user so ownership matches the file's documented owner.
|
||||||
|
# tee is invoked directly via s6-setuidgid (no `sh -c` wrapper) for the
|
||||||
|
# same shell-metacharacter safety described above.
|
||||||
|
printf 'docker\n' | s6-setuidgid hermes tee "$HERMES_HOME/.install_method" >/dev/null \
|
||||||
|
|| true
|
||||||
|
|
||||||
|
# --- Seed config files (only on first boot) ---
|
||||||
|
seed_one() {
|
||||||
|
dest=$1
|
||||||
|
src=$2
|
||||||
|
if [ ! -f "$HERMES_HOME/$dest" ] && [ -f "$INSTALL_DIR/$src" ]; then
|
||||||
|
s6-setuidgid hermes cp "$INSTALL_DIR/$src" "$HERMES_HOME/$dest"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
seed_one ".env" ".env.example"
|
||||||
|
seed_one "config.yaml" "cli-config.yaml.example"
|
||||||
|
seed_one "SOUL.md" "docker/SOUL.md"
|
||||||
|
|
||||||
|
# .env holds API keys and secrets — restrict to owner-only access. Applied
|
||||||
|
# unconditionally (not only on first-seed) so a host-mounted .env that was
|
||||||
|
# created with a permissive umask gets tightened on every container start.
|
||||||
|
if [ -f "$HERMES_HOME/.env" ]; then
|
||||||
|
chown hermes:hermes "$HERMES_HOME/.env" 2>/dev/null || true
|
||||||
|
chmod 600 "$HERMES_HOME/.env" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# auth.json: bootstrap from env on first boot only. Same semantics as the
|
||||||
|
# pre-s6 entrypoint — the [ ! -f ] guard is critical to avoid clobbering
|
||||||
|
# rotated refresh tokens on container restart.
|
||||||
|
if [ ! -f "$HERMES_HOME/auth.json" ] && [ -n "${HERMES_AUTH_JSON_BOOTSTRAP:-}" ]; then
|
||||||
|
printf '%s' "$HERMES_AUTH_JSON_BOOTSTRAP" > "$HERMES_HOME/auth.json"
|
||||||
|
chown hermes:hermes "$HERMES_HOME/auth.json" 2>/dev/null || true
|
||||||
|
chmod 600 "$HERMES_HOME/auth.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Sync bundled skills ---
|
||||||
|
# Invoke the venv's python by absolute path so we don't need a `sh -c`
|
||||||
|
# wrapper to source the activate script. This is safe because
|
||||||
|
# skills_sync.py doesn't depend on any environment exports beyond what
|
||||||
|
# the python binary's own bin-stub already sets up (sys.path is rooted
|
||||||
|
# at the venv's site-packages by virtue of running .venv/bin/python).
|
||||||
|
if [ -d "$INSTALL_DIR/skills" ]; then
|
||||||
|
s6-setuidgid hermes "$INSTALL_DIR/.venv/bin/python" "$INSTALL_DIR/tools/skills_sync.py" \
|
||||||
|
|| echo "[stage2] Warning: skills_sync.py failed; continuing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Discover agent-browser's Chromium binary ---
|
||||||
|
# The image's Dockerfile runs `npx playwright install chromium`, which
|
||||||
|
# populates ``$PLAYWRIGHT_BROWSERS_PATH`` (=/opt/hermes/.playwright) with
|
||||||
|
# a ``chromium_headless_shell-<build>/chrome-headless-shell-linux64/``
|
||||||
|
# directory. agent-browser (the runtime CLI Hermes spawns for the
|
||||||
|
# browser tool) doesn't recognise this layout in its own cache scan and
|
||||||
|
# fails with "Auto-launch failed: Chrome not found" — even though the
|
||||||
|
# binary is right there (#15697).
|
||||||
|
#
|
||||||
|
# Fix: locate the binary at boot and export ``AGENT_BROWSER_EXECUTABLE_PATH``
|
||||||
|
# via /run/s6/container_environment so the `with-contenv` shebang on
|
||||||
|
# main-wrapper.sh propagates it into the supervised ``hermes`` process
|
||||||
|
# and thence to agent-browser subprocesses.
|
||||||
|
#
|
||||||
|
# - Skipped when the user has already set ``AGENT_BROWSER_EXECUTABLE_PATH``
|
||||||
|
# (lets users override with a system Chrome install).
|
||||||
|
# - Filename-matched (not path-matched): the chromium dir contains many
|
||||||
|
# shared libraries (libGLESv2.so, libEGL.so, ...) which inherit the
|
||||||
|
# executable bit from Playwright's tarball but are NOT browser binaries.
|
||||||
|
# We only accept files whose basename is chrome / chromium /
|
||||||
|
# chrome-headless-shell / chromium-browser. Compare PR #18635's earlier
|
||||||
|
# ``find | grep -Ei 'chrome|chromium'`` which would match the path
|
||||||
|
# ``.../chrome-headless-shell-linux64/libGLESv2.so`` and pick a .so.
|
||||||
|
# - Quietly skipped when $PLAYWRIGHT_BROWSERS_PATH doesn't exist (e.g.
|
||||||
|
# custom builds that strip Playwright).
|
||||||
|
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 'chromium-browser' \) \
|
||||||
|
2>/dev/null | head -n 1)
|
||||||
|
if [ -n "$browser_bin" ]; then
|
||||||
|
echo "[stage2] Found agent-browser Chromium binary: $browser_bin"
|
||||||
|
# Write to s6's container_environment so with-contenv picks it
|
||||||
|
# up for all supervised services (main-hermes, dashboard, etc.).
|
||||||
|
# Idempotent: each boot overwrites with the current path.
|
||||||
|
printf '%s' "$browser_bin" > /run/s6/container_environment/AGENT_BROWSER_EXECUTABLE_PATH
|
||||||
|
else
|
||||||
|
echo "[stage2] Warning: no Chromium binary under $PLAYWRIGHT_BROWSERS_PATH; browser tool may fail"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[stage2] Setup complete; starting user services"
|
||||||
+5
-3
@@ -42,7 +42,7 @@ spec:
|
|||||||
httpGet:
|
httpGet:
|
||||||
path: /health
|
path: /health
|
||||||
port: 8642
|
port: 8642
|
||||||
initialDelaySeconds: 60
|
initialDelaySeconds: 180
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
successThreshold: 1
|
successThreshold: 1
|
||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
@@ -81,6 +81,8 @@ spec:
|
|||||||
value: '::'
|
value: '::'
|
||||||
- name: HERMES_DASHBOARD_HOST
|
- name: HERMES_DASHBOARD_HOST
|
||||||
value: '9119'
|
value: '9119'
|
||||||
|
- name: HERMES_DASHBOARD_INSECURE
|
||||||
|
value: "1"
|
||||||
envFrom:
|
envFrom:
|
||||||
- secretRef:
|
- secretRef:
|
||||||
name: hermes
|
name: hermes
|
||||||
@@ -101,8 +103,8 @@ spec:
|
|||||||
- name: tmp
|
- name: tmp
|
||||||
mountPath: /tmp
|
mountPath: /tmp
|
||||||
- name: start-sh
|
- name: start-sh
|
||||||
mountPath: /opt/hermes/docker/entrypoint.sh
|
mountPath: /opt/hermes/docker/stage2-hook.sh
|
||||||
subPath: entrypoint.sh
|
subPath: stage2-hook.sh
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
memory: "1Gi"
|
memory: "1Gi"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ resources:
|
|||||||
images:
|
images:
|
||||||
- name: image
|
- name: image
|
||||||
newName: cr.wetofu.me/nousresearch/hermes-agent
|
newName: cr.wetofu.me/nousresearch/hermes-agent
|
||||||
newTag: v2026.5.16
|
newTag: v2026.5.29
|
||||||
secretGenerator:
|
secretGenerator:
|
||||||
- name: hermes
|
- name: hermes
|
||||||
files:
|
files:
|
||||||
@@ -29,6 +29,9 @@ configMapGenerator:
|
|||||||
- config/TELEGRAM_REQUIRE_MENTION
|
- config/TELEGRAM_REQUIRE_MENTION
|
||||||
- config/SIYUAN_URL
|
- config/SIYUAN_URL
|
||||||
- config/PATH
|
- config/PATH
|
||||||
|
- config/NPM_CONFIG_REGISTRY
|
||||||
|
- config/UV_INDEX_URL
|
||||||
|
- config/PIP_INDEX_URL
|
||||||
- name: hermes-start
|
- name: hermes-start
|
||||||
files:
|
files:
|
||||||
- config/entrypoint.sh
|
- config/stage2-hook.sh
|
||||||
Reference in New Issue
Block a user