diff --git a/hermes/config/entrypoint.sh b/hermes/config/entrypoint.sh new file mode 100644 index 0000000..05e40a9 --- /dev/null +++ b/hermes/config/entrypoint.sh @@ -0,0 +1,120 @@ +#!/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 "$@" \ No newline at end of file diff --git a/hermes/deploy.yaml b/hermes/deploy.yaml index ad128cc..a504178 100644 --- a/hermes/deploy.yaml +++ b/hermes/deploy.yaml @@ -4,7 +4,6 @@ metadata: name: hermes namespace: hermes spec: - strategy: type: Recreate replicas: 1 @@ -16,10 +15,6 @@ spec: labels: app: hermes spec: - securityContext: - runAsUser: 10000 - supplementalGroups: - - 1000 volumes: - name: data hostPath: @@ -35,6 +30,10 @@ spec: type: DirectoryOrCreate - name: tmp emptyDir: {} + - name: start-sh + configMap: + name: hermes-start + defaultMode: 0555 containers: - name: gateway #image: cr.wetofu.me/nousresearch/hermes-agent:v2026.5.16 @@ -43,7 +42,7 @@ spec: httpGet: path: /health port: 8642 - initialDelaySeconds: 30 + initialDelaySeconds: 60 periodSeconds: 10 successThreshold: 1 failureThreshold: 3 @@ -58,6 +57,8 @@ spec: ports: - containerPort: 8642 name: gateway + - containerPort: 9119 + name: dashboard args: - gateway - run @@ -70,6 +71,16 @@ spec: value: "0.0.0.0" - name: API_SERVER_CORS_ORIGINS value: '*' + - name: HERMES_UID + value: '1000' + - name: HERMES_GID + value: '1000' + - name: HERMES_DASHBOARD + value: 'true' + - name: HERMES_DASHBOARD_HOST + value: '::' + - name: HERMES_DASHBOARD_HOST + value: '9119' envFrom: - secretRef: name: hermes @@ -89,6 +100,9 @@ spec: mountPath: /opt/data/workspace/Projects - name: tmp mountPath: /tmp + - name: start-sh + mountPath: /opt/hermes/docker/entrypoint.sh + subPath: entrypoint.sh resources: requests: memory: "1Gi" @@ -96,37 +110,3 @@ spec: limits: memory: "4Gi" cpu: "2" - - name: dashboard - image: image - args: - - dashboard - ports: - - containerPort: 9119 - name: dashboard - env: - - name: TZ - value: Asia/Shanghai - - name: GATEWAY_HEALTH_URL - value: localhost:8642 - - name: GATEWAY_HEALTH_TIMEOUT - value: "3" - - name: HERMES_DASHBOARD_HOST - value: "::" - envFrom: - - secretRef: - name: hermes - optional: true - - configMapRef: - name: hermes - volumeMounts: - - name: data - mountPath: /opt/data - - name: tmp - mountPath: /tmp - resources: - requests: - memory: "256Mi" - cpu: "100m" - limits: - memory: "512Mi" - cpu: "500m" diff --git a/hermes/kustomization.yaml b/hermes/kustomization.yaml index 4ccfdce..472d8b4 100644 --- a/hermes/kustomization.yaml +++ b/hermes/kustomization.yaml @@ -1,6 +1,9 @@ # yaml-language-server: $schema=https://json.schemastore.org/kustomization.json kind: Kustomization namespace: hermes +replicas: +- name: hermes + count: 1 resources: - deploy.yaml - services.yaml @@ -25,3 +28,7 @@ configMapGenerator: - config/TELEGRAM_OBSERVE_UNMENTIONED_GROUP_MESSAGES - config/TELEGRAM_REQUIRE_MENTION - config/SIYUAN_URL + - config/PATH +- name: hermes-start + files: + - config/entrypoint.sh \ No newline at end of file